backtester: Futures handling & FTX Cash and Carry example strategy (#930)

* implements futures functions and GRPC functions on new branch

* lint and test fixes

* Fix uneven split pnl. Adds collateral weight test. docs. New clear func

* Test protection if someone has zero collateral

* Uses string instead of double for accuracy

* Fixes old code panic

* context, match, docs

* Addresses Shazniterinos, var names, expanded tests

* Returns subaccount name, provides USD values when offlinecalc

* Fixes oopsie

* Fixes cool bug which allowed made up subaccount results

* Subaccount override on FTX, subaccount results for collateral

* Strenghten collateral account info checks. Improve FTX test

* English is my first language

* Fixes oopsies

* Adds some conceptual futures order details to track PNL

* Initial design of future order processing in the backtester

* Introduces futures concept for collateral and spot/futures config diffs

* Fixes most tests

* Simple designs for collateral funding pair concept

* Expands interface use so much it hurts

* Implements more collateral interfaces

* Adds liquidation, adds strategy, struggles with Binance

* Attempts at getting FTX to work

* Adds calculatePNL as a wrapper function and adds an `IsFutures` asset check

* Successfully loads backtester with collateral currency

* Fails to really get much going for supporting futures

* Merges master changes

* Fleshes out how FTX processes collateral

* Further FTX collateral workings

* hooks up more ftx collateral and pnl calculations

* more funcs to flesh out handling

* Adds more links, just can't fit the pieces together :(

* Greatly expands futures order processing

* Fleshes out position tracker to also handle asset and exchange +testing

* RM linkedOrderID. rn positioncontroller, unexport

* Successfully tracks futures order positions

* Fails to calculate PNL

* Calculates pnl from orders accurately with exception to flipping orders

* Calculates PNL from orders

* Adds another controller layer to make it ez from orderstore

* Backtester now compiles. Adds test coverage

* labels things add scaling collateral test

* Calculates pnl in line with fees

* Mostly accurate PNL, with exception to appending with diff prices

* Adds locks, adds rpc function

* grpc implementations

* Gracefully handles rpc function

* beautiful tests!

* rejiggles tests to polish

* Finishes FTX testing, adds comments

* Exposes collateral calculations to rpc

* Adds commands and testing for rpcserver.go functions

* Increase testing and fix up backtester code

* Returns cool changes to original branch

* end of day fixes

* Fixing some tests

* Fixing tests 🎉

* Fixes all the tests

* Splits the backtester setup and running into different files

* Merge, minor fixes

* Messing with some strategy updates

* Failed understanding at collateral usage

* Begins the creation of cash and carry strategy

* Adds underlying pair, adds filldependentevent for futures

* Completes fill prerequsite event implementation. Can't short though

* Some bug fixes

* investigating funds

* CAN NOW CREATE A SHORT ORDER

* Minor change in short size

* Fixes for unrealised PNL & collateral rendering

* Fixes lint and tests

* Adds some verbosity

* Updates to pnl calc

* Tracks pnl for short orders, minor update to strategy

* Close and open event based on conditions

* Adds pnl data for currency statistics

* Working through PNL calculation automatically. Now panics

* Adds tracking, is blocked from design

* Work to flesh out closing a position

* vain attempts at tracking zeroing out bugs

* woww, super fun new subloggers 🎉

* Begins attempt at automatically handling contracts and collateral based on direction

* Merge master + fixes

* Investigating issues with pnl and holdings

* Minor pnl fixes

* Fixes future position sizing, needs contract sizing

* Can render pnl results, focussing on funding statistics

* tracking candles for futures, but why not btc

* Improves funding statistics

* Colours and stats

* Fixes collateral and snapshot bugs

* Completes test

* Fixes totals bug

* Fix double buy, expand stats, fixes usd totals, introduce interface

* Begins report formatting and calculations

* Appends pnl to receiving curr. Fixes map[time]. accurate USD

* Improves report output rendering

* PNL stats in report. New tests for futures

* Fixes existing tests before adding new coverage

* Test coverage

* Completes portfolio coverage

* Increase coverage exchange, portfolio. fix size bug. NEW CHART

* WHAT IS GOING ON WITH PNL

* Fixes PNL calculation. Adds ability to skip om futures tracking

* minor commit before merge

* Adds basic liquidation to backtester

* Changes liquidation to order based

* Liquidationnnnnn

* Further fleshes out liquidations

* Completes liquidations in a honorable manner. Adds AppendReasonf

* Beginnings of spot futures gap chart. Needs to link currencies to render difference

* Removes fake liquidation. Adds cool new chart

* Fixes somet tests,allows for zero fee value v nil distinction,New tests

* Some annoying test fixes that took too long

* portfolio coverage

* holding coverage, privatisation funding

* Testwork

* boring tests

* engine coverage

* More backtesting coverage

* Funding, strategy, report test coverage

* Completes coverage of report package

* Documentation, fixes some assumptions on asset errors

* Changes before master merge

* Lint and Tests

* defaults to non-coloured rendering

* Chart rendering

* Fixes surprise non-local-lints

* Niterinos to the extremeos

* Fixes merge problems

* The linter splintered across the glinting plinths

* Many nits addressed. Now sells spot position on final candle

* Adds forgotten coverage

* Adds ability to size futures contracts to match spot positions.

* fixes order sell sizing

* Adds tests to sizing. Fixes charting issue

* clint splintered the linters with flint

* Improves stats, stat rendering

* minifix

* Fixes tests and fee bug

* Merge fixeroos

* Microfixes

* Updates orderPNL on first Correctly utilises fees. Adds committed funds

* New base funcs. New order summary

* Fun test updates

* Fix logo colouring

* Fixes niteroonies

* Fix report

* BAD COMMIT

* Fixes funding issues.Updates default fee rates.Combines cashcarry case

* doc regen

* Now returns err

* Fixes sizing bug issue introduced in PR

* Fixes fun fee/total US value bug

* Fix chart bug. Show log charts with disclaimer

* sellside fee

* fixes fee and slippage view

* Fixed slippage price issue

* Fixes calculation and removes rendering

* Fixes stats and some rendering

* Merge fix

* Fixes merge issues

* go mod tidy, lint updates

* New linter attempt

* Version bump in appveyor and makefile

* Regex filename, config fixes, template h2 fixes

* Removes bad stats.

* neatens config builder. Moves filename generator

* Fixes issue where linter wants to fix my spelling

* Fixes pointers and starts
This commit is contained in:
Scott
2022-06-30 15:43:41 +10:00
committed by GitHub
parent d3339ad0b8
commit f929b4d51e
161 changed files with 15137 additions and 7292 deletions

View File

@@ -55,7 +55,7 @@ before_test:
test_script:
# test back-end
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.45.2
- go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2
- '%GOPATH%\bin\golangci-lint.exe run --verbose'
- ps: >-
if($env:APPVEYOR_SCHEDULED_BUILD -eq 'true') {

View File

@@ -12,4 +12,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.45.2
version: v1.46.2

View File

@@ -1,6 +1,6 @@
LDFLAGS = -ldflags "-w -s"
GCTPKG = github.com/thrasher-corp/gocryptotrader
LINTPKG = github.com/golangci/golangci-lint/cmd/golangci-lint@v1.45.2
LINTPKG = github.com/golangci/golangci-lint/cmd/golangci-lint@v1.46.2
LINTBIN = $(GOPATH)/bin/golangci-lint
GCTLISTENPORT=9050
GCTPROFILERLISTENPORT=8085

View File

@@ -144,7 +144,7 @@ Binaries will be published once the codebase reaches a stable condition.
|User|Contribution Amount|
|--|--|
| [thrasher-](https://github.com/thrasher-) | 666 |
| [shazbert](https://github.com/shazbert) | 248 |
| [shazbert](https://github.com/shazbert) | 249 |
| [gloriousCode](https://github.com/gloriousCode) | 195 |
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 |
| [dependabot[bot]](https://github.com/apps/dependabot) | 73 |

View File

@@ -43,14 +43,17 @@ An event-driven backtesting tool to test and iterate trading strategies using hi
- Compliance manager to keep snapshots of every transaction and their changes at every interval
- Exchange level funding allows funding to be shared across multiple currency pairs and to allow for complex strategy design
- Fund transfer. At a strategy level, transfer funds between exchanges to allow for complex strategy design
- Backtesting support for futures asset types
- Example cash and carry spot futures strategy
## Planned Features
We welcome pull requests on any feature for the Backtester! We will be especially appreciative of any contribution towards the following planned features:
| Feature | Description |
|---------|-------------|
| Add backtesting support for futures asset types | Spot trading is currently the only supported asset type. Futures trading greatly expands the Backtester's potential |
| Example futures pairs trading strategy | Providing a basic example will allow for esteemed traders to build and customise their own |
| Long-running application | Transform the Backtester to run a GRPC server, where commands can be sent to run Backtesting operations. Allowing for many strategies to be run, analysed and tweaked in a more efficient manner |
| Leverage support | Leverage is a good way to enhance profit and loss and is important to include in strategies |
| Enhance config-builder | Create an application that can create strategy configs in a more visual manner and execute them via GRPC to allow for faster customisation of strategies |
| Save Backtester results to database | This will allow for easier comparison of results over time |
| Backtester result comparison report | Providing an executive summary of Backtester database results |
| Currency correlation | Compare multiple exchange, asset, currencies for a candle interval against indicators to highlight correlated pairs for use in pairs trading |

File diff suppressed because it is too large Load Diff

View File

@@ -1,653 +0,0 @@
package backtest
import (
"errors"
"os"
"strings"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/backtester/report"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/database/drivers"
"github.com/thrasher-corp/gocryptotrader/engine"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
const testExchange = "Bitstamp"
var leet *decimal.Decimal
func TestMain(m *testing.M) {
oneThreeThreeSeven := decimal.NewFromInt(1337)
leet = &oneThreeThreeSeven
os.Exit(m.Run())
}
func TestNewFromConfig(t *testing.T) {
t.Parallel()
_, err := NewFromConfig(nil, "", "")
if !errors.Is(err, errNilConfig) {
t.Errorf("received %v, expected %v", err, errNilConfig)
}
cfg := &config.Config{}
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, base.ErrStrategyNotFound) {
t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
}
cfg.CurrencySettings = []config.CurrencySettings{
{
ExchangeName: "test",
Base: "test",
Quote: "test",
},
}
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, engine.ErrExchangeNotFound) {
t.Errorf("received: %v, expected: %v", err, engine.ErrExchangeNotFound)
}
cfg.CurrencySettings[0].ExchangeName = testExchange
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, errInvalidConfigAsset) {
t.Errorf("received: %v, expected: %v", err, errInvalidConfigAsset)
}
cfg.CurrencySettings[0].Asset = asset.Spot.String()
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, currency.ErrPairNotFound) {
t.Errorf("received: %v, expected: %v", err, currency.ErrPairNotFound)
}
cfg.CurrencySettings[0].Base = "btc"
cfg.CurrencySettings[0].Quote = "usd"
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, base.ErrStrategyNotFound) {
t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
}
cfg.StrategySettings = config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
}
cfg.CurrencySettings[0].Base = "BTC"
cfg.CurrencySettings[0].Quote = "USD"
cfg.DataSettings.APIData = &config.APIData{
StartDate: time.Time{},
EndDate: time.Time{},
}
_, err = NewFromConfig(cfg, "", "")
if err != nil && !strings.Contains(err.Error(), "unrecognised dataType") {
t.Error(err)
}
cfg.DataSettings.DataType = common.CandleStr
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, errIntervalUnset) {
t.Errorf("received: %v, expected: %v", err, errIntervalUnset)
}
cfg.DataSettings.Interval = gctkline.OneMin.Duration()
cfg.CurrencySettings[0].MakerFee = decimal.Zero
cfg.CurrencySettings[0].TakerFee = decimal.Zero
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, gctcommon.ErrDateUnset) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrDateUnset)
}
cfg.DataSettings.APIData.StartDate = time.Now().Add(-time.Minute)
cfg.DataSettings.APIData.EndDate = time.Now()
cfg.DataSettings.APIData.InclusiveEndDate = true
_, err = NewFromConfig(cfg, "", "")
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
func TestLoadDataAPI(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot.String(),
Base: cp.Base.String(),
Quote: cp.Quote.String(),
InitialQuoteFunds: leet,
Leverage: config.Leverage{},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: decimal.Zero,
TakerFee: decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin.Duration(),
APIData: &config.APIData{
StartDate: time.Now().Add(-time.Minute),
EndDate: time.Now(),
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil {
t.Error(err)
}
}
func TestLoadDataDatabase(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot.String(),
Base: cp.Base.String(),
Quote: cp.Quote.String(),
InitialQuoteFunds: leet,
Leverage: config.Leverage{},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: decimal.Zero,
TakerFee: decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin.Duration(),
DatabaseData: &config.DatabaseData{
Config: database.Config{
Enabled: true,
Driver: "sqlite3",
ConnectionDetails: drivers.ConnectionDetails{
Database: "gocryptotrader.db",
},
},
StartDate: time.Now().Add(-time.Minute),
EndDate: time.Now(),
InclusiveEndDate: true,
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
bt.databaseManager, err = engine.SetupDatabaseConnectionManager(&cfg.DataSettings.DatabaseData.Config)
if err != nil {
t.Fatal(err)
}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil && !strings.Contains(err.Error(), "unable to retrieve data from GoCryptoTrader database") {
t.Error(err)
}
}
func TestLoadDataCSV(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot.String(),
Base: cp.Base.String(),
Quote: cp.Quote.String(),
InitialQuoteFunds: leet,
Leverage: config.Leverage{},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: decimal.Zero,
TakerFee: decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin.Duration(),
CSVData: &config.CSVData{
FullPath: "test",
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil &&
!strings.Contains(err.Error(), "The system cannot find the file specified.") &&
!strings.Contains(err.Error(), "no such file or directory") {
t.Error(err)
}
}
func TestLoadDataLive(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
shutdown: make(chan struct{}),
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot.String(),
Base: cp.Base.String(),
Quote: cp.Quote.String(),
InitialQuoteFunds: leet,
Leverage: config.Leverage{},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: decimal.Zero,
TakerFee: decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin.Duration(),
LiveData: &config.LiveData{
APIKeyOverride: "test",
APISecretOverride: "test",
APIClientIDOverride: "test",
API2FAOverride: "test",
RealOrders: true,
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil {
t.Error(err)
}
bt.Stop()
}
func TestLoadLiveData(t *testing.T) {
t.Parallel()
err := loadLiveData(nil, nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
cfg := &config.Config{}
err = loadLiveData(cfg, nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
b := &gctexchange.Base{
Name: testExchange,
API: gctexchange.API{
AuthenticatedSupport: false,
AuthenticatedWebsocketSupport: false,
PEMKeySupport: false,
CredentialsValidator: struct {
RequiresPEM bool
RequiresKey bool
RequiresSecret bool
RequiresClientID bool
RequiresBase64DecodeSecret bool
}{
RequiresPEM: true,
RequiresKey: true,
RequiresSecret: true,
RequiresClientID: true,
RequiresBase64DecodeSecret: true,
},
},
}
err = loadLiveData(cfg, b)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
cfg.DataSettings.LiveData = &config.LiveData{
RealOrders: true,
}
cfg.DataSettings.Interval = gctkline.OneDay.Duration()
cfg.DataSettings.DataType = common.CandleStr
err = loadLiveData(cfg, b)
if err != nil {
t.Error(err)
}
cfg.DataSettings.LiveData.APIKeyOverride = "1234"
cfg.DataSettings.LiveData.APISecretOverride = "1234"
cfg.DataSettings.LiveData.APIClientIDOverride = "1234"
cfg.DataSettings.LiveData.API2FAOverride = "1234"
cfg.DataSettings.LiveData.APISubAccountOverride = "1234"
err = loadLiveData(cfg, b)
if err != nil {
t.Error(err)
}
}
func TestReset(t *testing.T) {
t.Parallel()
f := funding.SetupFundingManager(true, false)
bt := BackTest{
shutdown: make(chan struct{}),
Datas: &data.HandlerPerCurrency{},
Strategy: &dollarcostaverage.Strategy{},
Portfolio: &portfolio.Portfolio{},
Exchange: &exchange.Exchange{},
Statistic: &statistics.Statistic{},
EventQueue: &eventholder.Holder{},
Reports: &report.Data{},
Funding: f,
}
bt.Reset()
if bt.Funding.IsUsingExchangeLevelFunding() {
t.Error("expected false")
}
}
func TestFullCycle(t *testing.T) {
t.Parallel()
ex := testExchange
cp := currency.NewPair(currency.BTC, currency.USD)
a := asset.Spot
tt := time.Now()
stats := &statistics.Statistic{}
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
port, err := portfolio.Setup(&size.Size{
BuySide: exchange.MinMax{},
SellSide: exchange.MinMax{},
}, &risk.Risk{}, decimal.Zero)
if err != nil {
t.Error(err)
}
_, err = port.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ex, Asset: a, Pair: cp})
if err != nil {
t.Error(err)
}
f := funding.SetupFundingManager(false, true)
b, err := funding.CreateItem(ex, a, cp.Base, decimal.Zero, decimal.Zero)
if err != nil {
t.Error(err)
}
quote, err := funding.CreateItem(ex, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if err != nil {
t.Error(err)
}
pair, err := funding.CreatePair(b, quote)
if err != nil {
t.Error(err)
}
err = f.AddPair(pair)
if err != nil {
t.Error(err)
}
bt := BackTest{
shutdown: nil,
Datas: &data.HandlerPerCurrency{},
Strategy: &dollarcostaverage.Strategy{},
Portfolio: port,
Exchange: &exchange.Exchange{},
Statistic: stats,
EventQueue: &eventholder.Holder{},
Reports: &report.Data{},
Funding: f,
}
bt.Datas.Setup()
k := kline.DataFromKline{
Item: gctkline.Item{
Exchange: ex,
Pair: cp,
Asset: a,
Interval: gctkline.FifteenMin,
Candles: []gctkline.Candle{{
Time: tt,
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
}},
},
Base: data.Base{},
RangeHolder: &gctkline.IntervalRangeHolder{
Start: gctkline.CreateIntervalTime(tt),
End: gctkline.CreateIntervalTime(tt.Add(gctkline.FifteenMin.Duration())),
Ranges: []gctkline.IntervalRange{
{
Start: gctkline.CreateIntervalTime(tt),
End: gctkline.CreateIntervalTime(tt.Add(gctkline.FifteenMin.Duration())),
Intervals: []gctkline.IntervalData{
{
Start: gctkline.CreateIntervalTime(tt),
End: gctkline.CreateIntervalTime(tt.Add(gctkline.FifteenMin.Duration())),
HasData: true,
},
},
},
},
},
}
err = k.Load()
if err != nil {
t.Error(err)
}
bt.Datas.SetDataForCurrency(ex, a, cp, &k)
err = bt.Run()
if err != nil {
t.Error(err)
}
}
func TestStop(t *testing.T) {
t.Parallel()
bt := BackTest{shutdown: make(chan struct{})}
bt.Stop()
}
func TestFullCycleMulti(t *testing.T) {
t.Parallel()
ex := testExchange
cp := currency.NewPair(currency.BTC, currency.USD)
a := asset.Spot
tt := time.Now()
stats := &statistics.Statistic{}
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
port, err := portfolio.Setup(&size.Size{
BuySide: exchange.MinMax{},
SellSide: exchange.MinMax{},
}, &risk.Risk{}, decimal.Zero)
if err != nil {
t.Error(err)
}
_, err = port.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ex, Asset: a, Pair: cp})
if err != nil {
t.Error(err)
}
f := funding.SetupFundingManager(false, true)
b, err := funding.CreateItem(ex, a, cp.Base, decimal.Zero, decimal.Zero)
if err != nil {
t.Error(err)
}
quote, err := funding.CreateItem(ex, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if err != nil {
t.Error(err)
}
pair, err := funding.CreatePair(b, quote)
if err != nil {
t.Error(err)
}
err = f.AddPair(pair)
if err != nil {
t.Error(err)
}
bt := BackTest{
shutdown: nil,
Datas: &data.HandlerPerCurrency{},
Portfolio: port,
Exchange: &exchange.Exchange{},
Statistic: stats,
EventQueue: &eventholder.Holder{},
Reports: &report.Data{},
Funding: f,
}
bt.Strategy, err = strategies.LoadStrategyByName(dollarcostaverage.Name, true)
if err != nil {
t.Error(err)
}
bt.Datas.Setup()
k := kline.DataFromKline{
Item: gctkline.Item{
Exchange: ex,
Pair: cp,
Asset: a,
Interval: gctkline.FifteenMin,
Candles: []gctkline.Candle{{
Time: tt,
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
}},
},
Base: data.Base{},
RangeHolder: &gctkline.IntervalRangeHolder{
Start: gctkline.CreateIntervalTime(tt),
End: gctkline.CreateIntervalTime(tt.Add(gctkline.FifteenMin.Duration())),
Ranges: []gctkline.IntervalRange{
{
Start: gctkline.CreateIntervalTime(tt),
End: gctkline.CreateIntervalTime(tt.Add(gctkline.FifteenMin.Duration())),
Intervals: []gctkline.IntervalData{
{
Start: gctkline.CreateIntervalTime(tt),
End: gctkline.CreateIntervalTime(tt.Add(gctkline.FifteenMin.Duration())),
HasData: true,
},
},
},
},
},
}
err = k.Load()
if err != nil {
t.Error(err)
}
bt.Datas.SetDataForCurrency(ex, a, cp, &k)
err = bt.Run()
if err != nil {
t.Error(err)
}
}

View File

@@ -1,6 +1,19 @@
package common
import "fmt"
import (
"fmt"
"regexp"
"strings"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
// CanTransact checks whether an order side is valid
// to the backtester's standards
func CanTransact(side gctorder.Side) bool {
return side.IsLong() || side.IsShort() || side == gctorder.ClosePosition
}
// DataTypeToInt converts the config string value into an int
func DataTypeToInt(dataType string) (int64, error) {
@@ -13,3 +26,151 @@ func DataTypeToInt(dataType string) (int64, error) {
return 0, fmt.Errorf("unrecognised dataType '%v'", dataType)
}
}
// GenerateFileName will convert a proposed filename into something that is more
// OS friendly
func GenerateFileName(fileName, extension string) (string, error) {
if fileName == "" {
return "", fmt.Errorf("%w missing filename", errCannotGenerateFileName)
}
if extension == "" {
return "", fmt.Errorf("%w missing filename extension", errCannotGenerateFileName)
}
reg := regexp.MustCompile(`[\w-]`)
parsedFileName := reg.FindAllString(fileName, -1)
parsedExtension := reg.FindAllString(extension, -1)
fileName = strings.Join(parsedFileName, "") + "." + strings.Join(parsedExtension, "")
return strings.ToLower(fileName), nil
}
// FitStringToLimit ensures a string is of the length of the limit
// either by truncating the string with ellipses or padding with the spacer
func FitStringToLimit(str, spacer string, limit int, upper bool) string {
if limit < 0 {
return str
}
if limit == 0 {
return ""
}
limResp := limit - len(str)
if upper {
str = strings.ToUpper(str)
}
if limResp < 0 {
if limit-3 > 0 {
return str[0:limit-3] + "..."
}
return str[0:limit]
}
spacerLen := len(spacer)
for i := 0; i < limResp; i++ {
str += spacer
for j := 0; j < spacerLen; j++ {
if j > 0 {
// prevent clever people from going beyond
// the limit by having a spacer longer than 1
i++
}
}
}
return str[0:limit]
}
// RegisterBacktesterSubLoggers sets up all custom Backtester sub-loggers
func RegisterBacktesterSubLoggers() error {
var err error
Backtester, err = log.NewSubLogger("Backtester")
if err != nil {
return err
}
Setup, err = log.NewSubLogger("Setup")
if err != nil {
return err
}
Strategy, err = log.NewSubLogger("Strategy")
if err != nil {
return err
}
Report, err = log.NewSubLogger("Report")
if err != nil {
return err
}
Statistics, err = log.NewSubLogger("Statistics")
if err != nil {
return err
}
CurrencyStatistics, err = log.NewSubLogger("CurrencyStatistics")
if err != nil {
return err
}
FundingStatistics, err = log.NewSubLogger("FundingStatistics")
if err != nil {
return err
}
Backtester, err = log.NewSubLogger("Sizing")
if err != nil {
return err
}
Holdings, err = log.NewSubLogger("Holdings")
if err != nil {
return err
}
Data, err = log.NewSubLogger("Data")
if err != nil {
return err
}
// Set to existing registered sub-loggers
Config = log.ConfigMgr
Portfolio = log.PortfolioMgr
Exchange = log.ExchangeSys
Fill = log.Fill
return nil
}
// PurgeColours removes colour information
func PurgeColours() {
ColourGreen = ""
ColourWhite = ""
ColourGrey = ""
ColourDefault = ""
ColourH1 = ""
ColourH2 = ""
ColourH3 = ""
ColourH4 = ""
ColourSuccess = ""
ColourInfo = ""
ColourDebug = ""
ColourWarn = ""
ColourDarkGrey = ""
ColourError = ""
}
// Logo returns the logo
func Logo() string {
sb := strings.Builder{}
sb.WriteString(" \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@@ \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@@@@@@@@ " + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@" + ColourGrey + ",,,,, " + ColourWhite + "@@@@@@@@@" + ColourGrey + ",,,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@" + ColourGrey + ",,,,,,, " + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@" + ColourGrey + "(,,,,,,,, " + ColourGrey + ",," + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",," + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,, #,,,,,,,,,,,,,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,,,,,,,,,,,,,,,,,,," + ColourGreen + "%%%%%%%" + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",,,,,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,,,,,,," + ColourGreen + "%%%%%" + ColourGrey + " ,,,,,," + ColourGrey + "%" + ColourGreen + "%%%%%%" + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",,,,,,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,,,," + ColourGreen + "%%%%%%%%%%%%%%%%%%" + ColourGrey + "#" + ColourGreen + "%%" + ColourGrey + " \n")
sb.WriteString(" " + ColourGrey + ",,,,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,," + ColourGreen + "%%%" + ColourGrey + " ,,,,," + ColourGreen + "%%%%%%%%" + ColourGrey + ",,,,, \n")
sb.WriteString(" " + ColourGrey + ",,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,," + ColourGreen + "%%" + ColourGrey + ",, ,,,,,,," + ColourWhite + "@" + ColourGreen + "*%%," + ColourWhite + "@" + ColourGrey + ",,,,,, \n")
sb.WriteString(" " + ColourGrey + "*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,, " + ColourGrey + ",,,,," + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,, " + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@" + ColourGrey + ",,,,,,, " + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@" + ColourGrey + ",,,, " + ColourWhite + "@@@@@@@@@" + ColourGrey + "#,,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@@@@@@@@ " + ColourGrey + "*,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@" + ColourDefault + " \n")
sb.WriteString(ASCIILogo)
return sb.String()
}

View File

@@ -1,11 +1,104 @@
package common
import (
"errors"
"fmt"
"testing"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
func TestCanTransact(t *testing.T) {
t.Parallel()
for _, ti := range []struct {
side gctorder.Side
expected bool
}{
{
side: gctorder.UnknownSide,
expected: false,
},
{
side: gctorder.Buy,
expected: true,
},
{
side: gctorder.Sell,
expected: true,
},
{
side: gctorder.Bid,
expected: true,
},
{
side: gctorder.Ask,
expected: true,
},
{
// while anyside can work in GCT, it's a no for the backtester
side: gctorder.AnySide,
expected: false,
},
{
side: gctorder.Long,
expected: true,
},
{
side: gctorder.Short,
expected: true,
},
{
side: gctorder.ClosePosition,
expected: true,
},
{
side: gctorder.DoNothing,
expected: false,
},
{
side: gctorder.TransferredFunds,
expected: false,
},
{
side: gctorder.CouldNotBuy,
expected: false,
},
{
side: gctorder.CouldNotSell,
expected: false,
},
{
side: gctorder.CouldNotShort,
expected: false,
},
{
side: gctorder.CouldNotLong,
expected: false,
},
{
side: gctorder.CouldNotCloseShort,
expected: false,
},
{
side: gctorder.CouldNotCloseLong,
expected: false,
},
{
side: gctorder.MissingData,
expected: false,
},
} {
t.Run(ti.side.String(), func(t *testing.T) {
t.Parallel()
if CanTransact(ti.side) != ti.expected {
t.Errorf("received '%v' expected '%v'", ti.side, ti.expected)
}
})
}
}
func TestDataTypeConversion(t *testing.T) {
t.Parallel()
for _, ti := range []struct {
title string
dataType string
@@ -30,6 +123,7 @@ func TestDataTypeConversion(t *testing.T) {
},
} {
t.Run(ti.title, func(t *testing.T) {
t.Parallel()
got, err := DataTypeToInt(ti.dataType)
if ti.expectErr {
if err == nil {
@@ -43,3 +137,110 @@ func TestDataTypeConversion(t *testing.T) {
})
}
}
func TestFitStringToLimit(t *testing.T) {
t.Parallel()
for _, ti := range []struct {
str string
sep string
limit int
expected string
upper bool
}{
{
str: "good",
sep: " ",
limit: 5,
expected: "GOOD ",
upper: true,
},
{
str: "negative limit",
sep: " ",
limit: -1,
expected: "negative limit",
},
{
str: "long spacer",
sep: "--",
limit: 14,
expected: "long spacer---",
},
{
str: "zero limit",
sep: "--",
limit: 0,
expected: "",
},
{
str: "over limit",
sep: "--",
limit: 6,
expected: "ove...",
},
{
str: "hi",
sep: " ",
limit: 1,
expected: "h",
},
} {
test := ti
t.Run(test.str, func(t *testing.T) {
t.Parallel()
result := FitStringToLimit(test.str, test.sep, test.limit, test.upper)
if result != test.expected {
t.Errorf("received '%v' expected '%v'", result, test.expected)
}
})
}
}
func TestLogo(t *testing.T) {
colourLogo := Logo()
if colourLogo == "" {
t.Error("expected a logo")
}
PurgeColours()
if len(colourLogo) == len(Logo()) {
t.Error("expected logo with colours removed")
}
}
func TestPurgeColours(t *testing.T) {
PurgeColours()
if ColourSuccess != "" {
t.Error("expected purged colour")
}
}
func TestGenerateFileName(t *testing.T) {
t.Parallel()
_, err := GenerateFileName("", "")
if !errors.Is(err, errCannotGenerateFileName) {
t.Errorf("received '%v' expected '%v'", err, errCannotGenerateFileName)
}
_, err = GenerateFileName("hello", "")
if !errors.Is(err, errCannotGenerateFileName) {
t.Errorf("received '%v' expected '%v'", err, errCannotGenerateFileName)
}
_, err = GenerateFileName("", "moto")
if !errors.Is(err, errCannotGenerateFileName) {
t.Errorf("received '%v' expected '%v'", err, errCannotGenerateFileName)
}
_, err = GenerateFileName("hello", "moto")
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
name, err := GenerateFileName("......HELL0. + _", "moto.")
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if name != "hell0_.moto" {
t.Errorf("received '%v' expected '%v'", name, "hell0_.moto")
}
}

View File

@@ -5,10 +5,12 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
@@ -16,10 +18,8 @@ const (
CandleStr = "candle"
// TradeStr is a config readable data type to tell the backtester to retrieve trade data
TradeStr = "trade"
)
// DataCandle is an int64 representation of a candle data type
const (
// DataCandle is an int64 representation of a candle data type
DataCandle = iota
DataTrade
)
@@ -32,25 +32,50 @@ var (
ErrNilEvent = errors.New("nil event received")
// ErrInvalidDataType occurs when an invalid data type is defined in the config
ErrInvalidDataType = errors.New("invalid datatype received")
errCannotGenerateFileName = errors.New("cannot generate filename")
)
// EventHandler interface implements required GetTime() & Pair() return
type EventHandler interface {
GetBase() *event.Base
GetOffset() int64
SetOffset(int64)
IsEvent() bool
GetTime() time.Time
Pair() currency.Pair
GetUnderlyingPair() currency.Pair
GetExchange() string
GetInterval() kline.Interval
GetAssetType() asset.Item
GetReason() string
GetConcatReasons() string
GetReasons() []string
GetClosePrice() decimal.Decimal
AppendReason(string)
AppendReasonf(string, ...interface{})
}
// custom subloggers for backtester use
var (
Backtester *log.SubLogger
Setup *log.SubLogger
Strategy *log.SubLogger
Config *log.SubLogger
Portfolio *log.SubLogger
Exchange *log.SubLogger
Fill *log.SubLogger
Report *log.SubLogger
Statistics *log.SubLogger
CurrencyStatistics *log.SubLogger
FundingStatistics *log.SubLogger
Holdings *log.SubLogger
Data *log.SubLogger
)
// DataEventHandler interface used for loading and interacting with Data
type DataEventHandler interface {
EventHandler
GetUnderlyingPair() currency.Pair
GetClosePrice() decimal.Decimal
GetHighPrice() decimal.Decimal
GetLowPrice() decimal.Decimal
@@ -63,27 +88,26 @@ type Directioner interface {
GetDirection() order.Side
}
// colours to display for the terminal output
var (
ColourDefault = "\u001b[0m"
ColourGreen = "\033[38;5;157m"
ColourWhite = "\033[38;5;255m"
ColourGrey = "\033[38;5;240m"
ColourDarkGrey = "\033[38;5;243m"
ColourH1 = "\033[38;5;33m"
ColourH2 = "\033[38;5;39m"
ColourH3 = "\033[38;5;45m"
ColourH4 = "\033[38;5;51m"
ColourSuccess = "\033[38;5;40m"
ColourInfo = "\u001B[32m"
ColourDebug = "\u001B[34m"
ColourWarn = "\u001B[33m"
ColourError = "\033[38;5;196m"
)
// ASCIILogo is a sweet logo that is optionally printed to the command line window
const ASCIILogo = `
@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@ ,,,,,,
@@@@@@@@,,,,, @@@@@@@@@,,,,,,,,
@@@@@@@@,,,,,,, @@@@@@@,,,,,,,
@@@@@@(,,,,,,,, ,,@@@@@@@,,,,,,
,,@@@@@@,,,,,,,,, #,,,,,,,,,,,,,,,,,
,,,,*@@@@@@,,,,,,,,,,,,,,,,,,,,,,,,,,%%%%%%%
,,,,,,,*@@@@@@,,,,,,,,,,,,,,%%%%%,,,,,,%%%%%%%%
,,,,,,,,*@@@@@@,,,,,,,,,,,%%%%%%%%%%%%%%%%%%#%%
,,,,,,*@@@@@@,,,,,,,,,%%%,,,,,%%%%%%%%,,,,,
,,,*@@@@@@,,,,,,%%, ,,,,,,,@*%%,@,,,,,,
*@@@@@@,,,,,,,,, ,,,,@@@@@@,,,,,,
@@@@@@,,,,,,,,, @@@@@@@,,,,,,
@@@@@@@@,,,,,,, @@@@@@@,,,,,,,
@@@@@@@@@,,,, @@@@@@@@@#,,,,,,,
@@@@@@@@@@@@@@@@@@@@@@@ *,,,,
@@@@@@@@@@@@@@@@
______ ______ __ ______ __
/ ____/___ / ____/______ ______ / /_____/_ __/________ _____/ /__ _____
/ / __/ __ \/ / / ___/ / / / __ \/ __/ __ \/ / / ___/ __ / __ / _ \/ ___/
@@ -95,4 +119,5 @@ const ASCIILogo = `
/ __ / __ / ___/ //_/ __/ _ \/ ___/ __/ _ \/ ___/
/ /_/ / /_/ / /__/ ,< / /_/ __(__ ) /_/ __/ /
/_____/\__,_/\___/_/|_|\__/\___/____/\__/\___/_/
`

View File

@@ -44,9 +44,9 @@ See below for a set of tables and fields, expected values and what they can do
| Goal | A description of what you would hope the outcome to be. When verifying output, you can review and confirm whether the strategy met that goal |
| CurrencySettings | Currency settings is an array of settings for each individual currency you wish to run the strategy against |
| StrategySettings | Select which strategy to run, what custom settings to load and whether the strategy can assess multiple currencies at once to make more in-depth decisions |
| FundingSettings | Defines whether individual funding settings can be used. Defines the funding exchange, asset, currencies at an individual level |
| PortfolioSettings | Contains a list of global rules for the portfolio manager. CurrencySettings contain their own rules on things like how big a position is allowable, the portfolio manager rules are the same, but override any individual currency's settings |
| StatisticSettings | Contains settings that impact statistics calculation. Such as the risk-free rate for the sharpe ratio |
| GoCryptoTraderConfigPath | The filepath for the location of GoCryptoTrader's config path. The Backtester utilises settings from GoCryptoTrader. If unset, will utilise the default filepath via `config.DefaultFilePath`, implemented [here](/config/config.go#L1460) |
#### Strategy Settings
@@ -56,11 +56,18 @@ See below for a set of tables and fields, expected values and what they can do
| Name | The strategy to use | `rsi` |
| UsesSimultaneousProcessing | This denotes whether multiple currencies are processed simultaneously with the strategy function `OnSimultaneousSignals`. Eg If you have multiple CurrencySettings and only wish to purchase BTC-USDT when XRP-DOGE is 1337, this setting is useful as you can analyse both signal events to output a purchase call for BTC | `true` |
| CustomSettings | This is a map where you can enter custom settings for a strategy. The RSI strategy allows for customisation of the upper, lower and length variables to allow you to change them from 70, 30 and 14 respectively to 69, 36, 12 | `"custom-settings": { "rsi-high": 70, "rsi-low": 30, "rsi-period": 14 } ` |
| UseExchangeLevelFunding | Allows shared funding at an exchange asset level. You can set funding for `USDT` and all pairs that feature `USDT` will have access to those funds when making orders. See [this](/backtester/funding/README.md) for more information | `false` |
| ExchangeLevelFunding | An array of exchange level funding settings. See below, or [this](/backtester/funding/README.md) for more information | `[]` |
| DisableUSDTracking | If `false`, will track all currencies used in your strategy against USD equivalent candles. For example, if you are running a strategy for BTC/XRP, then the GoCryptoTrader Backtester will also retreive candles data for BTC/USD and XRP/USD to then track strategy performance against a single currency. This also tracks against USDT and other USD tracked stablecoins, so one exchange supporting USDT and another BUSD will still allow unified strategy performance analysis. If disabled, will not track against USD, this can be especially helpful when running strategies under live, database and CSV based data | `false` |
##### Funding Config Settings
#### Funding Config Settings
| Key | Description | Example |
| --- | ------- | --- |
| UseExchangeLevelFunding | Allows shared funding at an exchange asset level. You can set funding for `USDT` and all pairs that feature `USDT` will have access to those funds when making orders. See [this](/backtester/funding/README.md) for more information | `false` |
| ExchangeLevelFunding | An array of exchange level funding settings. See below, or [this](/backtester/funding/README.md) for more information | `[]` |
##### Funding Item Config Settings
| Key | Description | Example |
| --- | ------- | ----- |
@@ -80,18 +87,30 @@ See below for a set of tables and fields, expected values and what they can do
| Base | The base of a currency | `BTC` |
| Quote | The quote of a currency | `USDT` |
| InitialFunds | A legacy field, will be temporarily migrated to `InitialQuoteFunds` if present in your strat config | `` |
| InitialBaseFunds | The funds that the GoCryptoTraderBacktester has for the base currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `2` |
| InitialQuoteFunds | The funds that the GoCryptoTraderBacktester has for the quote currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `10000` |
| Leverage | This struct defines the leverage rules that this specific currency setting must abide by | `1` |
| BuySide | This struct defines the buying side rules this specific currency setting must abide by such as maximum purchase amount | - |
| SellSide | This struct defines the selling side rules this specific currency setting must abide by such as maximum selling amount | - |
| MinimumSlippagePercent | Is the lower bounds in a random number generated that make purchases more expensive, or sell events less valuable. If this value is 90, then the most a price can be affected is 10% | `90` |
| MaximumSlippagePercent | Is the upper bounds in a random number generated that make purchases more expensive, or sell events less valuable. If this value is 99, then the least a price can be affected is 1%. Set both upper and lower to 100 to have no randomness applied to purchase events | `100` |
| MakerFee | The fee to use when sizing and purchasing currency | `0.001` |
| TakerFee | Unused fee for when an order is placed in the orderbook, rather than taken from the orderbook | `0.002` |
| MakerFee | The fee to use when sizing and purchasing currency. If `nil`, will lookup an exchange's fee details | `0.001` |
| TakerFee | Unused fee for when an order is placed in the orderbook, rather than taken from the orderbook. If `nil`, will lookup an exchange's fee details | `0.002` |
| MaximumHoldingsRatio | When multiple currency settings are used, you may set a maximum holdings ratio to prevent having too large a stake in a single currency | `0.5` |
| CanUseExchangeLimits | Will lookup exchange rules around purchase sizing eg minimum order increments of 0.0005. Note: Will retrieve up-to-date rules which may not have existed for the data you are using. Best to use this when considering to use this strategy live | `false` |
| SkipCandleVolumeFitting | When placing orders, by default the BackTester will shrink an order's size to fit the candle data's volume so as to not rewrite history. Set this to `true` to ignore this and to set order size at what the portfolio manager prescribes | `false` |
| SpotSettings | An optional field which contains initial funding data for SPOT currency pairs | See SpotSettings table below |
| FuturesSettings | An optional field which contains leverage data for FUTURES currency pairs | See FuturesSettings table below |
##### SpotSettings
| Key | Description | Example |
| --- | ------- | ----- |
| InitialBaseFunds | The funds that the GoCryptoTraderBacktester has for the base currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `2` |
| InitialQuoteFunds | The funds that the GoCryptoTraderBacktester has for the quote currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `10000` |
##### FuturesSettings
| Key | Description | Example |
| --- | ------- | ----- |
| Leverage | This struct defines the leverage rules that this specific currency setting must abide by | `1` |
#### PortfolioSettings

View File

@@ -8,10 +8,12 @@ import (
"strings"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -35,112 +37,6 @@ func LoadConfig(data []byte) (resp *Config, err error) {
return resp, err
}
// PrintSetting prints relevant settings to the console for easy reading
func (c *Config) PrintSetting() {
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------Backtester Settings------------------------")
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------Strategy Settings--------------------------")
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Infof(log.BackTester, "Strategy: %s", c.StrategySettings.Name)
if len(c.StrategySettings.CustomSettings) > 0 {
log.Info(log.BackTester, "Custom strategy variables:")
for k, v := range c.StrategySettings.CustomSettings {
log.Infof(log.BackTester, "%s: %v", k, v)
}
} else {
log.Info(log.BackTester, "Custom strategy variables: unset")
}
log.Infof(log.BackTester, "Simultaneous Signal Processing: %v", c.StrategySettings.SimultaneousSignalProcessing)
log.Infof(log.BackTester, "Use Exchange Level Funding: %v", c.StrategySettings.UseExchangeLevelFunding)
log.Infof(log.BackTester, "USD value tracking: %v", !c.StrategySettings.DisableUSDTracking)
if c.StrategySettings.UseExchangeLevelFunding && c.StrategySettings.SimultaneousSignalProcessing {
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------Funding Settings---------------------------")
for i := range c.StrategySettings.ExchangeLevelFunding {
log.Infof(log.BackTester, "Initial funds for %v %v %v: %v",
c.StrategySettings.ExchangeLevelFunding[i].ExchangeName,
c.StrategySettings.ExchangeLevelFunding[i].Asset,
c.StrategySettings.ExchangeLevelFunding[i].Currency,
c.StrategySettings.ExchangeLevelFunding[i].InitialFunds.Round(8))
}
}
for i := range c.CurrencySettings {
log.Info(log.BackTester, "-------------------------------------------------------------")
currStr := fmt.Sprintf("------------------%v %v-%v Currency Settings---------------------------------------------------------",
c.CurrencySettings[i].Asset,
c.CurrencySettings[i].Base,
c.CurrencySettings[i].Quote)
log.Infof(log.BackTester, currStr[:61])
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Infof(log.BackTester, "Exchange: %v", c.CurrencySettings[i].ExchangeName)
if !c.StrategySettings.UseExchangeLevelFunding {
if c.CurrencySettings[i].InitialBaseFunds != nil {
log.Infof(log.BackTester, "Initial base funds: %v %v",
c.CurrencySettings[i].InitialBaseFunds.Round(8),
c.CurrencySettings[i].Base)
}
if c.CurrencySettings[i].InitialQuoteFunds != nil {
log.Infof(log.BackTester, "Initial quote funds: %v %v",
c.CurrencySettings[i].InitialQuoteFunds.Round(8),
c.CurrencySettings[i].Quote)
}
}
log.Infof(log.BackTester, "Maker fee: %v", c.CurrencySettings[i].TakerFee.Round(8))
log.Infof(log.BackTester, "Taker fee: %v", c.CurrencySettings[i].MakerFee.Round(8))
log.Infof(log.BackTester, "Minimum slippage percent %v", c.CurrencySettings[i].MinimumSlippagePercent.Round(8))
log.Infof(log.BackTester, "Maximum slippage percent: %v", c.CurrencySettings[i].MaximumSlippagePercent.Round(8))
log.Infof(log.BackTester, "Buy rules: %+v", c.CurrencySettings[i].BuySide)
log.Infof(log.BackTester, "Sell rules: %+v", c.CurrencySettings[i].SellSide)
log.Infof(log.BackTester, "Leverage rules: %+v", c.CurrencySettings[i].Leverage)
log.Infof(log.BackTester, "Can use exchange defined order execution limits: %+v", c.CurrencySettings[i].CanUseExchangeLimits)
}
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------Portfolio Settings-------------------------")
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Infof(log.BackTester, "Buy rules: %+v", c.PortfolioSettings.BuySide)
log.Infof(log.BackTester, "Sell rules: %+v", c.PortfolioSettings.SellSide)
log.Infof(log.BackTester, "Leverage rules: %+v", c.PortfolioSettings.Leverage)
if c.DataSettings.LiveData != nil {
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------Live Settings------------------------------")
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Infof(log.BackTester, "Data type: %v", c.DataSettings.DataType)
log.Infof(log.BackTester, "Interval: %v", c.DataSettings.Interval)
log.Infof(log.BackTester, "REAL ORDERS: %v", c.DataSettings.LiveData.RealOrders)
log.Infof(log.BackTester, "Overriding GCT API settings: %v", c.DataSettings.LiveData.APIClientIDOverride != "")
}
if c.DataSettings.APIData != nil {
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------API Settings-------------------------------")
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Infof(log.BackTester, "Data type: %v", c.DataSettings.DataType)
log.Infof(log.BackTester, "Interval: %v", c.DataSettings.Interval)
log.Infof(log.BackTester, "Start date: %v", c.DataSettings.APIData.StartDate.Format(gctcommon.SimpleTimeFormat))
log.Infof(log.BackTester, "End date: %v", c.DataSettings.APIData.EndDate.Format(gctcommon.SimpleTimeFormat))
}
if c.DataSettings.CSVData != nil {
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------CSV Settings-------------------------------")
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Infof(log.BackTester, "Data type: %v", c.DataSettings.DataType)
log.Infof(log.BackTester, "Interval: %v", c.DataSettings.Interval)
log.Infof(log.BackTester, "CSV file: %v", c.DataSettings.CSVData.FullPath)
}
if c.DataSettings.DatabaseData != nil {
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Info(log.BackTester, "------------------Database Settings--------------------------")
log.Info(log.BackTester, "-------------------------------------------------------------")
log.Infof(log.BackTester, "Data type: %v", c.DataSettings.DataType)
log.Infof(log.BackTester, "Interval: %v", c.DataSettings.Interval)
log.Infof(log.BackTester, "Start date: %v", c.DataSettings.DatabaseData.StartDate.Format(gctcommon.SimpleTimeFormat))
log.Infof(log.BackTester, "End date: %v", c.DataSettings.DatabaseData.EndDate.Format(gctcommon.SimpleTimeFormat))
}
log.Info(log.BackTester, "-------------------------------------------------------------\n\n")
}
// Validate checks all config settings
func (c *Config) Validate() error {
err := c.validateDate()
@@ -207,23 +103,23 @@ func (c *Config) validateMinMaxes() (err error) {
}
func (c *Config) validateStrategySettings() error {
if c.StrategySettings.UseExchangeLevelFunding && !c.StrategySettings.SimultaneousSignalProcessing {
if c.FundingSettings.UseExchangeLevelFunding && !c.StrategySettings.SimultaneousSignalProcessing {
return errSimultaneousProcessingRequired
}
if len(c.StrategySettings.ExchangeLevelFunding) > 0 && !c.StrategySettings.UseExchangeLevelFunding {
if len(c.FundingSettings.ExchangeLevelFunding) > 0 && !c.FundingSettings.UseExchangeLevelFunding {
return errExchangeLevelFundingRequired
}
if c.StrategySettings.UseExchangeLevelFunding && len(c.StrategySettings.ExchangeLevelFunding) == 0 {
if c.FundingSettings.UseExchangeLevelFunding && len(c.FundingSettings.ExchangeLevelFunding) == 0 {
return errExchangeLevelFundingDataRequired
}
if c.StrategySettings.UseExchangeLevelFunding {
for i := range c.StrategySettings.ExchangeLevelFunding {
if c.StrategySettings.ExchangeLevelFunding[i].InitialFunds.IsNegative() {
if c.FundingSettings.UseExchangeLevelFunding {
for i := range c.FundingSettings.ExchangeLevelFunding {
if c.FundingSettings.ExchangeLevelFunding[i].InitialFunds.IsNegative() {
return fmt.Errorf("%w for %v %v %v",
errBadInitialFunds,
c.StrategySettings.ExchangeLevelFunding[i].ExchangeName,
c.StrategySettings.ExchangeLevelFunding[i].Asset,
c.StrategySettings.ExchangeLevelFunding[i].Currency,
c.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
c.FundingSettings.ExchangeLevelFunding[i].Asset,
c.FundingSettings.ExchangeLevelFunding[i].Currency,
)
}
}
@@ -268,51 +164,61 @@ func (c *Config) validateCurrencySettings() error {
if len(c.CurrencySettings) == 0 {
return errNoCurrencySettings
}
var hasFutures, hasSlippage bool
for i := range c.CurrencySettings {
if c.CurrencySettings[i].InitialLegacyFunds > 0 {
// temporarily migrate legacy start config value
log.Warn(log.BackTester, "config field 'initial-funds' no longer supported, please use 'initial-quote-funds'")
log.Warnf(log.BackTester, "temporarily setting 'initial-quote-funds' to 'initial-funds' value of %v", c.CurrencySettings[i].InitialLegacyFunds)
iqf := decimal.NewFromFloat(c.CurrencySettings[i].InitialLegacyFunds)
c.CurrencySettings[i].InitialQuoteFunds = &iqf
if c.CurrencySettings[i].Asset == asset.PerpetualSwap ||
c.CurrencySettings[i].Asset == asset.PerpetualContract {
return errPerpetualsUnsupported
}
if c.StrategySettings.UseExchangeLevelFunding {
if c.CurrencySettings[i].InitialQuoteFunds != nil &&
c.CurrencySettings[i].InitialQuoteFunds.GreaterThan(decimal.Zero) {
return fmt.Errorf("non-nil quote %w", errBadInitialFunds)
}
if c.CurrencySettings[i].InitialBaseFunds != nil &&
c.CurrencySettings[i].InitialBaseFunds.GreaterThan(decimal.Zero) {
return fmt.Errorf("non-nil base %w", errBadInitialFunds)
}
} else {
if c.CurrencySettings[i].InitialQuoteFunds == nil &&
c.CurrencySettings[i].InitialBaseFunds == nil {
return fmt.Errorf("nil base and quote %w", errBadInitialFunds)
}
if c.CurrencySettings[i].InitialQuoteFunds != nil &&
c.CurrencySettings[i].InitialBaseFunds != nil &&
c.CurrencySettings[i].InitialBaseFunds.IsZero() &&
c.CurrencySettings[i].InitialQuoteFunds.IsZero() {
return fmt.Errorf("base or quote funds set to zero %w", errBadInitialFunds)
}
if c.CurrencySettings[i].InitialQuoteFunds == nil {
c.CurrencySettings[i].InitialQuoteFunds = &decimal.Zero
}
if c.CurrencySettings[i].InitialBaseFunds == nil {
c.CurrencySettings[i].InitialBaseFunds = &decimal.Zero
if c.CurrencySettings[i].Asset == asset.Futures &&
(c.CurrencySettings[i].Quote.String() == "PERP" || c.CurrencySettings[i].Base.String() == "PI") {
return errPerpetualsUnsupported
}
if c.CurrencySettings[i].Asset.IsFutures() {
hasFutures = true
}
if c.CurrencySettings[i].SpotDetails != nil {
if c.FundingSettings.UseExchangeLevelFunding {
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds.GreaterThan(decimal.Zero) {
return fmt.Errorf("non-nil quote %w", errBadInitialFunds)
}
if c.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds.GreaterThan(decimal.Zero) {
return fmt.Errorf("non-nil base %w", errBadInitialFunds)
}
} else {
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds == nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds == nil {
return fmt.Errorf("nil base and quote %w", errBadInitialFunds)
}
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds.IsZero() &&
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds.IsZero() {
return fmt.Errorf("base or quote funds set to zero %w", errBadInitialFunds)
}
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds == nil {
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds = &decimal.Zero
}
if c.CurrencySettings[i].SpotDetails.InitialBaseFunds == nil {
c.CurrencySettings[i].SpotDetails.InitialBaseFunds = &decimal.Zero
}
}
}
if c.CurrencySettings[i].Base == "" {
if c.CurrencySettings[i].Base.IsEmpty() {
return errUnsetCurrency
}
if c.CurrencySettings[i].Asset == "" {
return errUnsetAsset
if !c.CurrencySettings[i].Asset.IsValid() {
return fmt.Errorf("%v %w", c.CurrencySettings[i].Asset, asset.ErrNotSupported)
}
if c.CurrencySettings[i].ExchangeName == "" {
return errUnsetExchange
}
if !c.CurrencySettings[i].MinimumSlippagePercent.IsZero() ||
!c.CurrencySettings[i].MaximumSlippagePercent.IsZero() {
hasSlippage = true
}
if c.CurrencySettings[i].MinimumSlippagePercent.LessThan(decimal.Zero) ||
c.CurrencySettings[i].MaximumSlippagePercent.LessThan(decimal.Zero) ||
c.CurrencySettings[i].MinimumSlippagePercent.GreaterThan(c.CurrencySettings[i].MaximumSlippagePercent) {
@@ -320,5 +226,112 @@ func (c *Config) validateCurrencySettings() error {
}
c.CurrencySettings[i].ExchangeName = strings.ToLower(c.CurrencySettings[i].ExchangeName)
}
if hasSlippage && hasFutures {
return fmt.Errorf("%w futures sizing currently incompatible with slippage", errFeatureIncompatible)
}
return nil
}
// PrintSetting prints relevant settings to the console for easy reading
func (c *Config) PrintSetting() {
log.Info(common.Config, common.ColourH1+"------------------Backtester Settings------------------------"+common.ColourDefault)
log.Info(common.Config, common.ColourH2+"------------------Strategy Settings--------------------------"+common.ColourDefault)
log.Infof(common.Config, "Strategy: %s", c.StrategySettings.Name)
if len(c.StrategySettings.CustomSettings) > 0 {
log.Info(common.Config, "Custom strategy variables:")
for k, v := range c.StrategySettings.CustomSettings {
log.Infof(common.Config, "%s: %v", k, v)
}
} else {
log.Info(common.Config, "Custom strategy variables: unset")
}
log.Infof(common.Config, "Simultaneous Signal Processing: %v", c.StrategySettings.SimultaneousSignalProcessing)
log.Infof(common.Config, "USD value tracking: %v", !c.StrategySettings.DisableUSDTracking)
if c.FundingSettings.UseExchangeLevelFunding && c.StrategySettings.SimultaneousSignalProcessing {
log.Info(common.Config, common.ColourH2+"------------------Funding Settings---------------------------"+common.ColourDefault)
log.Infof(common.Config, "Use Exchange Level Funding: %v", c.FundingSettings.UseExchangeLevelFunding)
for i := range c.FundingSettings.ExchangeLevelFunding {
log.Infof(common.Config, "Initial funds for %v %v %v: %v",
c.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
c.FundingSettings.ExchangeLevelFunding[i].Asset,
c.FundingSettings.ExchangeLevelFunding[i].Currency,
c.FundingSettings.ExchangeLevelFunding[i].InitialFunds.Round(8))
}
}
for i := range c.CurrencySettings {
currStr := fmt.Sprintf(common.ColourH2+"------------------%v %v-%v Currency Settings---------------------------------------------------------"+common.ColourDefault,
c.CurrencySettings[i].Asset,
c.CurrencySettings[i].Base,
c.CurrencySettings[i].Quote)
log.Infof(common.Config, currStr[:61])
log.Infof(common.Config, "Exchange: %v", c.CurrencySettings[i].ExchangeName)
if !c.FundingSettings.UseExchangeLevelFunding && c.CurrencySettings[i].SpotDetails != nil {
if c.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
log.Infof(common.Config, "Initial base funds: %v %v",
c.CurrencySettings[i].SpotDetails.InitialBaseFunds.Round(8),
c.CurrencySettings[i].Base)
}
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil {
log.Infof(common.Config, "Initial quote funds: %v %v",
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds.Round(8),
c.CurrencySettings[i].Quote)
}
}
if c.CurrencySettings[i].TakerFee != nil {
if c.CurrencySettings[i].UsingExchangeTakerFee {
log.Infof(common.Config, "Taker fee: Using Exchange's API default taker rate: %v", c.CurrencySettings[i].TakerFee.Round(8))
} else {
log.Infof(common.Config, "Taker fee: %v", c.CurrencySettings[i].TakerFee.Round(8))
}
}
if c.CurrencySettings[i].MakerFee != nil {
if c.CurrencySettings[i].UsingExchangeMakerFee {
log.Infof(common.Config, "Maker fee: Using Exchange's API default maker rate: %v", c.CurrencySettings[i].MakerFee.Round(8))
} else {
log.Infof(common.Config, "Maker fee: %v", c.CurrencySettings[i].MakerFee.Round(8))
}
}
log.Infof(common.Config, "Minimum slippage percent: %v", c.CurrencySettings[i].MinimumSlippagePercent.Round(8))
log.Infof(common.Config, "Maximum slippage percent: %v", c.CurrencySettings[i].MaximumSlippagePercent.Round(8))
log.Infof(common.Config, "Buy rules: %+v", c.CurrencySettings[i].BuySide)
log.Infof(common.Config, "Sell rules: %+v", c.CurrencySettings[i].SellSide)
if c.CurrencySettings[i].FuturesDetails != nil && c.CurrencySettings[i].Asset == asset.Futures {
log.Infof(common.Config, "Leverage rules: %+v", c.CurrencySettings[i].FuturesDetails.Leverage)
}
log.Infof(common.Config, "Can use exchange defined order execution limits: %+v", c.CurrencySettings[i].CanUseExchangeLimits)
}
log.Info(common.Config, common.ColourH2+"------------------Portfolio Settings-------------------------"+common.ColourDefault)
log.Infof(common.Config, "Buy rules: %+v", c.PortfolioSettings.BuySide)
log.Infof(common.Config, "Sell rules: %+v", c.PortfolioSettings.SellSide)
log.Infof(common.Config, "Leverage rules: %+v", c.PortfolioSettings.Leverage)
if c.DataSettings.LiveData != nil {
log.Info(common.Config, common.ColourH2+"------------------Live Settings------------------------------"+common.ColourDefault)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "REAL ORDERS: %v", c.DataSettings.LiveData.RealOrders)
log.Infof(common.Config, "Overriding GCT API settings: %v", c.DataSettings.LiveData.APIClientIDOverride != "")
}
if c.DataSettings.APIData != nil {
log.Info(common.Config, common.ColourH2+"------------------API Settings-------------------------------"+common.ColourDefault)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "Start date: %v", c.DataSettings.APIData.StartDate.Format(gctcommon.SimpleTimeFormat))
log.Infof(common.Config, "End date: %v", c.DataSettings.APIData.EndDate.Format(gctcommon.SimpleTimeFormat))
}
if c.DataSettings.CSVData != nil {
log.Info(common.Config, common.ColourH2+"------------------CSV Settings-------------------------------"+common.ColourDefault)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "CSV file: %v", c.DataSettings.CSVData.FullPath)
}
if c.DataSettings.DatabaseData != nil {
log.Info(common.Config, common.ColourH2+"------------------Database Settings--------------------------"+common.ColourDefault)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "Start date: %v", c.DataSettings.DatabaseData.StartDate.Format(gctcommon.SimpleTimeFormat))
log.Infof(common.Config, "End date: %v", c.DataSettings.DatabaseData.EndDate.Format(gctcommon.SimpleTimeFormat))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,10 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
// Errors for config validation
@@ -14,7 +17,6 @@ var (
errNoCurrencySettings = errors.New("no currency settings set in the config")
errBadInitialFunds = errors.New("initial funds set with invalid data, please check your config")
errUnsetExchange = errors.New("exchange name unset for currency settings, please check your config")
errUnsetAsset = errors.New("asset unset for currency settings, please check your config")
errUnsetCurrency = errors.New("currency unset for currency settings, please check your config")
errBadSlippageRates = errors.New("invalid slippage rates in currency settings, please check your config")
errStartEndUnset = errors.New("data start and end dates are invalid, please check your config")
@@ -24,6 +26,8 @@ var (
errSizeLessThanZero = errors.New("size less than zero")
errMaxSizeMinSizeMismatch = errors.New("maximum size must be greater to minimum size")
errMinMaxEqual = errors.New("minimum and maximum limits cannot be equal")
errPerpetualsUnsupported = errors.New("perpetual futures not yet supported")
errFeatureIncompatible = errors.New("feature is not compatible")
)
// Config defines what is in an individual strategy config
@@ -31,6 +35,7 @@ type Config struct {
Nickname string `json:"nickname"`
Goal string `json:"goal"`
StrategySettings StrategySettings `json:"strategy-settings"`
FundingSettings FundingSettings `json:"funding-settings"`
CurrencySettings []CurrencySettings `json:"currency-settings"`
DataSettings DataSettings `json:"data-settings"`
PortfolioSettings PortfolioSettings `json:"portfolio-settings"`
@@ -40,22 +45,27 @@ type Config struct {
// DataSettings is a container for each type of data retrieval setting.
// Only ONE can be populated per config
type DataSettings struct {
Interval time.Duration `json:"interval"`
DataType string `json:"data-type"`
APIData *APIData `json:"api-data,omitempty"`
DatabaseData *DatabaseData `json:"database-data,omitempty"`
LiveData *LiveData `json:"live-data,omitempty"`
CSVData *CSVData `json:"csv-data,omitempty"`
Interval kline.Interval `json:"interval"`
DataType string `json:"data-type"`
APIData *APIData `json:"api-data,omitempty"`
DatabaseData *DatabaseData `json:"database-data,omitempty"`
LiveData *LiveData `json:"live-data,omitempty"`
CSVData *CSVData `json:"csv-data,omitempty"`
}
// FundingSettings contains funding details for individual currencies
type FundingSettings struct {
UseExchangeLevelFunding bool `json:"use-exchange-level-funding"`
ExchangeLevelFunding []ExchangeLevelFunding `json:"exchange-level-funding,omitempty"`
}
// StrategySettings contains what strategy to load, along with custom settings map
// (variables defined per strategy)
// along with defining whether the strategy will assess all currencies at once, or individually
type StrategySettings struct {
Name string `json:"name"`
SimultaneousSignalProcessing bool `json:"use-simultaneous-signal-processing"`
UseExchangeLevelFunding bool `json:"use-exchange-level-funding"`
ExchangeLevelFunding []ExchangeLevelFunding `json:"exchange-level-funding,omitempty"`
Name string `json:"name"`
SimultaneousSignalProcessing bool `json:"use-simultaneous-signal-processing"`
// If true, won't track USD values against currency pair
// bool language is opposite to encourage use by default
DisableUSDTracking bool `json:"disable-usd-tracking"`
@@ -71,8 +81,8 @@ type StrategySettings struct {
// will have dibs
type ExchangeLevelFunding struct {
ExchangeName string `json:"exchange-name"`
Asset string `json:"asset"`
Currency string `json:"currency"`
Asset asset.Item `json:"asset"`
Currency currency.Code `json:"currency"`
InitialFunds decimal.Decimal `json:"initial-funds"`
TransferFee decimal.Decimal `json:"transfer-fee"`
}
@@ -97,7 +107,12 @@ type PortfolioSettings struct {
type Leverage struct {
CanUseLeverage bool `json:"can-use-leverage"`
MaximumOrdersWithLeverageRatio decimal.Decimal `json:"maximum-orders-with-leverage-ratio"`
MaximumLeverageRate decimal.Decimal `json:"maximum-leverage-rate"`
// MaximumOrderLeverageRate allows for orders to be placed with higher leverage rate. eg have $100 in collateral,
// but place an order for $200 using 2x leverage
MaximumOrderLeverageRate decimal.Decimal `json:"maximum-leverage-rate"`
// MaximumCollateralLeverageRate allows for orders to be placed at `1x leverage, but utilise collateral as leverage to place more.
// eg if this is 2x, and collateral is $100 I can place two long/shorts of $100
MaximumCollateralLeverageRate decimal.Decimal `json:"maximum-collateral-leverage-rate"`
}
// MinMax are the rules which limit the placement of orders.
@@ -112,32 +127,45 @@ type MinMax struct {
// you wish to trade with
// Backtester will load the data of the currencies specified here
type CurrencySettings struct {
ExchangeName string `json:"exchange-name"`
Asset string `json:"asset"`
Base string `json:"base"`
Quote string `json:"quote"`
ExchangeName string `json:"exchange-name"`
Asset asset.Item `json:"asset"`
Base currency.Code `json:"base"`
Quote currency.Code `json:"quote"`
// USDTrackingPair is used for price tracking data only
USDTrackingPair bool `json:"-"`
InitialBaseFunds *decimal.Decimal `json:"initial-base-funds,omitempty"`
InitialQuoteFunds *decimal.Decimal `json:"initial-quote-funds,omitempty"`
InitialLegacyFunds float64 `json:"initial-funds,omitempty"`
SpotDetails *SpotDetails `json:"spot-details,omitempty"`
FuturesDetails *FuturesDetails `json:"futures-details,omitempty"`
Leverage Leverage `json:"leverage"`
BuySide MinMax `json:"buy-side"`
SellSide MinMax `json:"sell-side"`
BuySide MinMax `json:"buy-side"`
SellSide MinMax `json:"sell-side"`
MinimumSlippagePercent decimal.Decimal `json:"min-slippage-percent"`
MaximumSlippagePercent decimal.Decimal `json:"max-slippage-percent"`
MakerFee decimal.Decimal `json:"maker-fee-override"`
TakerFee decimal.Decimal `json:"taker-fee-override"`
UsingExchangeMakerFee bool `json:"-"`
MakerFee *decimal.Decimal `json:"maker-fee-override,omitempty"`
UsingExchangeTakerFee bool `json:"-"`
TakerFee *decimal.Decimal `json:"taker-fee-override,omitempty"`
MaximumHoldingsRatio decimal.Decimal `json:"maximum-holdings-ratio"`
MaximumHoldingsRatio decimal.Decimal `json:"maximum-holdings-ratio"`
SkipCandleVolumeFitting bool `json:"skip-candle-volume-fitting"`
CanUseExchangeLimits bool `json:"use-exchange-order-limits"`
SkipCandleVolumeFitting bool `json:"skip-candle-volume-fitting"`
ShowExchangeOrderLimitWarning bool `json:"-"`
UseExchangePNLCalculation bool `json:"use-exchange-pnl-calculation"`
}
// SpotDetails contains funding information that cannot be shared with another
// pair during the backtesting run. Use exchange level funding to share funds
type SpotDetails struct {
InitialBaseFunds *decimal.Decimal `json:"initial-base-funds,omitempty"`
InitialQuoteFunds *decimal.Decimal `json:"initial-quote-funds,omitempty"`
}
// FuturesDetails contains data relevant to futures currency pairs
type FuturesDetails struct {
Leverage Leverage `json:"leverage"`
}
// APIData defines all fields to configure API based data

View File

@@ -19,6 +19,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
dbPSQL "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres"
dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3"
@@ -42,78 +43,59 @@ func main() {
fmt.Print(common.ASCIILogo)
fmt.Println("Welcome to the config generator!")
reader := bufio.NewReader(os.Stdin)
cfg := config.Config{
StrategySettings: config.StrategySettings{
Name: "",
SimultaneousSignalProcessing: false,
UseExchangeLevelFunding: false,
ExchangeLevelFunding: nil,
CustomSettings: nil,
},
CurrencySettings: []config.CurrencySettings{},
DataSettings: config.DataSettings{
Interval: 0,
DataType: "",
APIData: nil,
DatabaseData: nil,
LiveData: nil,
CSVData: nil,
},
PortfolioSettings: config.PortfolioSettings{
Leverage: config.Leverage{},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
},
StatisticSettings: config.StatisticSettings{},
}
fmt.Println("-----Strategy Settings-----")
var cfg config.Config
var err error
firstRun := true
for err != nil || firstRun {
firstRun = false
fmt.Println("-----Strategy Settings-----")
// loop in sections, so that if there is an error,
// a user only needs to redo that section
for {
err = parseStrategySettings(&cfg, reader)
if err != nil {
log.Println(err)
} else {
break
}
}
fmt.Println("-----Exchange Settings-----")
firstRun = true
for err != nil || firstRun {
firstRun = false
for {
err = parseExchangeSettings(reader, &cfg)
if err != nil {
log.Println(err)
} else {
break
}
}
fmt.Println("-----Portfolio Settings-----")
firstRun = true
for err != nil || firstRun {
firstRun = false
for {
err = parsePortfolioSettings(reader, &cfg)
if err != nil {
log.Println(err)
} else {
break
}
}
fmt.Println("-----Data Settings-----")
firstRun = true
for err != nil || firstRun {
firstRun = false
for {
err = parseDataSettings(&cfg, reader)
if err != nil {
log.Println(err)
} else {
break
}
}
fmt.Println("-----Statistics Settings-----")
firstRun = true
for err != nil || firstRun {
firstRun = false
for {
err = parseStatisticsSettings(&cfg, reader)
if err != nil {
log.Println(err)
} else {
break
}
}
@@ -125,26 +107,46 @@ func main() {
fmt.Println("Write strategy config to file? If no, the output will be on screen y/n")
yn := quickParse(reader)
if yn == y || yn == yes {
var wd string
wd, err = os.Getwd()
if err != nil {
log.Fatal(err)
}
fn := cfg.StrategySettings.Name
if cfg.Nickname != "" {
fn += "-" + cfg.Nickname
}
fn += ".strat" // nolint:misspell // its shorthand for strategy
wd = filepath.Join(wd, fn)
fmt.Printf("Enter output file. If blank, will output to \"%v\"\n", wd)
path := quickParse(reader)
if path == "" {
path = wd
}
err = os.WriteFile(path, resp, file.DefaultPermissionOctal)
if err != nil {
log.Fatal(err)
var fp, wd string
extension := "strat" // nolint:misspell // its shorthand for strategy
for {
wd, err = os.Getwd()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Enter output directory. If blank, will default to \"%v\"\n", wd)
parsedPath := quickParse(reader)
if parsedPath != "" {
wd = parsedPath
}
fn := cfg.StrategySettings.Name
if cfg.Nickname != "" {
fn += "-" + cfg.Nickname
}
fn, err = common.GenerateFileName(fn, extension)
if err != nil {
log.Printf("could not write file, please try again. err: %v", err)
continue
}
fmt.Printf("Enter output file. If blank, will default to \"%v\"\n", fn)
parsedFileName := quickParse(reader)
if parsedFileName != "" {
fn, err = common.GenerateFileName(parsedFileName, extension)
if err != nil {
log.Printf("could not write file, please try again. err: %v", err)
continue
}
}
fp = filepath.Join(wd, fn)
err = os.WriteFile(fp, resp, file.DefaultPermissionOctal)
if err != nil {
log.Printf("could not write file, please try again. err: %v", err)
continue
}
break
}
fmt.Printf("Successfully output strategy to \"%v\"\n", fp)
} else {
log.Print(string(resp))
}
@@ -219,7 +221,7 @@ func parseExchangeSettings(reader *bufio.Reader, cfg *config.Config) error {
addCurrency := y
for strings.Contains(addCurrency, y) {
var currencySetting *config.CurrencySettings
currencySetting, err = addCurrencySetting(reader, cfg.StrategySettings.UseExchangeLevelFunding)
currencySetting, err = addCurrencySetting(reader, cfg.FundingSettings.UseExchangeLevelFunding)
if err != nil {
return err
}
@@ -266,8 +268,8 @@ func parseStrategySettings(cfg *config.Config, reader *bufio.Reader) error {
}
fmt.Println("Will this strategy be able to share funds at an exchange level? y/n")
yn = quickParse(reader)
cfg.StrategySettings.UseExchangeLevelFunding = strings.Contains(yn, y)
if !cfg.StrategySettings.UseExchangeLevelFunding {
cfg.FundingSettings.UseExchangeLevelFunding = strings.Contains(yn, y)
if !cfg.FundingSettings.UseExchangeLevelFunding {
return nil
}
@@ -288,21 +290,21 @@ func parseStrategySettings(cfg *config.Config, reader *bufio.Reader) error {
if intNum > len(supported) || intNum <= 0 {
return errors.New("unknown option")
}
fund.Asset = supported[intNum-1].String()
fund.Asset = supported[intNum-1]
} else {
for i := range supported {
if strings.EqualFold(response, supported[i].String()) {
fund.Asset = supported[i].String()
fund.Asset = supported[i]
break
}
}
if fund.Asset == "" {
if fund.Asset == asset.Empty {
return errors.New("unrecognised data option")
}
}
fmt.Println("What is the individual currency to add funding to? eg BTC")
fund.Currency = quickParse(reader)
fund.Currency = currency.NewCode(quickParse(reader))
fmt.Printf("How much funding for %v?\n", fund.Currency)
fund.InitialFunds, err = decimal.NewFromString(quickParse(reader))
if err != nil {
@@ -317,7 +319,7 @@ func parseStrategySettings(cfg *config.Config, reader *bufio.Reader) error {
return err
}
}
cfg.StrategySettings.ExchangeLevelFunding = append(cfg.StrategySettings.ExchangeLevelFunding, fund)
cfg.FundingSettings.ExchangeLevelFunding = append(cfg.FundingSettings.ExchangeLevelFunding, fund)
fmt.Println("Add another source of funds? y/n")
addFunding = quickParse(reader)
}
@@ -334,7 +336,7 @@ func parseAPI(reader *bufio.Reader, cfg *config.Config) error {
fmt.Printf("What is the start date? Leave blank for \"%v\"\n", defaultStart.Format(gctcommon.SimpleTimeFormat))
startDate = quickParse(reader)
if startDate != "" {
cfg.DataSettings.APIData.StartDate, err = time.Parse(startDate, gctcommon.SimpleTimeFormat)
cfg.DataSettings.APIData.StartDate, err = time.Parse(gctcommon.SimpleTimeFormat, startDate)
if err != nil {
return err
}
@@ -342,10 +344,10 @@ func parseAPI(reader *bufio.Reader, cfg *config.Config) error {
cfg.DataSettings.APIData.StartDate = defaultStart
}
fmt.Printf("What is the end date? Leave blank for \"%v\"\n", defaultStart.Format(gctcommon.SimpleTimeFormat))
fmt.Printf("What is the end date? Leave blank for \"%v\"\n", defaultEnd.Format(gctcommon.SimpleTimeFormat))
endDate = quickParse(reader)
if endDate != "" {
cfg.DataSettings.APIData.EndDate, err = time.Parse(endDate, gctcommon.SimpleTimeFormat)
cfg.DataSettings.APIData.EndDate, err = time.Parse(gctcommon.SimpleTimeFormat, endDate)
if err != nil {
return err
}
@@ -374,7 +376,7 @@ func parseDatabase(reader *bufio.Reader, cfg *config.Config) error {
fmt.Printf("What is the start date? Leave blank for \"%v\"\n", defaultStart.Format(gctcommon.SimpleTimeFormat))
startDate := quickParse(reader)
if startDate != "" {
cfg.DataSettings.DatabaseData.StartDate, err = time.Parse(startDate, gctcommon.SimpleTimeFormat)
cfg.DataSettings.DatabaseData.StartDate, err = time.Parse(gctcommon.SimpleTimeFormat, startDate)
if err != nil {
return err
}
@@ -382,9 +384,9 @@ func parseDatabase(reader *bufio.Reader, cfg *config.Config) error {
cfg.DataSettings.DatabaseData.StartDate = defaultStart
}
fmt.Printf("What is the end date? Leave blank for \"%v\"\n", defaultStart.Format(gctcommon.SimpleTimeFormat))
fmt.Printf("What is the end date? Leave blank for \"%v\"\n", defaultEnd.Format(gctcommon.SimpleTimeFormat))
if endDate := quickParse(reader); endDate != "" {
cfg.DataSettings.DatabaseData.EndDate, err = time.Parse(endDate, gctcommon.SimpleTimeFormat)
cfg.DataSettings.DatabaseData.EndDate, err = time.Parse(gctcommon.SimpleTimeFormat, endDate)
if err != nil {
return err
}
@@ -500,7 +502,7 @@ func parseDataChoice(reader *bufio.Reader, multiCurrency bool) (string, error) {
return "", errors.New("unrecognised data option")
}
func parseKlineInterval(reader *bufio.Reader) (time.Duration, error) {
func parseKlineInterval(reader *bufio.Reader) (gctkline.Interval, error) {
allCandles := gctkline.SupportedIntervals
for i := range allCandles {
fmt.Printf("%v. %s\n", i+1, allCandles[i].Word())
@@ -512,11 +514,11 @@ func parseKlineInterval(reader *bufio.Reader) (time.Duration, error) {
if intNum > len(allCandles) || intNum <= 0 {
return 0, errors.New("unknown option")
}
return allCandles[intNum-1].Duration(), nil
return allCandles[intNum-1], nil
}
for i := range allCandles {
if strings.EqualFold(response, allCandles[i].Word()) {
return allCandles[i].Duration(), nil
return allCandles[i], nil
}
}
return 0, errors.New("unrecognised interval")
@@ -573,64 +575,81 @@ func addCurrencySetting(reader *bufio.Reader, usingExchangeLevelFunding bool) (*
if intNum > len(supported) || intNum <= 0 {
return nil, errors.New("unknown option")
}
setting.Asset = supported[intNum-1].String()
setting.Asset = supported[intNum-1]
}
for i := range supported {
if strings.EqualFold(response, supported[i].String()) {
setting.Asset = supported[i].String()
setting.Asset = supported[i]
}
}
var f float64
fmt.Println("Enter the currency base. eg BTC")
setting.Base = quickParse(reader)
if !usingExchangeLevelFunding {
fmt.Println("Enter the initial base funds. eg 0")
parseNum := quickParse(reader)
if parseNum != "" {
f, err = strconv.ParseFloat(parseNum, 64)
if err != nil {
return nil, err
setting.Base = currency.NewCode(quickParse(reader))
if setting.Asset == asset.Spot {
if !usingExchangeLevelFunding {
fmt.Println("Enter the initial base funds. eg 0")
parseNum := quickParse(reader)
if parseNum != "" {
var d decimal.Decimal
d, err = decimal.NewFromString(parseNum)
if err != nil {
return nil, err
}
setting.SpotDetails = &config.SpotDetails{
InitialBaseFunds: &d,
}
}
iqf := decimal.NewFromFloat(f)
setting.InitialBaseFunds = &iqf
}
}
fmt.Println("Enter the currency quote. eg USDT")
setting.Quote = quickParse(reader)
if !usingExchangeLevelFunding {
setting.Quote = currency.NewCode(quickParse(reader))
if setting.Asset == asset.Spot && !usingExchangeLevelFunding {
fmt.Println("Enter the initial quote funds. eg 10000")
parseNum := quickParse(reader)
if parseNum != "" {
f, err = strconv.ParseFloat(parseNum, 64)
var d decimal.Decimal
d, err = decimal.NewFromString(parseNum)
if err != nil {
return nil, err
}
iqf := decimal.NewFromFloat(f)
setting.InitialQuoteFunds = &iqf
if setting.SpotDetails == nil {
setting.SpotDetails = &config.SpotDetails{
InitialQuoteFunds: &d,
}
} else {
setting.SpotDetails.InitialQuoteFunds = &d
}
}
}
fmt.Println("Enter the maker-fee. eg 0.001")
parseNum := quickParse(reader)
if parseNum != "" {
f, err = strconv.ParseFloat(parseNum, 64)
if err != nil {
return nil, err
fmt.Println("Do you want to set custom fees? If no, Backtester will use default fees for exchange y/n")
yn := quickParse(reader)
if yn == y || yn == yes {
fmt.Println("Enter the maker-fee. eg 0.001")
parseNum := quickParse(reader)
if parseNum != "" {
var d decimal.Decimal
d, err = decimal.NewFromString(parseNum)
if err != nil {
return nil, err
}
setting.MakerFee = &d
}
setting.MakerFee = decimal.NewFromFloat(f)
}
fmt.Println("Enter the taker-fee. eg 0.01")
parseNum = quickParse(reader)
if parseNum != "" {
f, err = strconv.ParseFloat(parseNum, 64)
if err != nil {
return nil, err
fmt.Println("Enter the taker-fee. eg 0.01")
parseNum = quickParse(reader)
if parseNum != "" {
var d decimal.Decimal
d, err = decimal.NewFromString(parseNum)
if err != nil {
return nil, err
}
setting.TakerFee = &d
}
setting.TakerFee = decimal.NewFromFloat(f)
}
fmt.Println("Will there be buy-side limits? y/n")
yn := quickParse(reader)
yn = quickParse(reader)
if yn == y || yn == yes {
setting.BuySide, err = minMaxParse("buy", reader)
if err != nil {
@@ -665,18 +684,16 @@ func addCurrencySetting(reader *bufio.Reader, usingExchangeLevelFunding bool) (*
fmt.Println("If the upper bound is 100, then the price can be unaffected. A minimum of 80 and a maximum of 100 means that the price will randomly be set between those bounds as a way of emulating slippage")
fmt.Println("What is the lower bounds of slippage? eg 80")
f, err = strconv.ParseFloat(quickParse(reader), 64)
setting.MinimumSlippagePercent, err = decimal.NewFromString(quickParse(reader))
if err != nil {
return nil, err
}
setting.MinimumSlippagePercent = decimal.NewFromFloat(f)
fmt.Println("What is the upper bounds of slippage? eg 100")
f, err = strconv.ParseFloat(quickParse(reader), 64)
setting.MaximumSlippagePercent, err = decimal.NewFromString(quickParse(reader))
if err != nil {
return nil, err
}
setting.MaximumSlippagePercent = decimal.NewFromFloat(f)
}
return &setting, nil

View File

@@ -34,6 +34,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| dca-database-candles.strat | The same DCA strategy, but uses a database to retrieve candle data |
| rsi-api-candles.strat | Runs a strategy using rsi figures to make buy or sell orders based on market figures |
| t2b2-api-candles-exchange-funding.strat | Runs a more complex strategy using simultaneous signal processing, exchange level funding and MFI values to make buy or sell signals based on the two strongest and weakest MFI values |
| ftx-cash-carry.strat | Executes a cash and carry trade on FTX, buying BTC-USD while shorting the long dated futures contract BTC-20210924 |
### Want to make your own configs?
Use the provided config builder under `/backtester/config/configbuilder` or modify tests under `/backtester/config/config_test.go` to generates strategy files quickly

View File

@@ -4,29 +4,26 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": true,
"disable-usd-tracking": true
},
"funding-settings": {
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"currency": "USDT",
"initial-funds": "100000",
"transfer-fee": "0"
}
],
"disable-usd-tracking": true
]
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -39,22 +36,18 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "ETH",
"quote": "USDT",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -67,19 +60,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"api-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-12-01T00:00:00+11:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",
"inclusive-end-date": false
}
},
@@ -87,7 +81,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": false
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -31,22 +30,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "ETH",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -60,19 +57,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"api-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-12-01T00:00:00+11:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",
"inclusive-end-date": false
}
},
@@ -80,7 +78,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": true,
"use-exchange-level-funding": false,
"disable-usd-tracking": false
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "1000000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "1000000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -31,22 +30,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "ETH",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -60,19 +57,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"api-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-12-01T00:00:00+11:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",
"inclusive-end-date": false
}
},
@@ -80,7 +78,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": false
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -31,19 +30,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"api-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-12-01T00:00:00+11:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",
"inclusive-end-date": false
}
},
@@ -51,7 +51,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": false
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -31,19 +30,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": true,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": true
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 3600000000000,
"data-type": "trade",
"api-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-08-04T00:00:00+10:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-08-04T00:00:00+10:00",
"inclusive-end-date": false
}
},
@@ -51,7 +51,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.1",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": true
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -31,11 +30,12 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
@@ -54,7 +54,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": true
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -31,11 +30,12 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
@@ -49,7 +49,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": true
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0",
@@ -31,11 +30,12 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
@@ -49,7 +49,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0",

View File

@@ -4,20 +4,19 @@
"strategy-settings": {
"name": "dollarcostaverage",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": false
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -31,19 +30,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"database-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-12-01T00:00:00+11:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",
"config": {
"enabled": true,
"verbose": false,
@@ -65,7 +65,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -0,0 +1,101 @@
{
"nickname": "Example Cash and Carry",
"goal": "To demonstrate a cash and carry strategy",
"strategy-settings": {
"name": "ftx-cash-carry",
"use-simultaneous-signal-processing": true,
"disable-usd-tracking": false
},
"funding-settings": {
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "ftx",
"asset": "spot",
"currency": "USD",
"initial-funds": "100000",
"transfer-fee": "0"
}
]
},
"currency-settings": [
{
"exchange-name": "ftx",
"asset": "futures",
"base": "BTC",
"quote": "20210924",
"buy-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
},
"sell-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USD",
"buy-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
},
"sell-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"api-data": {
"start-date": "2021-01-14T00:00:00Z",
"end-date": "2021-09-24T00:00:00Z",
"inclusive-end-date": false
}
},
"portfolio-settings": {
"leverage": {
"can-use-leverage": true,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
},
"sell-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
}
},
"statistic-settings": {
"risk-free-rate": "0.03"
}
}

View File

@@ -4,7 +4,6 @@
"strategy-settings": {
"name": "rsi",
"use-simultaneous-signal-processing": false,
"use-exchange-level-funding": false,
"disable-usd-tracking": false,
"custom-settings": {
"rsi-high": 70,
@@ -12,17 +11,17 @@
"rsi-period": 14
}
},
"funding-settings": {
"use-exchange-level-funding": false
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"initial-quote-funds": "100000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -36,23 +35,21 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "ETH",
"quote": "USDT",
"initial-base-funds": "10",
"initial-quote-funds": "1000000",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"spot-details": {
"initial-base-funds": "10",
"initial-quote-funds": "1000000"
},
"buy-side": {
"minimum-size": "0.005",
@@ -66,19 +63,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"api-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-12-01T00:00:00+11:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",
"inclusive-end-date": false
}
},
@@ -86,7 +84,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -4,23 +4,6 @@
"strategy-settings": {
"name": "top2bottom2",
"use-simultaneous-signal-processing": true,
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "binance",
"asset": "spot",
"currency": "BTC",
"initial-funds": "3",
"transfer-fee": "0"
},
{
"exchange-name": "binance",
"asset": "spot",
"currency": "USDT",
"initial-funds": "10000",
"transfer-fee": "0"
}
],
"disable-usd-tracking": false,
"custom-settings": {
"mfi-high": 68,
@@ -28,17 +11,31 @@
"mfi-period": 14
}
},
"funding-settings": {
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "ftx",
"asset": "spot",
"currency": "BTC",
"initial-funds": "3",
"transfer-fee": "0"
},
{
"exchange-name": "ftx",
"asset": "spot",
"currency": "USDT",
"initial-funds": "10000",
"transfer-fee": "0"
}
]
},
"currency-settings": [
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -51,22 +48,18 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "DOGE",
"quote": "USDT",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -79,22 +72,18 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "ETH",
"quote": "BTC",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -107,22 +96,18 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "LTC",
"quote": "BTC",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -135,22 +120,18 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "XRP",
"quote": "USDT",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -163,22 +144,18 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"exchange-name": "ftx",
"asset": "spot",
"base": "BNB",
"quote": "BTC",
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
@@ -191,19 +168,20 @@
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.001",
"taker-fee-override": "0.002",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": false,
"use-exchange-order-limits": false,
"skip-candle-volume-fitting": false
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"api-data": {
"start-date": "2020-08-01T00:00:00+10:00",
"end-date": "2020-12-01T00:00:00+11:00",
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",
"inclusive-end-date": false
}
},
@@ -211,7 +189,8 @@
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0"
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",

View File

@@ -1,6 +1,7 @@
package data
import (
"fmt"
"sort"
"strings"
@@ -37,8 +38,15 @@ func (h *HandlerPerCurrency) GetAllData() map[string]map[asset.Item]map[currency
}
// GetDataForCurrency returns the Handler for a specific exchange, asset, currency
func (h *HandlerPerCurrency) GetDataForCurrency(e string, a asset.Item, p currency.Pair) Handler {
return h.data[e][a][p]
func (h *HandlerPerCurrency) GetDataForCurrency(ev common.EventHandler) (Handler, error) {
if ev == nil {
return nil, common.ErrNilEvent
}
handler, ok := h.data[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
if !ok {
return nil, fmt.Errorf("%s %s %s %w", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ErrHandlerNotFound)
}
return handler, nil
}
// Reset returns the struct to defaults
@@ -98,6 +106,9 @@ func (b *Base) History() []common.DataEventHandler {
// Latest will return latest data event
func (b *Base) Latest() common.DataEventHandler {
if b.latest == nil && len(b.stream) >= b.offset+1 {
b.latest = b.stream[b.offset]
}
return b.latest
}
@@ -107,6 +118,11 @@ func (b *Base) List() []common.DataEventHandler {
return b.stream[b.offset:]
}
// IsLastEvent determines whether the latest event is the last event
func (b *Base) IsLastEvent() bool {
return b.latest != nil && b.latest.GetOffset() == int64(len(b.stream))
}
// SortStream sorts the stream by timestamp
func (b *Base) SortStream() {
sort.Slice(b.stream, func(i, j int) bool {

View File

@@ -1,10 +1,14 @@
package data
import (
"errors"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -16,32 +20,58 @@ type fakeDataHandler struct {
time int
}
func TestLatest(t *testing.T) {
t.Parallel()
var d Base
d.AppendStream(&fakeDataHandler{time: 1})
if latest := d.Latest(); latest != d.stream[d.offset] {
t.Error("expected latest to match offset")
}
}
func TestBaseDataFunctions(t *testing.T) {
t.Parallel()
var d Base
if latest := d.Latest(); latest != nil {
t.Error("expected nil")
}
d.Next()
o := d.Offset()
if o != 0 {
t.Error("expected 0")
}
d.AppendStream(nil)
if d.IsLastEvent() {
t.Error("no")
}
d.AppendStream(nil)
d.AppendStream(nil)
d.Next()
o = d.Offset()
if o != 0 {
if len(d.stream) != 0 {
t.Error("expected 0")
}
if list := d.List(); list != nil {
t.Error("expected nil")
d.AppendStream(&fakeDataHandler{time: 1})
d.AppendStream(&fakeDataHandler{time: 2})
d.AppendStream(&fakeDataHandler{time: 3})
d.AppendStream(&fakeDataHandler{time: 4})
d.Next()
d.Next()
if list := d.List(); len(list) != 2 {
t.Errorf("expected 2 received %v", len(list))
}
if history := d.History(); history != nil {
t.Error("expected nil")
d.Next()
d.Next()
if !d.IsLastEvent() {
t.Error("expected last event")
}
o = d.Offset()
if o != 4 {
t.Error("expected 4")
}
if list := d.List(); len(list) != 0 {
t.Error("expected 0")
}
if history := d.History(); len(history) != 4 {
t.Errorf("expected 4 received %v", len(history))
}
d.SetStream(nil)
if st := d.GetStream(); st != nil {
t.Error("expected nil")
@@ -60,55 +90,6 @@ func TestSetup(t *testing.T) {
}
}
func TestStream(t *testing.T) {
var d Base
var f fakeDataHandler
// shut up coverage report
f.GetOffset()
f.SetOffset(1)
f.IsEvent()
f.Pair()
f.GetExchange()
f.GetInterval()
f.GetAssetType()
f.GetReason()
f.AppendReason("fake")
f.GetClosePrice()
f.GetHighPrice()
f.GetLowPrice()
f.GetOpenPrice()
d.AppendStream(fakeDataHandler{time: 1})
d.AppendStream(fakeDataHandler{time: 4})
d.AppendStream(fakeDataHandler{time: 10})
d.AppendStream(fakeDataHandler{time: 2})
d.AppendStream(fakeDataHandler{time: 20})
d.SortStream()
f, ok := d.Next().(fakeDataHandler)
if f.time != 1 || !ok {
t.Error("expected 1")
}
f, ok = d.Next().(fakeDataHandler)
if f.time != 2 || !ok {
t.Error("expected 2")
}
f, ok = d.Next().(fakeDataHandler)
if f.time != 4 || !ok {
t.Error("expected 4")
}
f, ok = d.Next().(fakeDataHandler)
if f.time != 10 || !ok {
t.Error("expected 10")
}
f, ok = d.Next().(fakeDataHandler)
if f.time != 20 || !ok {
t.Error("expected 20")
}
}
func TestSetDataForCurrency(t *testing.T) {
t.Parallel()
d := HandlerPerCurrency{}
@@ -144,15 +125,45 @@ func TestGetAllData(t *testing.T) {
func TestGetDataForCurrency(t *testing.T) {
t.Parallel()
d := HandlerPerCurrency{}
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d.SetDataForCurrency(exch, a, p, nil)
d.SetDataForCurrency(exch, a, currency.NewPair(currency.BTC, currency.DOGE), nil)
result := d.GetDataForCurrency(exch, a, p)
d.SetDataForCurrency(testExchange, a, p, nil)
d.SetDataForCurrency(testExchange, a, currency.NewPair(currency.BTC, currency.DOGE), nil)
ev := &order.Order{Base: &event.Base{
Exchange: testExchange,
AssetType: a,
CurrencyPair: p,
}}
result, err := d.GetDataForCurrency(ev)
if err != nil {
t.Error(err)
}
if result != nil {
t.Error("expected nil")
}
_, err = d.GetDataForCurrency(nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received '%v' expected '%v'", err, common.ErrNilEvent)
}
_, err = d.GetDataForCurrency(&order.Order{Base: &event.Base{
Exchange: "lol",
AssetType: asset.USDTMarginedFutures,
CurrencyPair: currency.NewPair(currency.EMB, currency.DOGE),
}})
if !errors.Is(err, ErrHandlerNotFound) {
t.Errorf("received '%v' expected '%v'", err, ErrHandlerNotFound)
}
_, err = d.GetDataForCurrency(&order.Order{Base: &event.Base{
Exchange: testExchange,
AssetType: asset.USDTMarginedFutures,
CurrencyPair: currency.NewPair(currency.EMB, currency.DOGE),
}})
if !errors.Is(err, ErrHandlerNotFound) {
t.Errorf("received '%v' expected '%v'", err, ErrHandlerNotFound)
}
}
func TestReset(t *testing.T) {
@@ -170,56 +181,74 @@ func TestReset(t *testing.T) {
}
// methods that satisfy the common.DataEventHandler interface
func (t fakeDataHandler) GetOffset() int64 {
return 0
func (f fakeDataHandler) GetOffset() int64 {
return 4
}
func (t fakeDataHandler) SetOffset(int64) {
func (f fakeDataHandler) SetOffset(int64) {
}
func (t fakeDataHandler) IsEvent() bool {
func (f fakeDataHandler) IsEvent() bool {
return false
}
func (t fakeDataHandler) GetTime() time.Time {
return time.Now().Add(time.Hour * time.Duration(t.time))
func (f fakeDataHandler) GetTime() time.Time {
return time.Now().Add(time.Hour * time.Duration(f.time))
}
func (t fakeDataHandler) Pair() currency.Pair {
func (f fakeDataHandler) Pair() currency.Pair {
return currency.NewPair(currency.BTC, currency.USD)
}
func (t fakeDataHandler) GetExchange() string {
func (f fakeDataHandler) GetExchange() string {
return "fake"
}
func (t fakeDataHandler) GetInterval() kline.Interval {
func (f fakeDataHandler) GetInterval() kline.Interval {
return kline.Interval(time.Minute)
}
func (t fakeDataHandler) GetAssetType() asset.Item {
func (f fakeDataHandler) GetAssetType() asset.Item {
return asset.Spot
}
func (t fakeDataHandler) GetReason() string {
func (f fakeDataHandler) GetReason() string {
return "fake"
}
func (t fakeDataHandler) AppendReason(string) {
func (f fakeDataHandler) AppendReason(string) {
}
func (t fakeDataHandler) GetClosePrice() decimal.Decimal {
func (f fakeDataHandler) GetClosePrice() decimal.Decimal {
return decimal.Zero
}
func (t fakeDataHandler) GetHighPrice() decimal.Decimal {
func (f fakeDataHandler) GetHighPrice() decimal.Decimal {
return decimal.Zero
}
func (t fakeDataHandler) GetLowPrice() decimal.Decimal {
func (f fakeDataHandler) GetLowPrice() decimal.Decimal {
return decimal.Zero
}
func (t fakeDataHandler) GetOpenPrice() decimal.Decimal {
func (f fakeDataHandler) GetOpenPrice() decimal.Decimal {
return decimal.Zero
}
func (f fakeDataHandler) GetUnderlyingPair() currency.Pair {
return f.Pair()
}
func (f fakeDataHandler) AppendReasonf(s string, i ...interface{}) {}
func (f fakeDataHandler) GetBase() *event.Base {
return &event.Base{}
}
func (f fakeDataHandler) GetConcatReasons() string {
return ""
}
func (f fakeDataHandler) GetReasons() []string {
return nil
}

View File

@@ -1,6 +1,7 @@
package data
import (
"errors"
"time"
"github.com/shopspring/decimal"
@@ -9,6 +10,9 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// ErrHandlerNotFound returned when a handler is not found for specified exchange, asset, pair
var ErrHandlerNotFound = errors.New("handler not found")
// HandlerPerCurrency stores an event handler per exchange asset pair
type HandlerPerCurrency struct {
data map[string]map[asset.Item]map[currency.Pair]Handler
@@ -19,7 +23,7 @@ type Holder interface {
Setup()
SetDataForCurrency(string, asset.Item, currency.Pair, Handler)
GetAllData() map[string]map[asset.Item]map[currency.Pair]Handler
GetDataForCurrency(string, asset.Item, currency.Pair) Handler
GetDataForCurrency(ev common.EventHandler) (Handler, error)
Reset()
}
@@ -50,6 +54,7 @@ type Streamer interface {
History() []common.DataEventHandler
Latest() common.DataEventHandler
List() []common.DataEventHandler
IsLastEvent() bool
Offset() int
StreamOpen() []decimal.Decimal

View File

@@ -33,7 +33,7 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat
defer func() {
err = csvFile.Close()
if err != nil {
log.Errorln(log.BackTester, err)
log.Errorln(common.Data, err)
}
}()

View File

@@ -38,7 +38,7 @@ func LoadData(startDate, endDate time.Time, interval time.Duration, exchangeName
resp.Item = klineItem
for i := range klineItem.Candles {
if klineItem.Candles[i].ValidationIssues != "" {
log.Warnf(log.BackTester, "candle validation issue for %v %v %v: %v", klineItem.Exchange, klineItem.Asset, klineItem.Pair, klineItem.Candles[i].ValidationIssues)
log.Warnf(common.Data, "candle validation issue for %v %v %v: %v", klineItem.Exchange, klineItem.Asset, klineItem.Pair, klineItem.Candles[i].ValidationIssues)
}
}
case common.DataTrade:

View File

@@ -22,21 +22,22 @@ func (d *DataFromKline) HasDataAtTime(t time.Time) bool {
// Load sets the candle data to the stream for processing
func (d *DataFromKline) Load() error {
d.addedTimes = make(map[time.Time]bool)
d.addedTimes = make(map[int64]bool)
if len(d.Item.Candles) == 0 {
return errNoCandleData
}
klineData := make([]common.DataEventHandler, len(d.Item.Candles))
for i := range d.Item.Candles {
klineData[i] = &kline.Kline{
Base: event.Base{
Offset: int64(i + 1),
Exchange: d.Item.Exchange,
Time: d.Item.Candles[i].Time,
Interval: d.Item.Interval,
CurrencyPair: d.Item.Pair,
AssetType: d.Item.Asset,
newKline := &kline.Kline{
Base: &event.Base{
Offset: int64(i + 1),
Exchange: d.Item.Exchange,
Time: d.Item.Candles[i].Time.UTC(),
Interval: d.Item.Interval,
CurrencyPair: d.Item.Pair,
AssetType: d.Item.Asset,
UnderlyingPair: d.Item.UnderlyingPair,
},
Open: decimal.NewFromFloat(d.Item.Candles[i].Open),
High: decimal.NewFromFloat(d.Item.Candles[i].High),
@@ -45,7 +46,8 @@ func (d *DataFromKline) Load() error {
Volume: decimal.NewFromFloat(d.Item.Candles[i].Volume),
ValidationIssues: d.Item.Candles[i].ValidationIssues,
}
d.addedTimes[d.Item.Candles[i].Time] = true
klineData[i] = newKline
d.addedTimes[d.Item.Candles[i].Time.UTC().UnixNano()] = true
}
d.SetStream(klineData)
@@ -56,14 +58,14 @@ func (d *DataFromKline) Load() error {
// AppendResults adds a candle item to the data stream and sorts it to ensure it is all in order
func (d *DataFromKline) AppendResults(ki *gctkline.Item) {
if d.addedTimes == nil {
d.addedTimes = make(map[time.Time]bool)
d.addedTimes = make(map[int64]bool)
}
var gctCandles []gctkline.Candle
for i := range ki.Candles {
if _, ok := d.addedTimes[ki.Candles[i].Time]; !ok {
if _, ok := d.addedTimes[ki.Candles[i].Time.UnixNano()]; !ok {
gctCandles = append(gctCandles, ki.Candles[i])
d.addedTimes[ki.Candles[i].Time] = true
d.addedTimes[ki.Candles[i].Time.UnixNano()] = true
}
}
@@ -71,7 +73,7 @@ func (d *DataFromKline) AppendResults(ki *gctkline.Item) {
candleTimes := make([]time.Time, len(gctCandles))
for i := range gctCandles {
klineData[i] = &kline.Kline{
Base: event.Base{
Base: &event.Base{
Offset: int64(i + 1),
Exchange: ki.Exchange,
Time: gctCandles[i].Time,
@@ -93,7 +95,7 @@ func (d *DataFromKline) AppendResults(ki *gctkline.Item) {
d.RangeHolder.Ranges[i].Intervals[j].HasData = true
}
}
log.Debugf(log.BackTester, "appending %v candle intervals: %v", len(gctCandles), candleTimes)
log.Debugf(common.Data, "appending %v candle intervals: %v", len(gctCandles), candleTimes)
d.AppendStream(klineData...)
d.SortStream()
}
@@ -108,7 +110,7 @@ func (d *DataFromKline) StreamOpen() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Open
} else {
log.Errorf(log.BackTester, "incorrect data loaded into stream")
log.Errorf(common.Data, "incorrect data loaded into stream")
}
}
return ret
@@ -124,7 +126,7 @@ func (d *DataFromKline) StreamHigh() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.High
} else {
log.Errorf(log.BackTester, "incorrect data loaded into stream")
log.Errorf(common.Data, "incorrect data loaded into stream")
}
}
return ret
@@ -140,7 +142,7 @@ func (d *DataFromKline) StreamLow() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Low
} else {
log.Errorf(log.BackTester, "incorrect data loaded into stream")
log.Errorf(common.Data, "incorrect data loaded into stream")
}
}
return ret
@@ -156,7 +158,7 @@ func (d *DataFromKline) StreamClose() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Close
} else {
log.Errorf(log.BackTester, "incorrect data loaded into stream")
log.Errorf(common.Data, "incorrect data loaded into stream")
}
}
return ret
@@ -172,7 +174,7 @@ func (d *DataFromKline) StreamVol() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Volume
} else {
log.Errorf(log.BackTester, "incorrect data loaded into stream")
log.Errorf(common.Data, "incorrect data loaded into stream")
}
}
return ret

View File

@@ -140,7 +140,7 @@ func TestStreamOpen(t *testing.T) {
}
d.SetStream([]common.DataEventHandler{
&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: time.Now(),
Interval: gctkline.OneDay,
@@ -171,7 +171,7 @@ func TestStreamVolume(t *testing.T) {
}
d.SetStream([]common.DataEventHandler{
&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: time.Now(),
Interval: gctkline.OneDay,
@@ -202,7 +202,7 @@ func TestStreamClose(t *testing.T) {
}
d.SetStream([]common.DataEventHandler{
&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: time.Now(),
Interval: gctkline.OneDay,
@@ -233,7 +233,7 @@ func TestStreamHigh(t *testing.T) {
}
d.SetStream([]common.DataEventHandler{
&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: time.Now(),
Interval: gctkline.OneDay,
@@ -266,7 +266,7 @@ func TestStreamLow(t *testing.T) {
}
d.SetStream([]common.DataEventHandler{
&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: time.Now(),
Interval: gctkline.OneDay,

View File

@@ -2,7 +2,6 @@ package kline
import (
"errors"
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -14,7 +13,7 @@ var errNoCandleData = errors.New("no candle data provided")
// It holds candle data for a specified range with helper functions
type DataFromKline struct {
data.Base
addedTimes map[time.Time]bool
addedTimes map[int64]bool
Item gctkline.Item
RangeHolder *gctkline.IntervalRangeHolder
}

View File

@@ -1,16 +1,16 @@
# GoCryptoTrader Backtester: Backtest package
# GoCryptoTrader Backtester: Engine package
<img src="/backtester/common/backtester.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/backtest)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/engine)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This backtest package is part of the GoCryptoTrader codebase.
This engine package is part of the GoCryptoTrader codebase.
## This is still in active development
@@ -18,9 +18,9 @@ You can track ideas, planned features and what's in progress on this Trello boar
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Backtest package overview
## Engine package overview
The backtest package is the most important package of the GoCryptoTrader backtester. It is the engine which combines all elements.
The engine package is the most important package of the GoCryptoTrader backtester. It is the engine which combines all elements.
It is responsible for the following functionality
- Loading settings from a provided config file
- Retrieving data

View File

@@ -0,0 +1,473 @@
package engine
import (
"errors"
"fmt"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
// New returns a new BackTest instance
func New() *BackTest {
return &BackTest{
shutdown: make(chan struct{}),
Datas: &data.HandlerPerCurrency{},
EventQueue: &eventholder.Holder{},
}
}
// Reset BackTest values to default
func (bt *BackTest) Reset() {
bt.EventQueue.Reset()
bt.Datas.Reset()
bt.Portfolio.Reset()
bt.Statistic.Reset()
bt.Exchange.Reset()
bt.Funding.Reset()
bt.exchangeManager = nil
bt.orderManager = nil
bt.databaseManager = nil
}
// Run will iterate over loaded data events
// save them and then handle the event based on its type
func (bt *BackTest) Run() {
log.Info(common.Backtester, "running backtester against pre-defined data")
dataLoadingIssue:
for ev := bt.EventQueue.NextEvent(); ; ev = bt.EventQueue.NextEvent() {
if ev == nil {
dataHandlerMap := bt.Datas.GetAllData()
var hasProcessedData bool
for exchangeName, exchangeMap := range dataHandlerMap {
for assetItem, assetMap := range exchangeMap {
for currencyPair, dataHandler := range assetMap {
d := dataHandler.Next()
if d == nil {
if !bt.hasHandledEvent {
log.Errorf(common.Backtester, "Unable to perform `Next` for %v %v %v", exchangeName, assetItem, currencyPair)
}
break dataLoadingIssue
}
if bt.Strategy.UsingSimultaneousProcessing() && hasProcessedData {
// only append one event, as simultaneous processing
// will retrieve all relevant events to process under
// processSimultaneousDataEvents()
continue
}
bt.EventQueue.AppendEvent(d)
hasProcessedData = true
}
}
}
} else {
err := bt.handleEvent(ev)
if err != nil {
log.Error(common.Backtester, err)
}
}
if !bt.hasHandledEvent {
bt.hasHandledEvent = true
}
}
}
// handleEvent is the main processor of data for the backtester
// after data has been loaded and Run has appended a data event to the queue,
// handle event will process events and add further events to the queue if they
// are required
func (bt *BackTest) handleEvent(ev common.EventHandler) error {
if ev == nil {
return fmt.Errorf("cannot handle event %w", errNilData)
}
funds, err := bt.Funding.GetFundingForEvent(ev)
if err != nil {
return err
}
if bt.Funding.HasFutures() {
err = bt.Funding.UpdateCollateral(ev)
if err != nil {
return err
}
}
switch eType := ev.(type) {
case common.DataEventHandler:
if bt.Strategy.UsingSimultaneousProcessing() {
err = bt.processSimultaneousDataEvents()
} else {
err = bt.processSingleDataEvent(eType, funds.FundReleaser())
}
case signal.Event:
err = bt.processSignalEvent(eType, funds.FundReserver())
case order.Event:
err = bt.processOrderEvent(eType, funds.FundReleaser())
case fill.Event:
err = bt.processFillEvent(eType, funds.FundReleaser())
default:
return fmt.Errorf("handleEvent %w %T received, could not process",
errUnhandledDatatype,
ev)
}
if err != nil {
return err
}
bt.Funding.CreateSnapshot(ev.GetTime())
return nil
}
// processSingleDataEvent will pass the event to the strategy and determine how it should be handled
func (bt *BackTest) processSingleDataEvent(ev common.DataEventHandler, funds funding.IFundReleaser) error {
err := bt.updateStatsForDataEvent(ev, funds)
if err != nil {
return err
}
d, err := bt.Datas.GetDataForCurrency(ev)
if err != nil {
return err
}
s, err := bt.Strategy.OnSignal(d, bt.Funding, bt.Portfolio)
if err != nil {
if errors.Is(err, base.ErrTooMuchBadData) {
// too much bad data is a severe error and backtesting must cease
return err
}
log.Errorf(common.Backtester, "OnSignal %v", err)
return nil
}
err = bt.Statistic.SetEventForOffset(s)
if err != nil {
log.Errorf(common.Backtester, "SetEventForOffset %v", err)
}
bt.EventQueue.AppendEvent(s)
return nil
}
// processSimultaneousDataEvents determines what signal events are generated and appended
// to the event queue. It will pass all currency events to the strategy to determine what
// currencies to act upon
func (bt *BackTest) processSimultaneousDataEvents() error {
var dataEvents []data.Handler
dataHandlerMap := bt.Datas.GetAllData()
for _, exchangeMap := range dataHandlerMap {
for _, assetMap := range exchangeMap {
for _, dataHandler := range assetMap {
latestData := dataHandler.Latest()
funds, err := bt.Funding.GetFundingForEvent(latestData)
if err != nil {
return err
}
err = bt.updateStatsForDataEvent(latestData, funds.FundReleaser())
if err != nil {
switch {
case errors.Is(err, statistics.ErrAlreadyProcessed):
continue
case errors.Is(err, gctorder.ErrPositionLiquidated):
return nil
default:
log.Error(common.Backtester, err)
}
}
dataEvents = append(dataEvents, dataHandler)
}
}
}
signals, err := bt.Strategy.OnSimultaneousSignals(dataEvents, bt.Funding, bt.Portfolio)
if err != nil {
if errors.Is(err, base.ErrTooMuchBadData) {
// too much bad data is a severe error and backtesting must cease
return err
}
log.Errorf(common.Backtester, "OnSimultaneousSignals %v", err)
return nil
}
for i := range signals {
err = bt.Statistic.SetEventForOffset(signals[i])
if err != nil {
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", signals[i].GetExchange(), signals[i].GetAssetType(), signals[i].Pair(), err)
}
bt.EventQueue.AppendEvent(signals[i])
}
return nil
}
// updateStatsForDataEvent makes various systems aware of price movements from
// data events
func (bt *BackTest) updateStatsForDataEvent(ev common.DataEventHandler, funds funding.IFundReleaser) error {
if ev == nil {
return common.ErrNilEvent
}
if funds == nil {
return fmt.Errorf("%v %v %v %w missing fund releaser", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), common.ErrNilArguments)
}
// update statistics with the latest price
err := bt.Statistic.SetupEventForTime(ev)
if err != nil {
if errors.Is(err, statistics.ErrAlreadyProcessed) {
return err
}
log.Errorf(common.Backtester, "SetupEventForTime %v", err)
}
// update portfolio manager with the latest price
err = bt.Portfolio.UpdateHoldings(ev, funds)
if err != nil {
log.Errorf(common.Backtester, "UpdateHoldings %v", err)
}
if ev.GetAssetType().IsFutures() {
var cr funding.ICollateralReleaser
cr, err = funds.CollateralReleaser()
if err != nil {
return err
}
err = bt.Portfolio.UpdatePNL(ev, ev.GetClosePrice())
if err != nil {
if errors.Is(err, gctorder.ErrPositionsNotLoadedForPair) {
// if there is no position yet, there's nothing to update
return nil
}
if !errors.Is(err, gctorder.ErrPositionLiquidated) {
return fmt.Errorf("UpdatePNL %v", err)
}
}
var pnl *portfolio.PNLSummary
pnl, err = bt.Portfolio.GetLatestPNLForEvent(ev)
if err != nil {
return err
}
if pnl.Result.IsLiquidated {
return nil
}
err = bt.Portfolio.CheckLiquidationStatus(ev, cr, pnl)
if err != nil {
if errors.Is(err, gctorder.ErrPositionLiquidated) {
liquidErr := bt.triggerLiquidationsForExchange(ev, pnl)
if liquidErr != nil {
return liquidErr
}
}
return err
}
return bt.Statistic.AddPNLForTime(pnl)
}
return nil
}
func (bt *BackTest) triggerLiquidationsForExchange(ev common.DataEventHandler, pnl *portfolio.PNLSummary) error {
if ev == nil {
return common.ErrNilEvent
}
if pnl == nil {
return fmt.Errorf("%w pnl summary", common.ErrNilArguments)
}
orders, err := bt.Portfolio.CreateLiquidationOrdersForExchange(ev, bt.Funding)
if err != nil {
return err
}
for i := range orders {
// these orders are raising events for event offsets
// which may not have been processed yet
// this will create and store stats for each order
// then liquidate it at the funding level
var datas data.Handler
datas, err = bt.Datas.GetDataForCurrency(orders[i])
if err != nil {
return err
}
latest := datas.Latest()
err = bt.Statistic.SetupEventForTime(latest)
if err != nil && !errors.Is(err, statistics.ErrAlreadyProcessed) {
return err
}
bt.EventQueue.AppendEvent(orders[i])
err = bt.Statistic.SetEventForOffset(orders[i])
if err != nil {
log.Errorf(common.Backtester, "SetupEventForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
bt.Funding.Liquidate(orders[i])
}
pnl.Result.IsLiquidated = true
pnl.Result.Status = gctorder.Liquidated
return bt.Statistic.AddPNLForTime(pnl)
}
// processSignalEvent receives an event from the strategy for processing under the portfolio
func (bt *BackTest) processSignalEvent(ev signal.Event, funds funding.IFundReserver) error {
if ev == nil {
return common.ErrNilEvent
}
if funds == nil {
return fmt.Errorf("%w funds", common.ErrNilArguments)
}
cs, err := bt.Exchange.GetCurrencySettings(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
if err != nil {
log.Errorf(common.Backtester, "GetCurrencySettings %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("GetCurrencySettings %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
var o *order.Order
o, err = bt.Portfolio.OnSignal(ev, &cs, funds)
if err != nil {
log.Errorf(common.Backtester, "OnSignal %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("OnSignal %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Statistic.SetEventForOffset(o)
if err != nil {
return fmt.Errorf("SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
bt.EventQueue.AppendEvent(o)
return nil
}
func (bt *BackTest) processOrderEvent(ev order.Event, funds funding.IFundReleaser) error {
if ev == nil {
return common.ErrNilEvent
}
if funds == nil {
return fmt.Errorf("%w funds", common.ErrNilArguments)
}
d, err := bt.Datas.GetDataForCurrency(ev)
if err != nil {
return err
}
f, err := bt.Exchange.ExecuteOrder(ev, d, bt.orderManager, funds)
if err != nil {
if f == nil {
log.Errorf(common.Backtester, "ExecuteOrder fill event should always be returned, please fix, %v", err)
return fmt.Errorf("ExecuteOrder fill event should always be returned, please fix, %v", err)
}
if !errors.Is(err, exchange.ErrCannotTransact) {
log.Errorf(common.Backtester, "ExecuteOrder %v %v %v %v", f.GetExchange(), f.GetAssetType(), f.Pair(), err)
}
}
err = bt.Statistic.SetEventForOffset(f)
if err != nil {
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
bt.EventQueue.AppendEvent(f)
return nil
}
func (bt *BackTest) processFillEvent(ev fill.Event, funds funding.IFundReleaser) error {
t, err := bt.Portfolio.OnFill(ev, funds)
if err != nil {
return fmt.Errorf("OnFill %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Statistic.SetEventForOffset(t)
if err != nil {
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
var holding *holdings.Holding
holding, err = bt.Portfolio.ViewHoldingAtTimePeriod(ev)
if err != nil {
log.Error(common.Backtester, err)
}
if holding == nil {
log.Error(common.Backtester, "ViewHoldingAtTimePeriod why is holdings nil?")
} else {
err = bt.Statistic.AddHoldingsForTime(holding)
if err != nil {
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
}
var cp *compliance.Manager
cp, err = bt.Portfolio.GetComplianceManager(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
if err != nil {
log.Errorf(common.Backtester, "GetComplianceManager %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
snap := cp.GetLatestSnapshot()
err = bt.Statistic.AddComplianceSnapshotForTime(snap, ev)
if err != nil {
log.Errorf(common.Backtester, "AddComplianceSnapshotForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
fde := ev.GetFillDependentEvent()
if fde != nil && !fde.IsNil() {
// some events can only be triggered on a successful fill event
fde.SetOffset(ev.GetOffset())
err = bt.Statistic.SetEventForOffset(fde)
if err != nil {
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", fde.GetExchange(), fde.GetAssetType(), fde.Pair(), err)
}
od := ev.GetOrder()
if fde.MatchOrderAmount() && od != nil {
fde.SetAmount(ev.GetAmount())
}
fde.AppendReasonf("raising event after %v %v %v fill", ev.GetExchange(), ev.GetAssetType(), ev.Pair())
bt.EventQueue.AppendEvent(fde)
}
if ev.GetAssetType().IsFutures() {
return bt.processFuturesFillEvent(ev, funds)
}
return nil
}
func (bt *BackTest) processFuturesFillEvent(ev fill.Event, funds funding.IFundReleaser) error {
if ev.GetOrder() != nil {
pnl, err := bt.Portfolio.TrackFuturesOrder(ev, funds)
if err != nil && !errors.Is(err, gctorder.ErrSubmissionIsNil) {
return fmt.Errorf("TrackFuturesOrder %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.GetExchangeByName(ev.GetExchange())
if err != nil {
return fmt.Errorf("GetExchangeByName %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
rPNL := pnl.GetRealisedPNL()
if !rPNL.PNL.IsZero() {
var receivingCurrency currency.Code
var receivingAsset asset.Item
receivingCurrency, receivingAsset, err = exch.GetCurrencyForRealisedPNL(ev.GetAssetType(), ev.Pair())
if err != nil {
return fmt.Errorf("GetCurrencyForRealisedPNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Funding.RealisePNL(ev.GetExchange(), receivingAsset, receivingCurrency, rPNL.PNL)
if err != nil {
return fmt.Errorf("RealisePNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
}
err = bt.Statistic.AddPNLForTime(pnl)
if err != nil {
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
}
err := bt.Funding.UpdateCollateral(ev)
if err != nil {
return fmt.Errorf("UpdateCollateral %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
return nil
}
// Stop shuts down the live data loop
func (bt *BackTest) Stop() {
close(bt.shutdown)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
package backtest
package engine
import (
"errors"

139
backtester/engine/live.go Normal file
View File

@@ -0,0 +1,139 @@
package engine
import (
"context"
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/live"
"github.com/thrasher-corp/gocryptotrader/currency"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/log"
)
// RunLive is a proof of concept function that does not yet support multi currency usage
// It runs by constantly checking for new live datas and running through the list of events
// once new data is processed. It will run until application close event has been received
func (bt *BackTest) RunLive() error {
log.Info(common.Backtester, "running backtester against live data")
timeoutTimer := time.NewTimer(time.Minute * 5)
// a frequent timer so that when a new candle is released by an exchange
// that it can be processed quickly
processEventTicker := time.NewTicker(time.Second)
doneARun := false
for {
select {
case <-bt.shutdown:
return nil
case <-timeoutTimer.C:
return errLiveDataTimeout
case <-processEventTicker.C:
for e := bt.EventQueue.NextEvent(); ; e = bt.EventQueue.NextEvent() {
if e == nil {
// as live only supports singular currency, just get the proper reference manually
var d data.Handler
dd := bt.Datas.GetAllData()
for k1, v1 := range dd {
for k2, v2 := range v1 {
for k3 := range v2 {
d = dd[k1][k2][k3]
}
}
}
de := d.Next()
if de == nil {
break
}
bt.EventQueue.AppendEvent(de)
doneARun = true
continue
}
err := bt.handleEvent(e)
if err != nil {
return err
}
}
if doneARun {
timeoutTimer = time.NewTimer(time.Minute * 5)
}
}
}
}
// loadLiveDataLoop is an incomplete function to continuously retrieve exchange data on a loop
// from live. Its purpose is to be able to perform strategy analysis against current data
func (bt *BackTest) loadLiveDataLoop(resp *kline.DataFromKline, cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, dataType int64) {
startDate := time.Now().Add(-cfg.DataSettings.Interval.Duration() * 2)
dates, err := gctkline.CalculateCandleDateRanges(
startDate,
startDate.AddDate(1, 0, 0),
cfg.DataSettings.Interval,
0)
if err != nil {
log.Errorf(common.Backtester, "%v. Please check your GoCryptoTrader configuration", err)
return
}
candles, err := live.LoadData(context.TODO(),
exch,
dataType,
cfg.DataSettings.Interval.Duration(),
fPair,
a)
if err != nil {
log.Errorf(common.Backtester, "%v. Please check your GoCryptoTrader configuration", err)
return
}
dates.SetHasDataFromCandles(candles.Candles)
resp.RangeHolder = dates
resp.Item = *candles
loadNewDataTimer := time.NewTimer(time.Second * 5)
for {
select {
case <-bt.shutdown:
return
case <-loadNewDataTimer.C:
log.Infof(common.Backtester, "fetching data for %v %v %v %v", exch.GetName(), a, fPair, cfg.DataSettings.Interval)
loadNewDataTimer.Reset(time.Second * 15)
err = bt.loadLiveData(resp, cfg, exch, fPair, a, dataType)
if err != nil {
log.Error(common.Backtester, err)
return
}
}
}
}
func (bt *BackTest) loadLiveData(resp *kline.DataFromKline, cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, dataType int64) error {
if resp == nil {
return errNilData
}
if cfg == nil {
return errNilConfig
}
if exch == nil {
return errNilExchange
}
candles, err := live.LoadData(context.TODO(),
exch,
dataType,
cfg.DataSettings.Interval.Duration(),
fPair,
a)
if err != nil {
return err
}
if len(candles.Candles) == 0 {
return nil
}
resp.AppendResults(candles)
bt.Reports.UpdateItem(&resp.Item)
log.Info(common.Backtester, "sleeping for 30 seconds before checking for new candle data")
return nil
}

867
backtester/engine/setup.go Normal file
View File

@@ -0,0 +1,867 @@
package engine
import (
"context"
"errors"
"fmt"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/api"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/csv"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/database"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange/slippage"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/backtester/funding/trackingcurrencies"
"github.com/thrasher-corp/gocryptotrader/backtester/report"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
gctconfig "github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
gctdatabase "github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/engine"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
// NewFromConfig takes a strategy config and configures a backtester variable to run
func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool) (*BackTest, error) {
log.Infoln(common.Setup, "loading config...")
if cfg == nil {
return nil, errNilConfig
}
var err error
bt := New()
bt.exchangeManager = engine.SetupExchangeManager()
bt.orderManager, err = engine.SetupOrderManager(bt.exchangeManager, &engine.CommunicationManager{}, &sync.WaitGroup{}, false, false)
if err != nil {
return nil, err
}
err = bt.orderManager.Start()
if err != nil {
return nil, err
}
if cfg.DataSettings.DatabaseData != nil {
bt.databaseManager, err = engine.SetupDatabaseConnectionManager(&cfg.DataSettings.DatabaseData.Config)
if err != nil {
return nil, err
}
}
reports := &report.Data{
Config: cfg,
TemplatePath: templatePath,
OutputPath: output,
}
bt.Reports = reports
buyRule := exchange.MinMax{
MinimumSize: cfg.PortfolioSettings.BuySide.MinimumSize,
MaximumSize: cfg.PortfolioSettings.BuySide.MaximumSize,
MaximumTotal: cfg.PortfolioSettings.BuySide.MaximumTotal,
}
sellRule := exchange.MinMax{
MinimumSize: cfg.PortfolioSettings.SellSide.MinimumSize,
MaximumSize: cfg.PortfolioSettings.SellSide.MaximumSize,
MaximumTotal: cfg.PortfolioSettings.SellSide.MaximumTotal,
}
sizeManager := &size.Size{
BuySide: buyRule,
SellSide: sellRule,
}
funds, err := funding.SetupFundingManager(
bt.exchangeManager,
cfg.FundingSettings.UseExchangeLevelFunding,
cfg.StrategySettings.DisableUSDTracking,
)
if err != nil {
return nil, err
}
if cfg.FundingSettings.UseExchangeLevelFunding {
for i := range cfg.FundingSettings.ExchangeLevelFunding {
a := cfg.FundingSettings.ExchangeLevelFunding[i].Asset
cq := cfg.FundingSettings.ExchangeLevelFunding[i].Currency
var item *funding.Item
item, err = funding.CreateItem(cfg.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
a,
cq,
cfg.FundingSettings.ExchangeLevelFunding[i].InitialFunds,
cfg.FundingSettings.ExchangeLevelFunding[i].TransferFee)
if err != nil {
return nil, err
}
err = funds.AddItem(item)
if err != nil {
return nil, err
}
}
}
var emm = make(map[string]gctexchange.IBotExchange)
for i := range cfg.CurrencySettings {
_, ok := emm[cfg.CurrencySettings[i].ExchangeName]
if ok {
continue
}
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.NewExchangeByName(cfg.CurrencySettings[i].ExchangeName)
if err != nil {
return nil, err
}
var conf *gctconfig.Exchange
conf, err = exch.GetDefaultConfig()
if err != nil {
return nil, err
}
conf.Enabled = true
conf.WebsocketTrafficTimeout = time.Second
conf.Websocket = convert.BoolPtr(false)
conf.WebsocketResponseCheckTimeout = time.Second
conf.WebsocketResponseMaxLimit = time.Second
conf.Verbose = verbose
err = exch.Setup(conf)
if err != nil {
return nil, err
}
exchBase := exch.GetBase()
err = exch.UpdateTradablePairs(context.Background(), true)
if err != nil {
return nil, err
}
assets := exchBase.CurrencyPairs.GetAssetTypes(false)
for i := range assets {
exchBase.CurrencyPairs.Pairs[assets[i]].AssetEnabled = convert.BoolPtr(true)
err = exch.SetPairs(exchBase.CurrencyPairs.Pairs[assets[i]].Available, assets[i], true)
if err != nil {
return nil, err
}
}
bt.exchangeManager.Add(exch)
emm[cfg.CurrencySettings[i].ExchangeName] = exch
}
portfolioRisk := &risk.Risk{
CurrencySettings: make(map[string]map[asset.Item]map[currency.Pair]*risk.CurrencySettings),
}
for i := range cfg.CurrencySettings {
if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName] == nil {
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName] = make(map[asset.Item]map[currency.Pair]*risk.CurrencySettings)
}
a := cfg.CurrencySettings[i].Asset
if err != nil {
return nil, fmt.Errorf(
"%w for %v %v %v-%v. Err %v",
errInvalidConfigAsset,
cfg.CurrencySettings[i].ExchangeName,
cfg.CurrencySettings[i].Asset,
cfg.CurrencySettings[i].Base,
cfg.CurrencySettings[i].Quote,
err)
}
if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] == nil {
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] = make(map[currency.Pair]*risk.CurrencySettings)
}
var curr currency.Pair
var b, q currency.Code
b = cfg.CurrencySettings[i].Base
q = cfg.CurrencySettings[i].Quote
curr = currency.NewPair(b, q)
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.GetExchangeByName(cfg.CurrencySettings[i].ExchangeName)
if err != nil {
return nil, err
}
exchBase := exch.GetBase()
var requestFormat currency.PairFormat
requestFormat, err = exchBase.GetPairFormat(a, true)
if err != nil {
return nil, fmt.Errorf("could not get pair format %v, %w", curr, err)
}
curr = curr.Format(requestFormat.Delimiter, requestFormat.Uppercase)
var avail, enabled currency.Pairs
avail, err = exch.GetAvailablePairs(a)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
enabled, err = exch.GetEnabledPairs(a)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
avail = avail.Add(curr)
enabled = enabled.Add(curr)
err = exch.SetPairs(enabled, a, true)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
err = exch.SetPairs(avail, a, false)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
portSet := &risk.CurrencySettings{
MaximumHoldingRatio: cfg.CurrencySettings[i].MaximumHoldingsRatio,
}
if cfg.CurrencySettings[i].FuturesDetails != nil {
portSet.MaximumOrdersWithLeverageRatio = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio
portSet.MaxLeverageRate = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate
}
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][curr] = portSet
if cfg.CurrencySettings[i].MakerFee != nil &&
cfg.CurrencySettings[i].TakerFee != nil &&
cfg.CurrencySettings[i].MakerFee.GreaterThan(*cfg.CurrencySettings[i].TakerFee) {
log.Warnf(common.Setup, "maker fee '%v' should not exceed taker fee '%v'. Please review config",
cfg.CurrencySettings[i].MakerFee,
cfg.CurrencySettings[i].TakerFee)
}
var baseItem, quoteItem, futureItem *funding.Item
if cfg.FundingSettings.UseExchangeLevelFunding {
switch {
case a == asset.Spot:
// add any remaining currency items that have no funding data in the strategy config
baseItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
a,
b,
decimal.Zero,
decimal.Zero)
if err != nil {
return nil, err
}
quoteItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
a,
q,
decimal.Zero,
decimal.Zero)
if err != nil {
return nil, err
}
err = funds.AddItem(baseItem)
if err != nil && !errors.Is(err, funding.ErrAlreadyExists) {
return nil, err
}
err = funds.AddItem(quoteItem)
if err != nil && !errors.Is(err, funding.ErrAlreadyExists) {
return nil, err
}
case a.IsFutures():
// setup contract items
c := funding.CreateFuturesCurrencyCode(b, q)
futureItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
a,
c,
decimal.Zero,
decimal.Zero)
if err != nil {
return nil, err
}
var collateralCurrency currency.Code
collateralCurrency, _, err = exch.GetCollateralCurrencyForContract(a, currency.NewPair(b, q))
if err != nil {
return nil, err
}
err = funds.LinkCollateralCurrency(futureItem, collateralCurrency)
if err != nil {
return nil, err
}
err = funds.AddItem(futureItem)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("%w: %v unsupported", errInvalidConfigAsset, a)
}
} else {
var bFunds, qFunds decimal.Decimal
if cfg.CurrencySettings[i].SpotDetails != nil {
if cfg.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
bFunds = *cfg.CurrencySettings[i].SpotDetails.InitialBaseFunds
}
if cfg.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil {
qFunds = *cfg.CurrencySettings[i].SpotDetails.InitialQuoteFunds
}
}
baseItem, err = funding.CreateItem(
cfg.CurrencySettings[i].ExchangeName,
a,
curr.Base,
bFunds,
decimal.Zero)
if err != nil {
return nil, err
}
quoteItem, err = funding.CreateItem(
cfg.CurrencySettings[i].ExchangeName,
a,
curr.Quote,
qFunds,
decimal.Zero)
if err != nil {
return nil, err
}
var pair *funding.SpotPair
pair, err = funding.CreatePair(baseItem, quoteItem)
if err != nil {
return nil, err
}
err = funds.AddPair(pair)
if err != nil {
return nil, err
}
}
}
bt.Funding = funds
var p *portfolio.Portfolio
p, err = portfolio.Setup(sizeManager, portfolioRisk, cfg.StatisticSettings.RiskFreeRate)
if err != nil {
return nil, err
}
bt.Strategy, err = strategies.LoadStrategyByName(cfg.StrategySettings.Name, cfg.StrategySettings.SimultaneousSignalProcessing)
if err != nil {
return nil, err
}
bt.Strategy.SetDefaults()
if cfg.StrategySettings.CustomSettings != nil {
err = bt.Strategy.SetCustomSettings(cfg.StrategySettings.CustomSettings)
if err != nil && !errors.Is(err, base.ErrCustomSettingsUnsupported) {
return nil, err
}
}
stats := &statistics.Statistic{
StrategyName: bt.Strategy.Name(),
StrategyNickname: cfg.Nickname,
StrategyDescription: bt.Strategy.Description(),
StrategyGoal: cfg.Goal,
ExchangeAssetPairStatistics: make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic),
RiskFreeRate: cfg.StatisticSettings.RiskFreeRate,
CandleInterval: cfg.DataSettings.Interval,
FundManager: bt.Funding,
}
bt.Statistic = stats
reports.Statistics = stats
if !cfg.StrategySettings.DisableUSDTracking {
var trackingPairs []trackingcurrencies.TrackingPair
for i := range cfg.CurrencySettings {
trackingPairs = append(trackingPairs, trackingcurrencies.TrackingPair{
Exchange: cfg.CurrencySettings[i].ExchangeName,
Asset: cfg.CurrencySettings[i].Asset,
Base: cfg.CurrencySettings[i].Base,
Quote: cfg.CurrencySettings[i].Quote,
})
}
trackingPairs, err = trackingcurrencies.CreateUSDTrackingPairs(trackingPairs, bt.exchangeManager)
if err != nil {
return nil, err
}
trackingPairCheck:
for i := range trackingPairs {
for j := range cfg.CurrencySettings {
if cfg.CurrencySettings[j].ExchangeName == trackingPairs[i].Exchange &&
cfg.CurrencySettings[j].Asset == trackingPairs[i].Asset &&
cfg.CurrencySettings[j].Base.Equal(trackingPairs[i].Base) &&
cfg.CurrencySettings[j].Quote.Equal(trackingPairs[i].Quote) {
continue trackingPairCheck
}
}
cfg.CurrencySettings = append(cfg.CurrencySettings, config.CurrencySettings{
ExchangeName: trackingPairs[i].Exchange,
Asset: trackingPairs[i].Asset,
Base: trackingPairs[i].Base,
Quote: trackingPairs[i].Quote,
USDTrackingPair: true,
})
}
}
e, err := bt.setupExchangeSettings(cfg)
if err != nil {
return nil, err
}
bt.Exchange = &e
for i := range e.CurrencySettings {
err = p.SetupCurrencySettingsMap(&e.CurrencySettings[i])
if err != nil {
return nil, err
}
}
bt.Portfolio = p
cfg.PrintSetting()
return bt, nil
}
func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange, error) {
log.Infoln(common.Setup, "setting exchange settings...")
resp := exchange.Exchange{}
for i := range cfg.CurrencySettings {
exch, pair, a, err := bt.loadExchangePairAssetBase(
cfg.CurrencySettings[i].ExchangeName,
cfg.CurrencySettings[i].Base,
cfg.CurrencySettings[i].Quote,
cfg.CurrencySettings[i].Asset)
if err != nil {
return resp, err
}
exchangeName := strings.ToLower(exch.GetName())
bt.Datas.Setup()
klineData, err := bt.loadData(cfg, exch, pair, a, cfg.CurrencySettings[i].USDTrackingPair)
if err != nil {
return resp, err
}
err = bt.Funding.AddUSDTrackingData(klineData)
if err != nil &&
!errors.Is(err, trackingcurrencies.ErrCurrencyDoesNotContainsUSD) &&
!errors.Is(err, funding.ErrUSDTrackingDisabled) {
return resp, err
}
if cfg.CurrencySettings[i].USDTrackingPair {
continue
}
bt.Datas.SetDataForCurrency(exchangeName, a, pair, klineData)
var makerFee, takerFee decimal.Decimal
if cfg.CurrencySettings[i].MakerFee != nil && cfg.CurrencySettings[i].MakerFee.GreaterThan(decimal.Zero) {
makerFee = *cfg.CurrencySettings[i].MakerFee
}
if cfg.CurrencySettings[i].TakerFee != nil && cfg.CurrencySettings[i].TakerFee.GreaterThan(decimal.Zero) {
takerFee = *cfg.CurrencySettings[i].TakerFee
}
if cfg.CurrencySettings[i].TakerFee == nil || cfg.CurrencySettings[i].MakerFee == nil {
var apiMakerFee, apiTakerFee decimal.Decimal
apiMakerFee, apiTakerFee = getFees(context.TODO(), exch, pair)
if cfg.CurrencySettings[i].MakerFee == nil {
makerFee = apiMakerFee
cfg.CurrencySettings[i].MakerFee = &makerFee
cfg.CurrencySettings[i].UsingExchangeMakerFee = true
}
if cfg.CurrencySettings[i].TakerFee == nil {
takerFee = apiTakerFee
cfg.CurrencySettings[i].TakerFee = &takerFee
cfg.CurrencySettings[i].UsingExchangeTakerFee = true
}
}
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(decimal.Zero) {
log.Warnf(common.Setup, "invalid maximum slippage percent '%v'. Slippage percent is defined as a number, eg '100.00', defaulting to '%v'",
cfg.CurrencySettings[i].MaximumSlippagePercent,
slippage.DefaultMaximumSlippagePercent)
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
}
if cfg.CurrencySettings[i].MaximumSlippagePercent.IsZero() {
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
}
if cfg.CurrencySettings[i].MinimumSlippagePercent.LessThan(decimal.Zero) {
log.Warnf(common.Setup, "invalid minimum slippage percent '%v'. Slippage percent is defined as a number, eg '80.00', defaulting to '%v'",
cfg.CurrencySettings[i].MinimumSlippagePercent,
slippage.DefaultMinimumSlippagePercent)
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
}
if cfg.CurrencySettings[i].MinimumSlippagePercent.IsZero() {
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
}
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(cfg.CurrencySettings[i].MinimumSlippagePercent) {
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
}
realOrders := false
if cfg.DataSettings.LiveData != nil {
realOrders = cfg.DataSettings.LiveData.RealOrders
}
buyRule := exchange.MinMax{
MinimumSize: cfg.CurrencySettings[i].BuySide.MinimumSize,
MaximumSize: cfg.CurrencySettings[i].BuySide.MaximumSize,
MaximumTotal: cfg.CurrencySettings[i].BuySide.MaximumTotal,
}
sellRule := exchange.MinMax{
MinimumSize: cfg.CurrencySettings[i].SellSide.MinimumSize,
MaximumSize: cfg.CurrencySettings[i].SellSide.MaximumSize,
MaximumTotal: cfg.CurrencySettings[i].SellSide.MaximumTotal,
}
limits, err := exch.GetOrderExecutionLimits(a, pair)
if err != nil && !errors.Is(err, gctorder.ErrExchangeLimitNotLoaded) {
return resp, err
}
if limits != (gctorder.MinMaxLevel{}) {
if !cfg.CurrencySettings[i].CanUseExchangeLimits {
log.Warnf(common.Setup, "exchange %s order execution limits supported but disabled for %s %s, live results may differ",
cfg.CurrencySettings[i].ExchangeName,
pair,
a)
cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true
}
}
var lev exchange.Leverage
if cfg.CurrencySettings[i].FuturesDetails != nil {
lev = exchange.Leverage{
CanUseLeverage: cfg.CurrencySettings[i].FuturesDetails.Leverage.CanUseLeverage,
MaximumLeverageRate: cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate,
MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio,
}
}
resp.CurrencySettings = append(resp.CurrencySettings, exchange.Settings{
Exchange: exch,
MinimumSlippageRate: cfg.CurrencySettings[i].MinimumSlippagePercent,
MaximumSlippageRate: cfg.CurrencySettings[i].MaximumSlippagePercent,
Pair: pair,
Asset: a,
MakerFee: makerFee,
TakerFee: takerFee,
UseRealOrders: realOrders,
BuySide: buyRule,
SellSide: sellRule,
Leverage: lev,
Limits: limits,
SkipCandleVolumeFitting: cfg.CurrencySettings[i].SkipCandleVolumeFitting,
CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits,
UseExchangePNLCalculation: cfg.CurrencySettings[i].UseExchangePNLCalculation,
})
}
return resp, nil
}
func (bt *BackTest) loadExchangePairAssetBase(exch string, base, quote currency.Code, ai asset.Item) (gctexchange.IBotExchange, currency.Pair, asset.Item, error) {
e, err := bt.exchangeManager.GetExchangeByName(exch)
if err != nil {
return nil, currency.EMPTYPAIR, asset.Empty, err
}
var cp, fPair currency.Pair
cp = currency.NewPair(base, quote)
exchangeBase := e.GetBase()
if exchangeBase.ValidateAPICredentials(exchangeBase.GetDefaultCredentials()) != nil {
log.Warnf(common.Setup, "no credentials set for %v, this is theoretical only", exchangeBase.Name)
}
fPair, err = exchangeBase.FormatExchangeCurrency(cp, ai)
if err != nil {
return nil, currency.EMPTYPAIR, asset.Empty, err
}
return e, fPair, ai, nil
}
// getFees will return an exchange's fee rate from GCT's wrapper function
func getFees(ctx context.Context, exch gctexchange.IBotExchange, fPair currency.Pair) (makerFee, takerFee decimal.Decimal) {
fTakerFee, err := exch.GetFeeByType(ctx,
&gctexchange.FeeBuilder{FeeType: gctexchange.OfflineTradeFee,
Pair: fPair,
IsMaker: false,
PurchasePrice: 1,
Amount: 1,
})
if err != nil {
log.Errorf(common.Setup, "Could not retrieve taker fee for %v. %v", exch.GetName(), err)
}
fMakerFee, err := exch.GetFeeByType(ctx,
&gctexchange.FeeBuilder{
FeeType: gctexchange.OfflineTradeFee,
Pair: fPair,
IsMaker: true,
PurchasePrice: 1,
Amount: 1,
})
if err != nil {
log.Errorf(common.Setup, "Could not retrieve maker fee for %v. %v", exch.GetName(), err)
}
return decimal.NewFromFloat(fMakerFee), decimal.NewFromFloat(fTakerFee)
}
// loadData will create kline data from the sources defined in start config files. It can exist from databases, csv or API endpoints
// it can also be generated from trade data which will be converted into kline data
func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
if exch == nil {
return nil, engine.ErrExchangeNotFound
}
b := exch.GetBase()
if cfg.DataSettings.DatabaseData == nil &&
cfg.DataSettings.LiveData == nil &&
cfg.DataSettings.APIData == nil &&
cfg.DataSettings.CSVData == nil {
return nil, errNoDataSource
}
if (cfg.DataSettings.APIData != nil && cfg.DataSettings.DatabaseData != nil) ||
(cfg.DataSettings.APIData != nil && cfg.DataSettings.LiveData != nil) ||
(cfg.DataSettings.APIData != nil && cfg.DataSettings.CSVData != nil) ||
(cfg.DataSettings.DatabaseData != nil && cfg.DataSettings.LiveData != nil) ||
(cfg.DataSettings.CSVData != nil && cfg.DataSettings.LiveData != nil) ||
(cfg.DataSettings.CSVData != nil && cfg.DataSettings.DatabaseData != nil) {
return nil, errAmbiguousDataSource
}
dataType, err := common.DataTypeToInt(cfg.DataSettings.DataType)
if err != nil {
return nil, err
}
log.Infof(common.Setup, "loading data for %v %v %v...\n", exch.GetName(), a, fPair)
resp := &kline.DataFromKline{}
switch {
case cfg.DataSettings.CSVData != nil:
if cfg.DataSettings.Interval <= 0 {
return nil, errIntervalUnset
}
resp, err = csv.LoadData(
dataType,
cfg.DataSettings.CSVData.FullPath,
strings.ToLower(exch.GetName()),
cfg.DataSettings.Interval.Duration(),
fPair,
a,
isUSDTrackingPair)
if err != nil {
return nil, fmt.Errorf("%v. Please check your GoCryptoTrader configuration", err)
}
resp.Item.RemoveDuplicates()
resp.Item.SortCandlesByTimestamp(false)
resp.RangeHolder, err = gctkline.CalculateCandleDateRanges(
resp.Item.Candles[0].Time,
resp.Item.Candles[len(resp.Item.Candles)-1].Time.Add(cfg.DataSettings.Interval.Duration()),
cfg.DataSettings.Interval,
0,
)
if err != nil {
return nil, err
}
resp.RangeHolder.SetHasDataFromCandles(resp.Item.Candles)
summary := resp.RangeHolder.DataSummary(false)
if len(summary) > 0 {
log.Warnf(common.Setup, "%v", summary)
}
case cfg.DataSettings.DatabaseData != nil:
if cfg.DataSettings.DatabaseData.InclusiveEndDate {
cfg.DataSettings.DatabaseData.EndDate = cfg.DataSettings.DatabaseData.EndDate.Add(cfg.DataSettings.Interval.Duration())
}
if cfg.DataSettings.DatabaseData.Path == "" {
cfg.DataSettings.DatabaseData.Path = filepath.Join(gctcommon.GetDefaultDataDir(runtime.GOOS), "database")
}
gctdatabase.DB.DataPath = cfg.DataSettings.DatabaseData.Path
err = gctdatabase.DB.SetConfig(&cfg.DataSettings.DatabaseData.Config)
if err != nil {
return nil, err
}
err = bt.databaseManager.Start(&sync.WaitGroup{})
if err != nil {
return nil, err
}
defer func() {
stopErr := bt.databaseManager.Stop()
if stopErr != nil {
log.Error(common.Setup, stopErr)
}
}()
resp, err = loadDatabaseData(cfg, exch.GetName(), fPair, a, dataType, isUSDTrackingPair)
if err != nil {
return nil, fmt.Errorf("unable to retrieve data from GoCryptoTrader database. Error: %v. Please ensure the database is setup correctly and has data before use", err)
}
resp.Item.RemoveDuplicates()
resp.Item.SortCandlesByTimestamp(false)
resp.RangeHolder, err = gctkline.CalculateCandleDateRanges(
cfg.DataSettings.DatabaseData.StartDate,
cfg.DataSettings.DatabaseData.EndDate,
cfg.DataSettings.Interval,
0,
)
if err != nil {
return nil, err
}
resp.RangeHolder.SetHasDataFromCandles(resp.Item.Candles)
summary := resp.RangeHolder.DataSummary(false)
if len(summary) > 0 {
log.Warnf(common.Setup, "%v", summary)
}
case cfg.DataSettings.APIData != nil:
if cfg.DataSettings.APIData.InclusiveEndDate {
cfg.DataSettings.APIData.EndDate = cfg.DataSettings.APIData.EndDate.Add(cfg.DataSettings.Interval.Duration())
}
resp, err = loadAPIData(
cfg,
exch,
fPair,
a,
b.Features.Enabled.Kline.ResultLimit,
dataType)
if err != nil {
return resp, err
}
case cfg.DataSettings.LiveData != nil:
if isUSDTrackingPair {
return nil, errLiveUSDTrackingNotSupported
}
if len(cfg.CurrencySettings) > 1 {
return nil, errors.New("live data simulation only supports one currency")
}
err = loadLiveData(cfg, b)
if err != nil {
return nil, err
}
go bt.loadLiveDataLoop(
resp,
cfg,
exch,
fPair,
a,
dataType)
return resp, nil
}
if resp == nil {
return nil, fmt.Errorf("processing error, response returned nil")
}
if a.IsFutures() {
// returning the collateral currency along with using the
// fPair base creates a pair that links the futures contract to
// is underlying pair
// eg BTC-PERP on FTX has a collateral currency of USD
// taking the BTC base and USD as quote, allows linking
// BTC-USD and BTC-PERP
var curr currency.Code
curr, _, err = exch.GetCollateralCurrencyForContract(a, fPair)
if err != nil {
return resp, err
}
resp.Item.UnderlyingPair = currency.NewPair(fPair.Base, curr)
}
err = b.ValidateKline(fPair, a, resp.Item.Interval)
if err != nil {
if dataType != common.DataTrade || !strings.EqualFold(err.Error(), "interval not supported") {
return nil, err
}
}
err = resp.Load()
if err != nil {
return nil, err
}
bt.Reports.AddKlineItem(&resp.Item)
return resp, nil
}
func loadDatabaseData(cfg *config.Config, name string, fPair currency.Pair, a asset.Item, dataType int64, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
if cfg == nil || cfg.DataSettings.DatabaseData == nil {
return nil, errors.New("nil config data received")
}
if cfg.DataSettings.Interval <= 0 {
return nil, errIntervalUnset
}
return database.LoadData(
cfg.DataSettings.DatabaseData.StartDate,
cfg.DataSettings.DatabaseData.EndDate,
cfg.DataSettings.Interval.Duration(),
strings.ToLower(name),
dataType,
fPair,
a,
isUSDTrackingPair)
}
func loadAPIData(cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, resultLimit uint32, dataType int64) (*kline.DataFromKline, error) {
if cfg.DataSettings.Interval <= 0 {
return nil, errIntervalUnset
}
dates, err := gctkline.CalculateCandleDateRanges(
cfg.DataSettings.APIData.StartDate,
cfg.DataSettings.APIData.EndDate,
cfg.DataSettings.Interval,
resultLimit)
if err != nil {
return nil, err
}
candles, err := api.LoadData(context.TODO(),
dataType,
cfg.DataSettings.APIData.StartDate,
cfg.DataSettings.APIData.EndDate,
cfg.DataSettings.Interval.Duration(),
exch,
fPair,
a)
if err != nil {
return nil, fmt.Errorf("%v. Please check your GoCryptoTrader configuration", err)
}
dates.SetHasDataFromCandles(candles.Candles)
summary := dates.DataSummary(false)
if len(summary) > 0 {
log.Warnf(common.Setup, "%v", summary)
}
candles.FillMissingDataWithEmptyEntries(dates)
candles.RemoveOutsideRange(cfg.DataSettings.APIData.StartDate, cfg.DataSettings.APIData.EndDate)
return &kline.DataFromKline{
Item: *candles,
RangeHolder: dates,
}, nil
}
func loadLiveData(cfg *config.Config, base *gctexchange.Base) error {
if cfg == nil || base == nil || cfg.DataSettings.LiveData == nil {
return common.ErrNilArguments
}
if cfg.DataSettings.Interval <= 0 {
return errIntervalUnset
}
if cfg.DataSettings.LiveData.APIKeyOverride != "" {
base.API.SetKey(cfg.DataSettings.LiveData.APIKeyOverride)
}
if cfg.DataSettings.LiveData.APISecretOverride != "" {
base.API.SetSecret(cfg.DataSettings.LiveData.APISecretOverride)
}
if cfg.DataSettings.LiveData.APIClientIDOverride != "" {
base.API.SetClientID(cfg.DataSettings.LiveData.APIClientIDOverride)
}
if cfg.DataSettings.LiveData.API2FAOverride != "" {
base.API.SetPEMKey(cfg.DataSettings.LiveData.API2FAOverride)
}
if cfg.DataSettings.LiveData.APISubAccountOverride != "" {
base.API.SetSubAccount(cfg.DataSettings.LiveData.APISubAccountOverride)
}
validated := base.AreCredentialsValid(context.TODO())
base.API.AuthenticatedSupport = validated
if !validated && cfg.DataSettings.LiveData.RealOrders {
log.Warn(common.Setup, "invalid API credentials set, real orders set to false")
cfg.DataSettings.LiveData.RealOrders = false
}
return nil
}

View File

@@ -2,14 +2,15 @@ package exchange
import (
"context"
"errors"
"fmt"
"strings"
"github.com/gofrs/uuid"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange/slippage"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
@@ -25,44 +26,58 @@ func (e *Exchange) Reset() {
*e = Exchange{}
}
// ErrCannotTransact returns when its an issue to do nothing for an event
var ErrCannotTransact = errors.New("cannot transact")
// ExecuteOrder assesses the portfolio manager's order event and if it passes validation
// will send an order to the exchange/fake order manager to be stored and raise a fill event
func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *engine.OrderManager, funds funding.IPairReleaser) (*fill.Fill, error) {
func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *engine.OrderManager, funds funding.IFundReleaser) (fill.Event, error) {
f := &fill.Fill{
Base: event.Base{
Offset: o.GetOffset(),
Exchange: o.GetExchange(),
Time: o.GetTime(),
CurrencyPair: o.Pair(),
AssetType: o.GetAssetType(),
Interval: o.GetInterval(),
Reason: o.GetReason(),
},
Direction: o.GetDirection(),
Amount: o.GetAmount(),
ClosePrice: data.Latest().GetClosePrice(),
Base: o.GetBase(),
Direction: o.GetDirection(),
Amount: o.GetAmount(),
ClosePrice: o.GetClosePrice(),
FillDependentEvent: o.GetFillDependentEvent(),
Liquidated: o.IsLiquidating(),
}
eventFunds := o.GetAllocatedFunds()
if !common.CanTransact(o.GetDirection()) {
return f, fmt.Errorf("%w order direction %v", ErrCannotTransact, o.GetDirection())
}
allocatedFunds := o.GetAllocatedFunds()
cs, err := e.GetCurrencySettings(o.GetExchange(), o.GetAssetType(), o.Pair())
if err != nil {
return f, err
}
f.ExchangeFee = cs.ExchangeFee // defaulting to just using taker fee right now without orderbook
f.Direction = o.GetDirection()
if o.GetDirection() != gctorder.Buy && o.GetDirection() != gctorder.Sell {
return f, nil
}
highStr := data.StreamHigh()
high := highStr[len(highStr)-1]
lowStr := data.StreamLow()
low := lowStr[len(lowStr)-1]
volStr := data.StreamVol()
volume := volStr[len(volStr)-1]
var adjustedPrice, amount decimal.Decimal
var price, adjustedPrice,
amount, adjustedAmount,
fee decimal.Decimal
amount = o.GetAmount()
price = o.GetClosePrice()
if cs.UseRealOrders {
if o.IsLiquidating() {
// Liquidation occurs serverside
if o.GetAssetType().IsFutures() {
var cr funding.ICollateralReleaser
cr, err = funds.CollateralReleaser()
if err != nil {
return f, err
}
// update local records
cr.Liquidate()
} else {
var pr funding.IPairReleaser
pr, err = funds.PairReleaser()
if err != nil {
return f, err
}
// update local records
pr.Liquidate()
}
return f, nil
}
// get current orderbook
var ob *orderbook.Base
ob, err = orderbook.Get(f.Exchange, f.CurrencyPair, f.AssetType)
@@ -70,73 +85,87 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
return f, err
}
// calculate an estimated slippage rate
adjustedPrice, amount = slippage.CalculateSlippageByOrderbook(ob, o.GetDirection(), eventFunds, f.ExchangeFee)
f.Slippage = adjustedPrice.Sub(f.ClosePrice).Div(f.ClosePrice).Mul(decimal.NewFromInt(100))
price, amount = slippage.CalculateSlippageByOrderbook(ob, o.GetDirection(), allocatedFunds, f.ExchangeFee)
f.Slippage = price.Sub(f.ClosePrice).Div(f.ClosePrice).Mul(decimal.NewFromInt(100))
} else {
adjustedPrice, amount, err = e.sizeOfflineOrder(high, low, volume, &cs, f)
if err != nil {
slippageRate := slippage.EstimateSlippagePercentage(cs.MinimumSlippageRate, cs.MaximumSlippageRate)
if cs.SkipCandleVolumeFitting || o.GetAssetType().IsFutures() {
f.VolumeAdjustedPrice = f.ClosePrice
amount = f.Amount
} else {
highStr := data.StreamHigh()
high := highStr[len(highStr)-1]
lowStr := data.StreamLow()
low := lowStr[len(lowStr)-1]
volStr := data.StreamVol()
volume := volStr[len(volStr)-1]
adjustedPrice, adjustedAmount = ensureOrderFitsWithinHLV(price, amount, high, low, volume)
if !amount.Equal(adjustedAmount) {
f.AppendReasonf("Order size shrunk from %v to %v to fit candle", amount, adjustedAmount)
amount = adjustedAmount
}
if !adjustedPrice.Equal(price) {
f.AppendReasonf("Price adjusted fitting to candle from %v to %v", price, adjustedPrice)
price = adjustedPrice
f.VolumeAdjustedPrice = price
}
}
if amount.LessThanOrEqual(decimal.Zero) && f.GetAmount().GreaterThan(decimal.Zero) {
switch f.GetDirection() {
case gctorder.Buy:
case gctorder.Buy, gctorder.Bid:
f.SetDirection(gctorder.CouldNotBuy)
case gctorder.Sell:
case gctorder.Sell, gctorder.Ask:
f.SetDirection(gctorder.CouldNotSell)
case gctorder.Short:
f.SetDirection(gctorder.CouldNotShort)
case gctorder.Long:
f.SetDirection(gctorder.CouldNotLong)
default:
f.SetDirection(gctorder.DoNothing)
}
f.AppendReason(err.Error())
f.AppendReasonf("amount set to 0, %s", errDataMayBeIncorrect)
return f, err
}
adjustedPrice, err = applySlippageToPrice(f.GetDirection(), price, slippageRate)
if err != nil {
return f, err
}
if !adjustedPrice.Equal(price) {
f.AppendReasonf("Price has slipped from %v to %v", price, adjustedPrice)
price = adjustedPrice
}
f.Slippage = slippageRate.Mul(decimal.NewFromInt(100)).Sub(decimal.NewFromInt(100))
}
portfolioLimitedAmount := reduceAmountToFitPortfolioLimit(adjustedPrice, amount, eventFunds, f.GetDirection())
if !portfolioLimitedAmount.Equal(amount) {
f.AppendReason(fmt.Sprintf("Order size shrunk from %v to %v to remain within portfolio limits", amount, portfolioLimitedAmount))
adjustedAmount = reduceAmountToFitPortfolioLimit(adjustedPrice, amount, allocatedFunds, f.GetDirection())
if !adjustedAmount.Equal(amount) {
f.AppendReasonf("Order size shrunk from %v to %v to remain within portfolio limits", amount, adjustedAmount)
amount = adjustedAmount
}
limitReducedAmount := portfolioLimitedAmount
if cs.CanUseExchangeLimits {
// Conforms the amount to the exchange order defined step amount
// reducing it when needed
limitReducedAmount = cs.Limits.ConformToDecimalAmount(portfolioLimitedAmount)
if !limitReducedAmount.Equal(portfolioLimitedAmount) {
f.AppendReason(fmt.Sprintf("Order size shrunk from %v to %v to remain within exchange step amount limits",
portfolioLimitedAmount,
limitReducedAmount))
adjustedAmount = cs.Limits.ConformToDecimalAmount(amount)
if !adjustedAmount.Equal(amount) {
f.AppendReasonf("Order size shrunk from %v to %v to remain within exchange step amount limits",
adjustedAmount,
amount)
amount = adjustedAmount
}
}
err = verifyOrderWithinLimits(f, limitReducedAmount, &cs)
err = verifyOrderWithinLimits(f, amount, &cs)
if err != nil {
return f, err
}
f.ExchangeFee = calculateExchangeFee(adjustedPrice, limitReducedAmount, cs.ExchangeFee)
orderID, err := e.placeOrder(context.TODO(), adjustedPrice, limitReducedAmount, cs.UseRealOrders, cs.CanUseExchangeLimits, f, orderManager)
fee = calculateExchangeFee(price, amount, cs.TakerFee)
orderID, err := e.placeOrder(context.TODO(), price, amount, fee, cs.UseRealOrders, cs.CanUseExchangeLimits, f, orderManager)
if err != nil {
fundErr := funds.Release(eventFunds, eventFunds, f.GetDirection())
if fundErr != nil {
f.AppendReason(fundErr.Error())
}
if f.GetDirection() == gctorder.Buy {
f.SetDirection(gctorder.CouldNotBuy)
} else if f.GetDirection() == gctorder.Sell {
f.SetDirection(gctorder.CouldNotSell)
}
return f, err
}
switch f.GetDirection() {
case gctorder.Buy:
err = funds.Release(eventFunds, eventFunds.Sub(limitReducedAmount.Mul(adjustedPrice)), f.GetDirection())
if err != nil {
return f, err
}
funds.IncreaseAvailable(limitReducedAmount, f.GetDirection())
case gctorder.Sell:
err = funds.Release(eventFunds, eventFunds.Sub(limitReducedAmount), f.GetDirection())
if err != nil {
return f, err
}
funds.IncreaseAvailable(limitReducedAmount.Mul(adjustedPrice), f.GetDirection())
}
ords := orderManager.GetOrdersSnapshot(gctorder.UnknownStatus)
for i := range ords {
@@ -148,7 +177,17 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
ords[i].CloseTime = o.GetTime()
f.Order = &ords[i]
f.PurchasePrice = decimal.NewFromFloat(ords[i].Price)
f.Total = f.PurchasePrice.Mul(limitReducedAmount).Add(f.ExchangeFee)
f.Amount = decimal.NewFromFloat(ords[i].Amount)
if ords[i].Fee > 0 {
f.ExchangeFee = decimal.NewFromFloat(ords[i].Fee)
}
f.Total = f.PurchasePrice.Mul(f.Amount).Add(f.ExchangeFee)
}
if !o.IsLiquidating() {
err = allocateFundsPostOrder(f, funds, err, o.GetAmount(), allocatedFunds, amount, adjustedPrice, fee)
if err != nil {
return f, err
}
}
if f.Order == nil {
@@ -158,8 +197,106 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
return f, nil
}
func allocateFundsPostOrder(f *fill.Fill, funds funding.IFundReleaser, orderError error, orderAmount, allocatedFunds, limitReducedAmount, adjustedPrice, fee decimal.Decimal) error {
if f == nil {
return fmt.Errorf("%w: fill event", common.ErrNilEvent)
}
if funds == nil {
return fmt.Errorf("%w: funding", common.ErrNilArguments)
}
switch f.AssetType {
case asset.Spot:
pr, err := funds.PairReleaser()
if err != nil {
return err
}
if orderError != nil {
err = pr.Release(allocatedFunds, allocatedFunds, f.GetDirection())
if err != nil {
f.AppendReason(err.Error())
}
switch f.GetDirection() {
case gctorder.Buy, gctorder.Bid:
f.SetDirection(gctorder.CouldNotBuy)
case gctorder.Sell, gctorder.Ask, gctorder.ClosePosition:
f.SetDirection(gctorder.CouldNotSell)
}
return orderError
}
switch f.GetDirection() {
case gctorder.Buy, gctorder.Bid:
err = pr.Release(allocatedFunds, allocatedFunds.Sub(limitReducedAmount.Mul(adjustedPrice).Add(fee)), f.GetDirection())
if err != nil {
return err
}
err = pr.IncreaseAvailable(limitReducedAmount, f.GetDirection())
if err != nil {
return err
}
case gctorder.Sell, gctorder.Ask:
err = pr.Release(allocatedFunds, allocatedFunds.Sub(limitReducedAmount), f.GetDirection())
if err != nil {
return err
}
err = pr.IncreaseAvailable(limitReducedAmount.Mul(adjustedPrice).Sub(fee), f.GetDirection())
if err != nil {
return err
}
default:
return fmt.Errorf("%w asset type %v", common.ErrInvalidDataType, f.GetDirection())
}
f.AppendReason(summarisePosition(f.GetDirection(), f.Amount, f.Amount.Mul(f.PurchasePrice), f.ExchangeFee, f.Order.Pair, currency.EMPTYPAIR))
case asset.Futures:
cr, err := funds.CollateralReleaser()
if err != nil {
return err
}
if orderError != nil {
err = cr.ReleaseContracts(orderAmount)
if err != nil {
return err
}
switch f.GetDirection() {
case gctorder.Short:
f.SetDirection(gctorder.CouldNotShort)
case gctorder.Long:
f.SetDirection(gctorder.CouldNotLong)
default:
return fmt.Errorf("%w asset type %v", common.ErrInvalidDataType, f.GetDirection())
}
return orderError
}
f.AppendReason(summarisePosition(f.GetDirection(), f.Amount, f.Amount.Mul(f.PurchasePrice), f.ExchangeFee, f.Order.Pair, f.UnderlyingPair))
default:
return fmt.Errorf("%w asset type %v", common.ErrInvalidDataType, f.AssetType)
}
return nil
}
func summarisePosition(direction gctorder.Side, orderAmount, orderTotal, orderFee decimal.Decimal, pair, underlying currency.Pair) string {
baseCurr := pair.Base.String()
quoteCurr := pair.Quote
if !underlying.IsEmpty() {
baseCurr = pair.String()
quoteCurr = underlying.Quote
}
return fmt.Sprintf("Placed %s order of %v %v for %v %v, with %v %v in fees, totalling %v %v",
direction,
orderAmount.Round(8),
baseCurr,
orderTotal.Round(8),
quoteCurr,
orderFee.Round(8),
quoteCurr,
orderTotal.Add(orderFee).Round(8),
quoteCurr,
)
}
// verifyOrderWithinLimits conforms the amount to fall into the minimum size and maximum size limit after reduced
func verifyOrderWithinLimits(f *fill.Fill, limitReducedAmount decimal.Decimal, cs *Settings) error {
func verifyOrderWithinLimits(f fill.Event, amount decimal.Decimal, cs *Settings) error {
if f == nil {
return common.ErrNilEvent
}
@@ -170,12 +307,20 @@ func verifyOrderWithinLimits(f *fill.Fill, limitReducedAmount decimal.Decimal, c
var minMax MinMax
var direction gctorder.Side
switch f.GetDirection() {
case gctorder.Buy:
case gctorder.Buy, gctorder.Bid:
minMax = cs.BuySide
direction = gctorder.CouldNotBuy
case gctorder.Sell:
case gctorder.Sell, gctorder.Ask:
minMax = cs.SellSide
direction = gctorder.CouldNotSell
case gctorder.Long:
minMax = cs.BuySide
direction = gctorder.CouldNotLong
case gctorder.Short:
minMax = cs.SellSide
direction = gctorder.CouldNotShort
case gctorder.ClosePosition:
return nil
default:
direction = f.GetDirection()
f.SetDirection(gctorder.DoNothing)
@@ -183,13 +328,13 @@ func verifyOrderWithinLimits(f *fill.Fill, limitReducedAmount decimal.Decimal, c
}
var minOrMax, belowExceed string
var size decimal.Decimal
if limitReducedAmount.LessThan(minMax.MinimumSize) && minMax.MinimumSize.GreaterThan(decimal.Zero) {
if amount.LessThan(minMax.MinimumSize) && minMax.MinimumSize.GreaterThan(decimal.Zero) {
isBeyondLimit = true
belowExceed = "below"
minOrMax = "minimum"
size = minMax.MinimumSize
}
if limitReducedAmount.GreaterThan(minMax.MaximumSize) && minMax.MaximumSize.GreaterThan(decimal.Zero) {
if amount.GreaterThan(minMax.MaximumSize) && minMax.MaximumSize.GreaterThan(decimal.Zero) {
isBeyondLimit = true
belowExceed = "exceeded"
minOrMax = "maximum"
@@ -197,22 +342,22 @@ func verifyOrderWithinLimits(f *fill.Fill, limitReducedAmount decimal.Decimal, c
}
if isBeyondLimit {
f.SetDirection(direction)
e := fmt.Sprintf("Order size %v %s %s size %v", limitReducedAmount, belowExceed, minOrMax, size)
e := fmt.Sprintf("Order size %v %s %s size %v", amount, belowExceed, minOrMax, size)
f.AppendReason(e)
return fmt.Errorf("%w %v", errExceededPortfolioLimit, e)
return errExceededPortfolioLimit
}
return nil
}
func reduceAmountToFitPortfolioLimit(adjustedPrice, amount, sizedPortfolioTotal decimal.Decimal, side gctorder.Side) decimal.Decimal {
switch side {
case gctorder.Buy:
case gctorder.Buy, gctorder.Bid:
if adjustedPrice.Mul(amount).GreaterThan(sizedPortfolioTotal) {
// adjusted amounts exceeds portfolio manager's allowed funds
// the amount has to be reduced to equal the sizedPortfolioTotal
amount = sizedPortfolioTotal.Div(adjustedPrice)
}
case gctorder.Sell:
case gctorder.Sell, gctorder.Ask:
if amount.GreaterThan(sizedPortfolioTotal) {
amount = sizedPortfolioTotal
}
@@ -220,7 +365,7 @@ func reduceAmountToFitPortfolioLimit(adjustedPrice, amount, sizedPortfolioTotal
return amount
}
func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal, useRealOrders, useExchangeLimits bool, f *fill.Fill, orderManager *engine.OrderManager) (string, error) {
func (e *Exchange) placeOrder(ctx context.Context, price, amount, fee decimal.Decimal, useRealOrders, useExchangeLimits bool, f fill.Event, orderManager *engine.OrderManager) (string, error) {
if f == nil {
return "", common.ErrNilEvent
}
@@ -232,9 +377,9 @@ func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal
submit := &gctorder.Submit{
Price: price.InexactFloat64(),
Amount: amount.InexactFloat64(),
Exchange: f.Exchange,
Side: f.Direction,
AssetType: f.AssetType,
Exchange: f.GetExchange(),
Side: f.GetDirection(),
AssetType: f.GetAssetType(),
Pair: f.Pair(),
Type: gctorder.Market,
}
@@ -250,7 +395,7 @@ func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal
}
submitResponse.Status = gctorder.Filled
submitResponse.OrderID = orderID.String()
submitResponse.Fee = f.ExchangeFee.InexactFloat64()
submitResponse.Fee = fee.InexactFloat64()
submitResponse.Cost = submit.Price
submitResponse.LastUpdated = f.GetTime()
submitResponse.Date = f.GetTime()
@@ -262,45 +407,26 @@ func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal
return resp.OrderID, nil
}
func (e *Exchange) sizeOfflineOrder(high, low, volume decimal.Decimal, cs *Settings, f *fill.Fill) (adjustedPrice, adjustedAmount decimal.Decimal, err error) {
if cs == nil || f == nil {
return decimal.Zero, decimal.Zero, common.ErrNilArguments
}
// provide history and estimate volatility
slippageRate := slippage.EstimateSlippagePercentage(cs.MinimumSlippageRate, cs.MaximumSlippageRate)
if cs.SkipCandleVolumeFitting {
f.VolumeAdjustedPrice = f.ClosePrice
adjustedAmount = f.Amount
} else {
f.VolumeAdjustedPrice, adjustedAmount = ensureOrderFitsWithinHLV(f.ClosePrice, f.Amount, high, low, volume)
if !adjustedAmount.Equal(f.Amount) {
f.AppendReason(fmt.Sprintf("Order size shrunk from %v to %v to fit candle", f.Amount, adjustedAmount))
}
}
if adjustedAmount.LessThanOrEqual(decimal.Zero) && f.Amount.GreaterThan(decimal.Zero) {
return decimal.Zero, decimal.Zero, fmt.Errorf("amount set to 0, %w", errDataMayBeIncorrect)
}
adjustedPrice = applySlippageToPrice(f.GetDirection(), f.GetVolumeAdjustedPrice(), slippageRate)
f.Slippage = slippageRate.Mul(decimal.NewFromInt(100)).Sub(decimal.NewFromInt(100))
f.ExchangeFee = calculateExchangeFee(adjustedPrice, adjustedAmount, cs.TakerFee)
return adjustedPrice, adjustedAmount, nil
}
func applySlippageToPrice(direction gctorder.Side, price, slippageRate decimal.Decimal) decimal.Decimal {
adjustedPrice := price
if direction == gctorder.Buy {
func applySlippageToPrice(direction gctorder.Side, price, slippageRate decimal.Decimal) (decimal.Decimal, error) {
var adjustedPrice decimal.Decimal
switch direction {
case gctorder.Buy, gctorder.Bid, gctorder.Long:
adjustedPrice = price.Add(price.Mul(decimal.NewFromInt(1).Sub(slippageRate)))
} else if direction == gctorder.Sell {
case gctorder.Sell, gctorder.Ask, gctorder.Short:
adjustedPrice = price.Mul(slippageRate)
default:
return decimal.Decimal{}, fmt.Errorf("%v %w", direction, gctorder.ErrSideIsInvalid)
}
return adjustedPrice
if adjustedPrice.IsZero() {
adjustedPrice = price
}
return adjustedPrice, nil
}
// SetExchangeAssetCurrencySettings sets the settings for an exchange, asset, currency
func (e *Exchange) SetExchangeAssetCurrencySettings(exch string, a asset.Item, cp currency.Pair, c *Settings) {
if c.Exchange == "" ||
func (e *Exchange) SetExchangeAssetCurrencySettings(a asset.Item, cp currency.Pair, c *Settings) {
if c.Exchange == nil ||
c.Asset == asset.Empty ||
c.Pair.IsEmpty() {
return
@@ -309,7 +435,7 @@ func (e *Exchange) SetExchangeAssetCurrencySettings(exch string, a asset.Item, c
for i := range e.CurrencySettings {
if e.CurrencySettings[i].Pair.Equal(cp) &&
e.CurrencySettings[i].Asset == a &&
exch == e.CurrencySettings[i].Exchange {
strings.EqualFold(c.Exchange.GetName(), e.CurrencySettings[i].Exchange.GetName()) {
e.CurrencySettings[i] = *c
return
}
@@ -322,36 +448,36 @@ func (e *Exchange) GetCurrencySettings(exch string, a asset.Item, cp currency.Pa
for i := range e.CurrencySettings {
if e.CurrencySettings[i].Pair.Equal(cp) {
if e.CurrencySettings[i].Asset == a {
if exch == e.CurrencySettings[i].Exchange {
if strings.EqualFold(exch, e.CurrencySettings[i].Exchange.GetName()) {
return e.CurrencySettings[i], nil
}
}
}
}
return Settings{}, fmt.Errorf("no currency settings found for %v %v %v", exch, a, cp)
return Settings{}, fmt.Errorf("%w for %v %v %v", errNoCurrencySettingsFound, exch, a, cp)
}
func ensureOrderFitsWithinHLV(slippagePrice, amount, high, low, volume decimal.Decimal) (adjustedPrice, adjustedAmount decimal.Decimal) {
adjustedPrice = slippagePrice
func ensureOrderFitsWithinHLV(price, amount, high, low, volume decimal.Decimal) (adjustedPrice, adjustedAmount decimal.Decimal) {
adjustedPrice = price
if adjustedPrice.LessThan(low) {
adjustedPrice = low
}
if adjustedPrice.GreaterThan(high) {
adjustedPrice = high
}
if volume.LessThanOrEqual(decimal.Zero) {
return adjustedPrice, adjustedAmount
orderVolume := amount.Mul(adjustedPrice)
if volume.LessThanOrEqual(decimal.Zero) || orderVolume.LessThanOrEqual(volume) {
return adjustedPrice, amount
}
currentVolume := amount.Mul(adjustedPrice)
if currentVolume.GreaterThan(volume) {
if orderVolume.GreaterThan(volume) {
// reduce the volume to not exceed the total volume of the candle
// it is slightly less than the total to still allow for the illusion
// that open high low close values are valid with the remaining volume
// this is very opinionated
currentVolume = volume.Mul(decimal.NewFromFloat(0.99999999))
orderVolume = volume.Mul(decimal.NewFromFloat(0.99999999))
}
// extract the amount from the adjusted volume
adjustedAmount = currentVolume.Div(adjustedPrice)
adjustedAmount = orderVolume.Div(adjustedPrice)
return adjustedPrice, adjustedAmount
}

View File

@@ -13,18 +13,55 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/ftx"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
const testExchange = "binance"
const testExchange = "ftx"
type fakeFund struct{}
func (f *fakeFund) GetPairReader() (funding.IPairReader, error) {
return nil, nil
}
func (f *fakeFund) GetCollateralReader() (funding.ICollateralReader, error) {
return nil, nil
}
func (f *fakeFund) PairReleaser() (funding.IPairReleaser, error) {
btc, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(9999), decimal.NewFromInt(9999))
if err != nil {
return nil, err
}
usd, err := funding.CreateItem(testExchange, asset.Spot, currency.USD, decimal.NewFromInt(9999), decimal.NewFromInt(9999))
if err != nil {
return nil, err
}
p, err := funding.CreatePair(btc, usd)
if err != nil {
return nil, err
}
err = p.Reserve(decimal.NewFromInt(1337), gctorder.Buy)
if err != nil {
return nil, err
}
err = p.Reserve(decimal.NewFromInt(1337), gctorder.Sell)
if err != nil {
return nil, err
}
return p, nil
}
func (f *fakeFund) CollateralReleaser() (funding.ICollateralReleaser, error) {
return nil, nil
}
func (f *fakeFund) IncreaseAvailable(decimal.Decimal, gctorder.Side) {}
func (f *fakeFund) Release(decimal.Decimal, decimal.Decimal, gctorder.Side) error {
return nil
@@ -44,25 +81,19 @@ func TestReset(t *testing.T) {
func TestSetCurrency(t *testing.T) {
t.Parallel()
e := Exchange{}
e.SetExchangeAssetCurrencySettings("", asset.Empty, currency.EMPTYPAIR, &Settings{})
e.SetExchangeAssetCurrencySettings(asset.Empty, currency.EMPTYPAIR, &Settings{})
if len(e.CurrencySettings) != 0 {
t.Error("expected 0")
}
f := &ftx.FTX{}
f.Name = testExchange
cs := &Settings{
Exchange: testExchange,
UseRealOrders: true,
Pair: currency.NewPair(currency.BTC, currency.USDT),
Asset: asset.Spot,
ExchangeFee: decimal.Zero,
MakerFee: decimal.Zero,
TakerFee: decimal.Zero,
BuySide: MinMax{},
SellSide: MinMax{},
Leverage: Leverage{},
MinimumSlippageRate: decimal.Zero,
MaximumSlippageRate: decimal.Zero,
Exchange: f,
UseRealOrders: true,
Pair: currency.NewPair(currency.BTC, currency.USDT),
Asset: asset.Spot,
}
e.SetExchangeAssetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
e.SetExchangeAssetCurrencySettings(asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
result, err := e.GetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
@@ -70,7 +101,7 @@ func TestSetCurrency(t *testing.T) {
if !result.UseRealOrders {
t.Error("expected true")
}
e.SetExchangeAssetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
e.SetExchangeAssetCurrencySettings(asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
if len(e.CurrencySettings) != 1 {
t.Error("expected 1")
}
@@ -107,35 +138,6 @@ func TestCalculateExchangeFee(t *testing.T) {
}
}
func TestSizeOrder(t *testing.T) {
t.Parallel()
e := Exchange{}
_, _, err := e.sizeOfflineOrder(decimal.Zero, decimal.Zero, decimal.Zero, nil, nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
cs := &Settings{}
f := &fill.Fill{
ClosePrice: decimal.NewFromInt(1337),
Amount: decimal.NewFromInt(1),
}
_, _, err = e.sizeOfflineOrder(decimal.Zero, decimal.Zero, decimal.Zero, cs, f)
if !errors.Is(err, errDataMayBeIncorrect) {
t.Errorf("received: %v, expected: %v", err, errDataMayBeIncorrect)
}
var p, a decimal.Decimal
p, a, err = e.sizeOfflineOrder(decimal.NewFromInt(10), decimal.NewFromInt(2), decimal.NewFromInt(10), cs, f)
if err != nil {
t.Error(err)
}
if !p.Equal(decimal.NewFromInt(10)) {
t.Error("expected 10")
}
if !a.Equal(decimal.NewFromInt(1)) {
t.Error("expected 1")
}
}
func TestPlaceOrder(t *testing.T) {
t.Parallel()
bot := &engine.Engine{}
@@ -156,7 +158,7 @@ func TestPlaceOrder(t *testing.T) {
}
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false)
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false, false)
if err != nil {
t.Error(err)
}
@@ -165,30 +167,32 @@ func TestPlaceOrder(t *testing.T) {
t.Error(err)
}
e := Exchange{}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, nil, nil)
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, nil, nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
f := &fill.Fill{}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
f := &fill.Fill{
Base: &event.Base{},
}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, f, bot.OrderManager)
if !errors.Is(err, engine.ErrExchangeNameIsEmpty) {
t.Errorf("received: %v, expected: %v", err, engine.ErrExchangeNameIsEmpty)
}
f.Exchange = testExchange
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, f, bot.OrderManager)
if !errors.Is(err, gctorder.ErrPairIsEmpty) {
t.Errorf("received: %v, expected: %v", err, gctorder.ErrPairIsEmpty)
}
f.CurrencyPair = currency.NewPair(currency.BTC, currency.USDT)
f.AssetType = asset.Spot
f.Direction = gctorder.Buy
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, f, bot.OrderManager)
if err != nil {
t.Error(err)
}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), true, true, f, bot.OrderManager)
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, true, true, f, bot.OrderManager)
if !errors.Is(err, exchange.ErrAuthenticationSupportNotEnabled) {
t.Errorf("received: %v but expected: %v", err, exchange.ErrAuthenticationSupportNotEnabled)
}
@@ -214,7 +218,7 @@ func TestExecuteOrder(t *testing.T) {
}
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false)
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false, false)
if err != nil {
t.Error(err)
}
@@ -229,25 +233,19 @@ func TestExecuteOrder(t *testing.T) {
if err != nil {
t.Fatal(err)
}
f := &ftx.FTX{}
f.Name = testExchange
cs := Settings{
Exchange: testExchange,
Exchange: f,
UseRealOrders: false,
Pair: p,
Asset: a,
ExchangeFee: decimal.NewFromFloat(0.01),
MakerFee: decimal.NewFromFloat(0.01),
TakerFee: decimal.NewFromFloat(0.01),
BuySide: MinMax{},
SellSide: MinMax{},
Leverage: Leverage{},
MinimumSlippageRate: decimal.Zero,
MaximumSlippageRate: decimal.NewFromInt(1),
}
e := Exchange{
CurrencySettings: []Settings{cs},
}
ev := event.Base{
e := Exchange{}
ev := &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.FifteenMin,
@@ -259,32 +257,33 @@ func TestExecuteOrder(t *testing.T) {
Direction: gctorder.Buy,
Amount: decimal.NewFromInt(10),
AllocatedFunds: decimal.NewFromInt(1337),
ClosePrice: decimal.NewFromInt(1),
}
d := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: "",
Pair: currency.EMPTYPAIR,
Asset: asset.Empty,
Interval: 0,
Candles: []gctkline.Candle{
{
Close: 1,
High: 1,
Low: 1,
Volume: 1,
},
item := gctkline.Item{
Exchange: testExchange,
Pair: p,
Asset: a,
Interval: 0,
Candles: []gctkline.Candle{
{
Close: 1,
High: 1,
Low: 1,
Volume: 1,
},
},
}
d := &kline.DataFromKline{
Item: item,
}
err = d.Load()
if err != nil {
t.Error(err)
}
d.Next()
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if err != nil {
if !errors.Is(err, errNoCurrencySettingsFound) {
t.Error(err)
}
@@ -319,7 +318,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false)
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false, false)
if err != nil {
t.Error(err)
}
@@ -343,32 +342,28 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
if err != nil {
t.Fatal(err)
}
f := &ftx.FTX{}
f.Name = testExchange
cs := Settings{
Exchange: testExchange,
Exchange: f,
UseRealOrders: false,
Pair: p,
Asset: a,
ExchangeFee: decimal.NewFromFloat(0.01),
MakerFee: decimal.NewFromFloat(0.01),
TakerFee: decimal.NewFromFloat(0.01),
BuySide: MinMax{
MaximumSize: decimal.NewFromFloat(0.01),
MinimumSize: decimal.Zero,
},
SellSide: MinMax{
MaximumSize: decimal.NewFromFloat(0.1),
MinimumSize: decimal.Zero,
},
Leverage: Leverage{},
MinimumSlippageRate: decimal.Zero,
MaximumSlippageRate: decimal.NewFromInt(1),
Limits: limits,
}
e := Exchange{
CurrencySettings: []Settings{cs},
}
ev := event.Base{
ev := &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.FifteenMin,
@@ -459,6 +454,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
Direction: gctorder.Sell,
Amount: decimal.NewFromFloat(0.02),
AllocatedFunds: decimal.NewFromFloat(0.01337),
ClosePrice: decimal.NewFromFloat(1337),
}
cs.SellSide.MaximumSize = decimal.Zero
cs.SellSide.MinimumSize = decimal.NewFromFloat(0.01)
@@ -466,6 +462,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
cs.UseRealOrders = true
cs.CanUseExchangeLimits = true
o.Direction = gctorder.Sell
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, exchange.ErrAuthenticationSupportNotEnabled) {
@@ -475,14 +472,34 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
func TestApplySlippageToPrice(t *testing.T) {
t.Parallel()
resp := applySlippageToPrice(gctorder.Buy, decimal.NewFromInt(1), decimal.NewFromFloat(0.9))
resp, err := applySlippageToPrice(gctorder.Buy, decimal.NewFromInt(1), decimal.NewFromFloat(0.9))
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !resp.Equal(decimal.NewFromFloat(1.1)) {
t.Errorf("received: %v, expected: %v", resp, decimal.NewFromFloat(1.1))
}
resp = applySlippageToPrice(gctorder.Sell, decimal.NewFromInt(1), decimal.NewFromFloat(0.9))
resp, err = applySlippageToPrice(gctorder.Sell, decimal.NewFromInt(1), decimal.NewFromFloat(0.9))
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !resp.Equal(decimal.NewFromFloat(0.9)) {
t.Errorf("received: %v, expected: %v", resp, decimal.NewFromFloat(0.9))
}
resp, err = applySlippageToPrice(gctorder.Sell, decimal.NewFromInt(1), decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !resp.Equal(decimal.NewFromFloat(1)) {
t.Errorf("received: %v, expected: %v", resp, decimal.NewFromFloat(1))
}
_, err = applySlippageToPrice(gctorder.UnknownSide, decimal.NewFromInt(1), decimal.NewFromFloat(0.9))
if !errors.Is(err, gctorder.ErrSideIsInvalid) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}
func TestReduceAmountToFitPortfolioLimit(t *testing.T) {
@@ -535,6 +552,7 @@ func TestVerifyOrderWithinLimits(t *testing.T) {
MaximumSize: decimal.NewFromInt(1),
},
}
f.Base = &event.Base{}
err = verifyOrderWithinLimits(f, decimal.NewFromFloat(0.5), s)
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
@@ -560,3 +578,118 @@ func TestVerifyOrderWithinLimits(t *testing.T) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
}
}
func TestAllocateFundsPostOrder(t *testing.T) {
t.Parallel()
expectedError := common.ErrNilEvent
err := allocateFundsPostOrder(nil, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, decimal.Zero, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = common.ErrNilArguments
f := &fill.Fill{
Base: &event.Base{
AssetType: asset.Spot,
},
Direction: gctorder.Buy,
}
err = allocateFundsPostOrder(f, nil, nil, decimal.Zero, decimal.Zero, decimal.Zero, decimal.Zero, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = nil
one := decimal.NewFromInt(1)
item, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
item2, err := funding.CreateItem(testExchange, asset.Spot, currency.USD, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
err = item.Reserve(one)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
err = item2.Reserve(one)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
fundPair, err := funding.CreatePair(item, item2)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
f.Order = &gctorder.Detail{}
err = allocateFundsPostOrder(f, fundPair, nil, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
f.SetDirection(gctorder.Sell)
err = allocateFundsPostOrder(f, fundPair, nil, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = gctorder.ErrSubmissionIsNil
orderError := gctorder.ErrSubmissionIsNil
err = allocateFundsPostOrder(f, fundPair, orderError, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
f.AssetType = asset.Futures
f.SetDirection(gctorder.Short)
expectedError = nil
item3, err := funding.CreateItem(testExchange, asset.Futures, currency.BTC, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
item4, err := funding.CreateItem(testExchange, asset.Futures, currency.USD, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
err = item3.Reserve(one)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
err = item4.Reserve(one)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
collateralPair, err := funding.CreateCollateral(item, item2)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = gctorder.ErrSubmissionIsNil
err = allocateFundsPostOrder(f, collateralPair, orderError, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = nil
err = allocateFundsPostOrder(f, collateralPair, nil, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = gctorder.ErrSubmissionIsNil
f.SetDirection(gctorder.Long)
err = allocateFundsPostOrder(f, collateralPair, orderError, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = nil
err = allocateFundsPostOrder(f, collateralPair, nil, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
f.AssetType = asset.Margin
expectedError = common.ErrInvalidDataType
err = allocateFundsPostOrder(f, collateralPair, nil, one, one, one, one, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
}

View File

@@ -10,22 +10,24 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
var (
errDataMayBeIncorrect = errors.New("data may be incorrect")
errExceededPortfolioLimit = errors.New("exceeded portfolio limit")
errNilCurrencySettings = errors.New("received nil currency settings")
errInvalidDirection = errors.New("received invalid order direction")
errDataMayBeIncorrect = errors.New("data may be incorrect")
errExceededPortfolioLimit = errors.New("exceeded portfolio limit")
errNilCurrencySettings = errors.New("received nil currency settings")
errInvalidDirection = errors.New("received invalid order direction")
errNoCurrencySettingsFound = errors.New("no currency settings found")
)
// ExecutionHandler interface dictates what functions are required to submit an order
type ExecutionHandler interface {
SetExchangeAssetCurrencySettings(string, asset.Item, currency.Pair, *Settings)
SetExchangeAssetCurrencySettings(asset.Item, currency.Pair, *Settings)
GetCurrencySettings(string, asset.Item, currency.Pair) (Settings, error)
ExecuteOrder(order.Event, data.Handler, *engine.OrderManager, funding.IPairReleaser) (*fill.Fill, error)
ExecuteOrder(order.Event, data.Handler, *engine.OrderManager, funding.IFundReleaser) (fill.Event, error)
Reset()
}
@@ -36,15 +38,14 @@ type Exchange struct {
// Settings allow the eventhandler to size an order within the limitations set by the config file
type Settings struct {
Exchange string
Exchange exchange.IBotExchange
UseRealOrders bool
Pair currency.Pair
Asset asset.Item
ExchangeFee decimal.Decimal
MakerFee decimal.Decimal
TakerFee decimal.Decimal
MakerFee decimal.Decimal
TakerFee decimal.Decimal
BuySide MinMax
SellSide MinMax
@@ -57,6 +58,8 @@ type Settings struct {
Limits gctorder.MinMaxLevel
CanUseExchangeLimits bool
SkipCandleVolumeFitting bool
UseExchangePNLCalculation bool
}
// MinMax are the rules which limit the placement of orders.

View File

@@ -31,8 +31,8 @@ func EstimateSlippagePercentage(maximumSlippageRate, minimumSlippageRate decimal
// CalculateSlippageByOrderbook will analyse a provided orderbook and return the result of attempting to
// place the order on there
func CalculateSlippageByOrderbook(ob *orderbook.Base, side gctorder.Side, amountOfFunds, feeRate decimal.Decimal) (price, amount decimal.Decimal) {
result := ob.SimulateOrder(amountOfFunds.InexactFloat64(), side == gctorder.Buy)
func CalculateSlippageByOrderbook(ob *orderbook.Base, side gctorder.Side, allocatedFunds, feeRate decimal.Decimal) (price, amount decimal.Decimal) {
result := ob.SimulateOrder(allocatedFunds.InexactFloat64(), side == gctorder.Buy)
rate := (result.MinimumPrice - result.MaximumPrice) / result.MaximumPrice
price = decimal.NewFromFloat(result.MinimumPrice * (rate + 1))
amount = decimal.NewFromFloat(result.Amount * (1 - feeRate.InexactFloat64()))

View File

@@ -7,24 +7,20 @@ import (
// AddSnapshot creates a snapshot in time of the orders placed to allow for finer detail tracking
// and to protect against anything modifying order details elsewhere
func (m *Manager) AddSnapshot(orders []SnapshotOrder, t time.Time, offset int64, overwriteExisting bool) error {
func (m *Manager) AddSnapshot(snap *Snapshot, overwriteExisting bool) error {
if overwriteExisting {
if len(m.Snapshots) == 0 {
return errSnapshotNotFound
}
for i := len(m.Snapshots) - 1; i >= 0; i-- {
if offset == m.Snapshots[i].Offset {
m.Snapshots[i].Orders = orders
if snap.Offset == m.Snapshots[i].Offset {
m.Snapshots[i].Orders = snap.Orders
return nil
}
}
return fmt.Errorf("%w at %v", errSnapshotNotFound, offset)
return fmt.Errorf("%w at %v", errSnapshotNotFound, snap.Offset)
}
m.Snapshots = append(m.Snapshots, Snapshot{
Orders: orders,
Timestamp: t,
Offset: offset,
})
m.Snapshots = append(m.Snapshots, *snap)
return nil
}

View File

@@ -6,37 +6,57 @@ import (
"time"
"github.com/shopspring/decimal"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
func TestAddSnapshot(t *testing.T) {
t.Parallel()
m := Manager{}
tt := time.Now()
err := m.AddSnapshot([]SnapshotOrder{}, tt, 1, true)
err := m.AddSnapshot(&Snapshot{}, true)
if !errors.Is(err, errSnapshotNotFound) {
t.Errorf("received: %v, expected: %v", err, errSnapshotNotFound)
}
err = m.AddSnapshot([]SnapshotOrder{}, tt, 1, false)
err = m.AddSnapshot(&Snapshot{
Offset: 0,
Timestamp: tt,
Orders: nil,
}, false)
if err != nil {
t.Error(err)
}
err = m.AddSnapshot([]SnapshotOrder{}, tt, 1, true)
if len(m.Snapshots) != 1 {
t.Error("expected 1")
}
err = m.AddSnapshot(&Snapshot{
Offset: 0,
Timestamp: tt,
Orders: nil,
}, true)
if err != nil {
t.Error(err)
}
if len(m.Snapshots) != 1 {
t.Error("expected 1")
}
}
func TestGetSnapshotAtTime(t *testing.T) {
t.Parallel()
m := Manager{}
tt := time.Now()
err := m.AddSnapshot([]SnapshotOrder{
{
ClosePrice: decimal.NewFromInt(1337),
err := m.AddSnapshot(&Snapshot{Offset: 0,
Timestamp: tt,
Orders: []SnapshotOrder{
{
Order: &gctorder.Detail{
Price: 1337,
},
ClosePrice: decimal.NewFromInt(1337),
},
},
}, tt, 1, false)
}, false)
if err != nil {
t.Error(err)
}
@@ -69,21 +89,21 @@ func TestGetLatestSnapshot(t *testing.T) {
t.Error("expected blank snapshot")
}
tt := time.Now()
err := m.AddSnapshot([]SnapshotOrder{
{
ClosePrice: decimal.NewFromInt(1337),
},
}, tt, 1, false)
err := m.AddSnapshot(&Snapshot{
Offset: 0,
Timestamp: tt,
Orders: nil,
}, false)
if err != nil {
t.Error(err)
}
err = m.AddSnapshot([]SnapshotOrder{
{
ClosePrice: decimal.NewFromInt(1337),
},
}, tt.Add(time.Hour), 1, false)
if err != nil {
t.Error(err)
err = m.AddSnapshot(&Snapshot{
Offset: 1,
Timestamp: tt.Add(time.Hour),
Orders: nil,
}, false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
snappySnap = m.GetLatestSnapshot()
if snappySnap.Timestamp.Equal(tt) {

View File

@@ -21,9 +21,9 @@ type Manager struct {
// Snapshot consists of the timestamp the snapshot is from, along with all orders made
// up until that time
type Snapshot struct {
Orders []SnapshotOrder `json:"orders"`
Timestamp time.Time `json:"timestamp"`
Offset int64 `json:"offset"`
Timestamp time.Time `json:"timestamp"`
Orders []SnapshotOrder `json:"orders"`
}
// SnapshotOrder adds some additional data that's only relevant for backtesting
@@ -33,5 +33,5 @@ type SnapshotOrder struct {
VolumeAdjustedPrice decimal.Decimal `json:"volume-adjusted-price"`
SlippageRate decimal.Decimal `json:"slippage-rate"`
CostBasis decimal.Decimal `json:"cost-basis"`
*order.Detail `json:"order-detail"`
Order *order.Detail `json:"order-detail"`
}

View File

@@ -1,41 +1,67 @@
package holdings
import (
"fmt"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// Create makes a Holding struct to track total values of strategy holdings over the course of a backtesting run
func Create(ev ClosePriceReader, funding funding.IPairReader) (Holding, error) {
func Create(ev ClosePriceReader, fundReader funding.IFundReader) (Holding, error) {
if ev == nil {
return Holding{}, common.ErrNilEvent
}
if funding.QuoteInitialFunds().LessThan(decimal.Zero) {
return Holding{}, ErrInitialFundsZero
}
return Holding{
Offset: ev.GetOffset(),
Pair: ev.Pair(),
Asset: ev.GetAssetType(),
Exchange: ev.GetExchange(),
Timestamp: ev.GetTime(),
QuoteInitialFunds: funding.QuoteInitialFunds(),
QuoteSize: funding.QuoteInitialFunds(),
BaseInitialFunds: funding.BaseInitialFunds(),
BaseSize: funding.BaseInitialFunds(),
TotalInitialValue: funding.QuoteInitialFunds().Add(funding.BaseInitialFunds().Mul(ev.GetClosePrice())),
}, nil
if ev.GetAssetType().IsFutures() {
funds, err := fundReader.GetCollateralReader()
if err != nil {
return Holding{}, err
}
return Holding{
Offset: ev.GetOffset(),
Pair: ev.Pair(),
Asset: ev.GetAssetType(),
Exchange: ev.GetExchange(),
Timestamp: ev.GetTime(),
QuoteInitialFunds: funds.InitialFunds(),
QuoteSize: funds.InitialFunds(),
TotalInitialValue: funds.InitialFunds(),
}, nil
} else if ev.GetAssetType() == asset.Spot {
funds, err := fundReader.GetPairReader()
if err != nil {
return Holding{}, err
}
if funds.QuoteInitialFunds().LessThan(decimal.Zero) {
return Holding{}, ErrInitialFundsZero
}
return Holding{
Offset: ev.GetOffset(),
Pair: ev.Pair(),
Asset: ev.GetAssetType(),
Exchange: ev.GetExchange(),
Timestamp: ev.GetTime(),
QuoteInitialFunds: funds.QuoteInitialFunds(),
QuoteSize: funds.QuoteInitialFunds(),
BaseInitialFunds: funds.BaseInitialFunds(),
BaseSize: funds.BaseInitialFunds(),
TotalInitialValue: funds.QuoteInitialFunds().Add(funds.BaseInitialFunds().Mul(ev.GetClosePrice())),
}, nil
}
return Holding{}, fmt.Errorf("%v %w", ev.GetAssetType(), asset.ErrNotSupported)
}
// Update calculates holding statistics for the events time
func (h *Holding) Update(e fill.Event, f funding.IPairReader) {
func (h *Holding) Update(e fill.Event, f funding.IFundReader) error {
h.Timestamp = e.GetTime()
h.Offset = e.GetOffset()
h.update(e, f)
return h.update(e, f)
}
// UpdateValue calculates the holding's value for a data event's time and price
@@ -43,58 +69,75 @@ func (h *Holding) UpdateValue(d common.DataEventHandler) {
h.Timestamp = d.GetTime()
latest := d.GetClosePrice()
h.Offset = d.GetOffset()
h.updateValue(latest)
h.scaleValuesToCurrentPrice(latest)
}
// HasInvestments determines whether there are any holdings in the base funds
func (h *Holding) HasInvestments() bool {
return h.BaseSize.GreaterThan(decimal.Zero)
}
// HasFunds determines whether there are any holdings in the quote funds
func (h *Holding) HasFunds() bool {
return h.QuoteSize.GreaterThan(decimal.Zero)
}
func (h *Holding) update(e fill.Event, f funding.IPairReader) {
func (h *Holding) update(e fill.Event, f funding.IFundReader) error {
direction := e.GetDirection()
if o := e.GetOrder(); o != nil {
amount := decimal.NewFromFloat(o.Amount)
fee := decimal.NewFromFloat(o.Fee)
price := decimal.NewFromFloat(o.Price)
h.BaseSize = f.BaseAvailable()
h.QuoteSize = f.QuoteAvailable()
h.BaseValue = h.BaseSize.Mul(price)
h.TotalFees = h.TotalFees.Add(fee)
switch direction {
case order.Buy:
h.BoughtAmount = h.BoughtAmount.Add(amount)
h.BoughtValue = h.BoughtAmount.Mul(price)
case order.Sell:
h.SoldAmount = h.SoldAmount.Add(amount)
h.SoldValue = h.SoldAmount.Mul(price)
case order.DoNothing, order.CouldNotSell, order.CouldNotBuy, order.MissingData, order.TransferredFunds, order.UnknownSide:
}
o := e.GetOrder()
if o == nil {
h.scaleValuesToCurrentPrice(e.GetClosePrice())
return nil
}
h.TotalValueLostToVolumeSizing = h.TotalValueLostToVolumeSizing.Add(e.GetClosePrice().Sub(e.GetVolumeAdjustedPrice()).Mul(e.GetAmount()))
h.TotalValueLostToSlippage = h.TotalValueLostToSlippage.Add(e.GetVolumeAdjustedPrice().Sub(e.GetPurchasePrice()).Mul(e.GetAmount()))
h.updateValue(e.GetClosePrice())
amount := decimal.NewFromFloat(o.Amount)
fee := decimal.NewFromFloat(o.Fee)
price := decimal.NewFromFloat(o.Price)
a := e.GetAssetType()
switch {
case a == asset.Spot:
spotR, err := f.GetPairReader()
if err != nil {
return err
}
h.BaseSize = spotR.BaseAvailable()
h.QuoteSize = spotR.QuoteAvailable()
case a.IsFutures():
collat, err := f.GetCollateralReader()
if err != nil {
return err
}
h.BaseSize = collat.CurrentHoldings()
h.QuoteSize = collat.AvailableFunds()
default:
return fmt.Errorf("%v %w", a, asset.ErrNotSupported)
}
h.BaseValue = h.BaseSize.Mul(price)
h.TotalFees = h.TotalFees.Add(fee)
if e.GetAssetType().IsFutures() {
// responsibility of tracking futures orders is
// with order.PositionTracker
return nil
}
switch direction {
case order.Buy,
order.Bid:
h.BoughtAmount = h.BoughtAmount.Add(amount)
h.CommittedFunds = h.BaseSize.Mul(price)
case order.Sell,
order.Ask:
h.SoldAmount = h.SoldAmount.Add(amount)
h.CommittedFunds = h.BaseSize.Mul(price)
}
if !e.GetVolumeAdjustedPrice().IsZero() {
h.TotalValueLostToVolumeSizing = h.TotalValueLostToVolumeSizing.Add(e.GetClosePrice().Sub(e.GetVolumeAdjustedPrice()).Mul(e.GetAmount()))
}
if !e.GetClosePrice().Equal(e.GetPurchasePrice()) && !e.GetPurchasePrice().IsZero() {
h.TotalValueLostToSlippage = h.TotalValueLostToSlippage.Add(e.GetClosePrice().Sub(e.GetPurchasePrice()).Mul(e.GetAmount()))
}
h.scaleValuesToCurrentPrice(e.GetClosePrice())
return nil
}
func (h *Holding) updateValue(latestPrice decimal.Decimal) {
func (h *Holding) scaleValuesToCurrentPrice(currentPrice decimal.Decimal) {
origPosValue := h.BaseValue
origBoughtValue := h.BoughtValue
origSoldValue := h.SoldValue
origTotalValue := h.TotalValue
h.BaseValue = h.BaseSize.Mul(latestPrice)
h.BoughtValue = h.BoughtAmount.Mul(latestPrice)
h.SoldValue = h.SoldAmount.Mul(latestPrice)
h.BaseValue = h.BaseSize.Mul(currentPrice)
h.TotalValue = h.BaseValue.Add(h.QuoteSize)
h.TotalValueDifference = h.TotalValue.Sub(origTotalValue)
h.BoughtValueDifference = h.BoughtValue.Sub(origBoughtValue)
h.PositionsValueDifference = h.BaseValue.Sub(origPosValue)
h.SoldValueDifference = h.SoldValue.Sub(origSoldValue)
if !origTotalValue.IsZero() {
h.ChangeInTotalValuePercent = h.TotalValue.Sub(origTotalValue).Div(origTotalValue)

View File

@@ -19,7 +19,7 @@ import (
const testExchange = "binance"
func pair(t *testing.T) *funding.Pair {
func pair(t *testing.T) *funding.SpotPair {
t.Helper()
b, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.Zero, decimal.Zero)
if err != nil {
@@ -36,13 +36,39 @@ func pair(t *testing.T) *funding.Pair {
return p
}
func collateral(t *testing.T) *funding.CollateralPair {
t.Helper()
b, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.Zero, decimal.Zero)
if err != nil {
t.Fatal(err)
}
q, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(1337), decimal.Zero)
if err != nil {
t.Fatal(err)
}
p, err := funding.CreateCollateral(b, q)
if err != nil {
t.Fatal(err)
}
return p
}
func TestCreate(t *testing.T) {
t.Parallel()
_, err := Create(nil, pair(t))
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
_, err = Create(&fill.Fill{}, pair(t))
_, err = Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, pair(t))
if err != nil {
t.Error(err)
}
_, err = Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Futures},
}, collateral(t))
if err != nil {
t.Error(err)
}
@@ -50,17 +76,21 @@ func TestCreate(t *testing.T) {
func TestUpdate(t *testing.T) {
t.Parallel()
h, err := Create(&fill.Fill{}, pair(t))
h, err := Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, pair(t))
if err != nil {
t.Error(err)
}
t1 := h.Timestamp // nolint:ifshort,nolintlint // false positive and triggers only on Windows
h.Update(&fill.Fill{
Base: event.Base{
err = h.Update(&fill.Fill{
Base: &event.Base{
Time: time.Now(),
},
}, pair(t))
if err != nil {
t.Error(err)
}
if t1.Equal(h.Timestamp) {
t.Errorf("expected '%v' received '%v'", h.Timestamp, t1)
}
@@ -68,12 +98,16 @@ func TestUpdate(t *testing.T) {
func TestUpdateValue(t *testing.T) {
t.Parallel()
h, err := Create(&fill.Fill{}, pair(t))
b := &event.Base{AssetType: asset.Spot}
h, err := Create(&fill.Fill{
Base: b,
}, pair(t))
if err != nil {
t.Error(err)
}
h.BaseSize = decimal.NewFromInt(1)
h.UpdateValue(&kline.Kline{
Base: b,
Close: decimal.NewFromInt(1337),
})
if !h.BaseValue.Equal(decimal.NewFromInt(1337)) {
@@ -95,13 +129,15 @@ func TestUpdateBuyStats(t *testing.T) {
if err != nil {
t.Fatal(err)
}
h, err := Create(&fill.Fill{}, p)
h, err := Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, pair(t))
if err != nil {
t.Error(err)
}
h.update(&fill.Fill{
Base: event.Base{
err = h.update(&fill.Fill{
Base: &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.OneHour,
@@ -113,8 +149,6 @@ func TestUpdateBuyStats(t *testing.T) {
ClosePrice: decimal.NewFromInt(500),
VolumeAdjustedPrice: decimal.NewFromInt(500),
PurchasePrice: decimal.NewFromInt(500),
ExchangeFee: decimal.Zero,
Slippage: decimal.Zero,
Order: &order.Detail{
Price: 500,
Amount: 1,
@@ -150,18 +184,15 @@ func TestUpdateBuyStats(t *testing.T) {
if !h.BoughtAmount.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected '%v' received '%v'", 1, h.BoughtAmount)
}
if !h.BoughtValue.Equal(decimal.NewFromInt(500)) {
t.Errorf("expected '%v' received '%v'", 500, h.BoughtValue)
}
if !h.SoldAmount.Equal(decimal.Zero) {
if !h.SoldAmount.IsZero() {
t.Errorf("expected '%v' received '%v'", 0, h.SoldAmount)
}
if !h.TotalFees.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected '%v' received '%v'", 1, h.TotalFees)
}
h.update(&fill.Fill{
Base: event.Base{
err = h.update(&fill.Fill{
Base: &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.OneHour,
@@ -173,8 +204,6 @@ func TestUpdateBuyStats(t *testing.T) {
ClosePrice: decimal.NewFromInt(500),
VolumeAdjustedPrice: decimal.NewFromInt(500),
PurchasePrice: decimal.NewFromInt(500),
ExchangeFee: decimal.Zero,
Slippage: decimal.Zero,
Order: &order.Detail{
Price: 500,
Amount: 0.5,
@@ -199,10 +228,7 @@ func TestUpdateBuyStats(t *testing.T) {
if !h.BoughtAmount.Equal(decimal.NewFromFloat(1.5)) {
t.Errorf("expected '%v' received '%v'", 1, h.BoughtAmount)
}
if !h.BoughtValue.Equal(decimal.NewFromInt(750)) {
t.Errorf("expected '%v' received '%v'", 750, h.BoughtValue)
}
if !h.SoldAmount.Equal(decimal.Zero) {
if !h.SoldAmount.IsZero() {
t.Errorf("expected '%v' received '%v'", 0, h.SoldAmount)
}
if !h.TotalFees.Equal(decimal.NewFromFloat(1.5)) {
@@ -224,12 +250,15 @@ func TestUpdateSellStats(t *testing.T) {
if err != nil {
t.Fatal(err)
}
h, err := Create(&fill.Fill{}, p)
h, err := Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, p)
if err != nil {
t.Error(err)
}
h.update(&fill.Fill{
Base: event.Base{
err = h.update(&fill.Fill{
Base: &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.OneHour,
@@ -241,8 +270,6 @@ func TestUpdateSellStats(t *testing.T) {
ClosePrice: decimal.NewFromInt(500),
VolumeAdjustedPrice: decimal.NewFromInt(500),
PurchasePrice: decimal.NewFromInt(500),
ExchangeFee: decimal.Zero,
Slippage: decimal.Zero,
Order: &order.Detail{
Price: 500,
Amount: 1,
@@ -256,7 +283,6 @@ func TestUpdateSellStats(t *testing.T) {
CloseTime: time.Now(),
LastUpdated: time.Now(),
Pair: currency.NewPair(currency.BTC, currency.USDT),
Trades: nil,
Fee: 1,
},
}, p)
@@ -281,18 +307,15 @@ func TestUpdateSellStats(t *testing.T) {
if !h.BoughtAmount.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected '%v' received '%v'", 1, h.BoughtAmount)
}
if !h.BoughtValue.Equal(decimal.NewFromInt(500)) {
t.Errorf("expected '%v' received '%v'", 500, h.BoughtValue)
}
if !h.SoldAmount.Equal(decimal.Zero) {
if !h.SoldAmount.IsZero() {
t.Errorf("expected '%v' received '%v'", 0, h.SoldAmount)
}
if !h.TotalFees.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected '%v' received '%v'", 1, h.TotalFees)
}
h.update(&fill.Fill{
Base: event.Base{
err = h.update(&fill.Fill{
Base: &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.OneHour,
@@ -304,8 +327,6 @@ func TestUpdateSellStats(t *testing.T) {
ClosePrice: decimal.NewFromInt(500),
VolumeAdjustedPrice: decimal.NewFromInt(500),
PurchasePrice: decimal.NewFromInt(500),
ExchangeFee: decimal.Zero,
Slippage: decimal.Zero,
Order: &order.Detail{
Price: 500,
Amount: 1,
@@ -323,13 +344,13 @@ func TestUpdateSellStats(t *testing.T) {
Fee: 1,
},
}, p)
if err != nil {
t.Error(err)
}
if !h.BoughtAmount.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected '%v' received '%v'", 1, h.BoughtAmount)
}
if !h.BoughtValue.Equal(decimal.NewFromInt(500)) {
t.Errorf("expected '%v' received '%v'", 500, h.BoughtValue)
}
if !h.SoldAmount.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected '%v' received '%v'", 1, h.SoldAmount)
}

View File

@@ -31,12 +31,12 @@ type Holding struct {
SoldAmount decimal.Decimal `json:"sold-amount"`
SoldValue decimal.Decimal `json:"sold-value"`
BoughtAmount decimal.Decimal `json:"bought-amount"`
BoughtValue decimal.Decimal `json:"bought-value"`
CommittedFunds decimal.Decimal `json:"committed-funds"`
IsLiquidated bool
TotalValueDifference decimal.Decimal
ChangeInTotalValuePercent decimal.Decimal
BoughtValueDifference decimal.Decimal
SoldValueDifference decimal.Decimal
PositionsValueDifference decimal.Decimal
TotalValue decimal.Decimal `json:"total-value"`

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package portfolio
import (
"errors"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
@@ -14,9 +15,13 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
const notEnoughFundsTo = "not enough funds to"
var (
errInvalidDirection = errors.New("invalid direction")
errRiskManagerUnset = errors.New("risk manager unset")
@@ -29,6 +34,7 @@ var (
errNoHoldings = errors.New("no holdings found")
errHoldingsNoTimestamp = errors.New("holding with unset timestamp received")
errHoldingsAlreadySet = errors.New("holding already set")
errUnsetFuturesTracker = errors.New("portfolio settings futures tracker unset")
)
// Portfolio stores all holdings and rules to assess orders, allowing the portfolio manager to
@@ -42,36 +48,66 @@ type Portfolio struct {
// Handler contains all functions expected to operate a portfolio manager
type Handler interface {
OnSignal(signal.Event, *exchange.Settings, funding.IPairReserver) (*order.Order, error)
OnFill(fill.Event, funding.IPairReader) (*fill.Fill, error)
OnSignal(signal.Event, *exchange.Settings, funding.IFundReserver) (*order.Order, error)
OnFill(fill.Event, funding.IFundReleaser) (fill.Event, error)
GetLatestOrderSnapshotForEvent(common.EventHandler) (compliance.Snapshot, error)
GetLatestOrderSnapshots() ([]compliance.Snapshot, error)
ViewHoldingAtTimePeriod(common.EventHandler) (*holdings.Holding, error)
setHoldingsForOffset(*holdings.Holding, bool) error
UpdateHoldings(common.DataEventHandler, funding.IPairReader) error
UpdateHoldings(common.DataEventHandler, funding.IFundReleaser) error
GetComplianceManager(string, asset.Item, currency.Pair) (*compliance.Manager, error)
SetFee(string, asset.Item, currency.Pair, decimal.Decimal)
GetFee(string, asset.Item, currency.Pair) decimal.Decimal
GetPositions(common.EventHandler) ([]gctorder.PositionStats, error)
TrackFuturesOrder(fill.Event, funding.IFundReleaser) (*PNLSummary, error)
UpdatePNL(common.EventHandler, decimal.Decimal) error
GetLatestPNLForEvent(common.EventHandler) (*PNLSummary, error)
GetLatestPNLs() []PNLSummary
CheckLiquidationStatus(common.DataEventHandler, funding.ICollateralReader, *PNLSummary) error
CreateLiquidationOrdersForExchange(common.DataEventHandler, funding.IFundingManager) ([]order.Event, error)
Reset()
}
// SizeHandler is the interface to help size orders
type SizeHandler interface {
SizeOrder(order.Event, decimal.Decimal, *exchange.Settings) (*order.Order, error)
SizeOrder(order.Event, decimal.Decimal, *exchange.Settings) (*order.Order, decimal.Decimal, error)
}
// Settings holds all important information for the portfolio manager
// to assess purchasing decisions
type Settings struct {
Fee decimal.Decimal
BuySideSizing exchange.MinMax
SellSideSizing exchange.MinMax
Leverage exchange.Leverage
HoldingsSnapshots []holdings.Holding
ComplianceManager compliance.Manager
Exchange gctexchange.IBotExchange
FuturesTracker *gctorder.MultiPositionTracker
}
// PNLSummary holds a PNL result along with
// exchange details
type PNLSummary struct {
Exchange string
Item asset.Item
Pair currency.Pair
CollateralCurrency currency.Code
Offset int64
Result gctorder.PNLResult
}
// IPNL defines an interface for an implementation
// to retrieve PNL from a position
type IPNL interface {
GetUnrealisedPNL() BasicPNLResult
GetRealisedPNL() BasicPNLResult
GetCollateralCurrency() currency.Code
GetDirection() gctorder.Side
GetPositionStatus() gctorder.Status
}
// BasicPNLResult holds the time and the pnl
// of a position
type BasicPNLResult struct {
Currency currency.Code
Time time.Time
PNL decimal.Decimal
}

View File

@@ -37,7 +37,8 @@ func (r *Risk) EvaluateOrder(o order.Event, latestHoldings []holdings.Holding, s
if ratio.GreaterThan(lookup.MaximumOrdersWithLeverageRatio) && lookup.MaximumOrdersWithLeverageRatio.GreaterThan(decimal.Zero) {
return nil, fmt.Errorf("proceeding with the order would put maximum orders using leverage ratio beyond its limit of %v to %v and %w", lookup.MaximumOrdersWithLeverageRatio, ratio, errCannotPlaceLeverageOrder)
}
if retOrder.GetLeverage().GreaterThan(lookup.MaxLeverageRate) && lookup.MaxLeverageRate.GreaterThan(decimal.Zero) {
lr := lookup.MaxLeverageRate
if retOrder.GetLeverage().GreaterThan(lr) && lr.GreaterThan(decimal.Zero) {
return nil, fmt.Errorf("proceeding with the order would put leverage rate beyond its limit of %v to %v and %w", lookup.MaxLeverageRate, retOrder.GetLeverage(), errCannotPlaceLeverageOrder)
}
}
@@ -59,7 +60,7 @@ func existingLeverageRatio(s compliance.Snapshot) decimal.Decimal {
}
var ordersWithLeverage decimal.Decimal
for o := range s.Orders {
if s.Orders[o].Leverage != 0 {
if s.Orders[o].Order.Leverage != 0 {
ordersWithLeverage = ordersWithLeverage.Add(decimal.NewFromInt(1))
}
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
@@ -56,15 +57,17 @@ func TestEvaluateOrder(t *testing.T) {
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
o := &order.Order{}
h := []holdings.Holding{}
p := currency.NewPair(currency.BTC, currency.USDT)
e := "binance"
a := asset.Spot
o.Exchange = e
o.AssetType = a
o.CurrencyPair = p
o := &order.Order{
Base: &event.Base{
Exchange: e,
AssetType: a,
CurrencyPair: p,
},
}
h := []holdings.Holding{}
r.CurrencySettings = make(map[string]map[asset.Item]map[currency.Pair]*CurrencySettings)
r.CurrencySettings[e] = make(map[asset.Item]map[currency.Pair]*CurrencySettings)
r.CurrencySettings[e][a] = make(map[currency.Pair]*CurrencySettings)
@@ -89,8 +92,7 @@ func TestEvaluateOrder(t *testing.T) {
}
h = append(h, holdings.Holding{
Pair: currency.NewPair(currency.DOGE, currency.USDT),
BaseSize: decimal.Zero,
Pair: currency.NewPair(currency.DOGE, currency.USDT),
})
o.Leverage = decimal.NewFromFloat(1.1)
r.CurrencySettings[e][a][p].MaximumHoldingRatio = decimal.Zero
@@ -117,7 +119,7 @@ func TestEvaluateOrder(t *testing.T) {
_, err = r.EvaluateOrder(o, h, compliance.Snapshot{
Orders: []compliance.SnapshotOrder{
{
Detail: &gctorder.Detail{
Order: &gctorder.Detail{
Leverage: 3,
},
},

View File

@@ -0,0 +1,99 @@
package portfolio
import (
"strings"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// Setup creates a portfolio manager instance and sets private fields
func Setup(sh SizeHandler, r risk.Handler, riskFreeRate decimal.Decimal) (*Portfolio, error) {
if sh == nil {
return nil, errSizeManagerUnset
}
if riskFreeRate.IsNegative() {
return nil, errNegativeRiskFreeRate
}
if r == nil {
return nil, errRiskManagerUnset
}
p := &Portfolio{}
p.sizeManager = sh
p.riskManager = r
p.riskFreeRate = riskFreeRate
return p, nil
}
// Reset returns the portfolio manager to its default state
func (p *Portfolio) Reset() {
p.exchangeAssetPairSettings = nil
}
// SetupCurrencySettingsMap ensures a map is created and no panics happen
func (p *Portfolio) SetupCurrencySettingsMap(setup *exchange.Settings) error {
if setup == nil {
return errNoPortfolioSettings
}
if setup.Exchange == nil {
return errExchangeUnset
}
if setup.Asset == asset.Empty {
return errAssetUnset
}
if setup.Pair.IsEmpty() {
return errCurrencyPairUnset
}
if p.exchangeAssetPairSettings == nil {
p.exchangeAssetPairSettings = make(map[string]map[asset.Item]map[currency.Pair]*Settings)
}
name := strings.ToLower(setup.Exchange.GetName())
if p.exchangeAssetPairSettings[name] == nil {
p.exchangeAssetPairSettings[name] = make(map[asset.Item]map[currency.Pair]*Settings)
}
if p.exchangeAssetPairSettings[name][setup.Asset] == nil {
p.exchangeAssetPairSettings[name][setup.Asset] = make(map[currency.Pair]*Settings)
}
if _, ok := p.exchangeAssetPairSettings[name][setup.Asset][setup.Pair]; ok {
return nil
}
collateralCurrency, _, err := setup.Exchange.GetCollateralCurrencyForContract(setup.Asset, setup.Pair)
if err != nil {
return err
}
settings := &Settings{
BuySideSizing: setup.BuySide,
SellSideSizing: setup.SellSide,
Leverage: setup.Leverage,
Exchange: setup.Exchange,
ComplianceManager: compliance.Manager{},
}
if setup.Asset.IsFutures() {
futureTrackerSetup := &gctorder.MultiPositionTrackerSetup{
Exchange: name,
Asset: setup.Asset,
Pair: setup.Pair,
Underlying: setup.Pair.Base,
OfflineCalculation: true,
UseExchangePNLCalculation: setup.UseExchangePNLCalculation,
CollateralCurrency: collateralCurrency,
}
if setup.UseExchangePNLCalculation {
futureTrackerSetup.ExchangePNLCalculation = setup.Exchange
}
var tracker *gctorder.MultiPositionTracker
tracker, err = gctorder.SetupMultiPositionTracker(futureTrackerSetup)
if err != nil {
return err
}
settings.FuturesTracker = tracker
}
p.exchangeAssetPairSettings[name][setup.Asset][setup.Pair] = settings
return nil
}

View File

@@ -1,6 +1,7 @@
package size
import (
"context"
"fmt"
"github.com/shopspring/decimal"
@@ -11,74 +12,125 @@ import (
)
// SizeOrder is responsible for ensuring that the order size is within config limits
func (s *Size) SizeOrder(o order.Event, amountAvailable decimal.Decimal, cs *exchange.Settings) (*order.Order, error) {
func (s *Size) SizeOrder(o order.Event, amountAvailable decimal.Decimal, cs *exchange.Settings) (*order.Order, decimal.Decimal, error) {
if o == nil || cs == nil {
return nil, common.ErrNilArguments
return nil, decimal.Decimal{}, common.ErrNilArguments
}
if amountAvailable.LessThanOrEqual(decimal.Zero) {
return nil, errNoFunds
return nil, decimal.Decimal{}, errNoFunds
}
retOrder, ok := o.(*order.Order)
if !ok {
return nil, fmt.Errorf("%w expected order event", common.ErrInvalidDataType)
return nil, decimal.Decimal{}, fmt.Errorf("%w expected order event", common.ErrInvalidDataType)
}
var amount decimal.Decimal
var err error
switch retOrder.GetDirection() {
case gctorder.Buy:
// check size against currency specific settings
amount, err = s.calculateBuySize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetBuyLimit(), cs.BuySide)
if err != nil {
return nil, err
}
// check size against portfolio specific settings
var portfolioSize decimal.Decimal
portfolioSize, err = s.calculateBuySize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetBuyLimit(), s.BuySide)
if err != nil {
return nil, err
}
// global settings overrule individual currency settings
if amount.GreaterThan(portfolioSize) {
amount = portfolioSize
}
case gctorder.Sell:
// check size against currency specific settings
amount, err = s.calculateSellSize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetSellLimit(), cs.SellSide)
if fde := o.GetFillDependentEvent(); fde != nil && fde.MatchOrderAmount() {
scalingInfo, err := cs.Exchange.ScaleCollateral(context.TODO(), &gctorder.CollateralCalculator{
CalculateOffline: true,
CollateralCurrency: o.Pair().Base,
Asset: fde.GetAssetType(),
Side: gctorder.Short,
USDPrice: fde.GetClosePrice(),
IsForNewPosition: true,
FreeCollateral: amountAvailable,
})
if err != nil {
return nil, err
return nil, decimal.Decimal{}, err
}
// check size against portfolio specific settings
portfolioSize, err := s.calculateSellSize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetSellLimit(), s.SellSide)
initialAmount := amountAvailable.Mul(scalingInfo.Weighting).Div(fde.GetClosePrice())
oNotionalPosition := initialAmount.Mul(o.GetClosePrice())
sizedAmount, estFee, err := s.calculateAmount(o.GetDirection(), o.GetClosePrice(), oNotionalPosition, cs, o)
if err != nil {
return nil, err
return nil, decimal.Decimal{}, err
}
// global settings overrule individual currency settings
if amount.GreaterThan(portfolioSize) {
amount = portfolioSize
scaledCollateralFromAmount := sizedAmount.Mul(scalingInfo.Weighting)
excess := amountAvailable.Sub(sizedAmount).Add(scaledCollateralFromAmount)
if excess.IsNegative() {
return nil, decimal.Decimal{}, fmt.Errorf("%w not enough funding for position", errCannotAllocate)
}
retOrder.SetAmount(sizedAmount)
fde.SetAmount(sizedAmount)
return retOrder, estFee, nil
}
amount = amount.Round(8)
if amount.LessThanOrEqual(decimal.Zero) {
return retOrder, fmt.Errorf("%w at %v for %v %v %v", errCannotAllocate, o.GetTime(), o.GetExchange(), o.GetAssetType(), o.Pair())
amount, estFee, err := s.calculateAmount(retOrder.Direction, retOrder.ClosePrice, amountAvailable, cs, o)
if err != nil {
return nil, decimal.Decimal{}, err
}
retOrder.SetAmount(amount)
return retOrder, nil
return retOrder, estFee, nil
}
func (s *Size) calculateAmount(direction gctorder.Side, price, amountAvailable decimal.Decimal, cs *exchange.Settings, o order.Event) (amount, fee decimal.Decimal, err error) {
var portfolioAmount, portfolioFee decimal.Decimal
switch direction {
case gctorder.ClosePosition:
amount = amountAvailable
fee = amount.Mul(price).Mul(cs.TakerFee)
case gctorder.Buy, gctorder.Long:
// check size against currency specific settings
amount, fee, err = s.calculateBuySize(price, amountAvailable, cs.TakerFee, o.GetBuyLimit(), cs.BuySide)
if err != nil {
return decimal.Decimal{}, decimal.Decimal{}, err
}
// check size against portfolio specific settings
portfolioAmount, portfolioFee, err = s.calculateBuySize(price, amountAvailable, cs.TakerFee, o.GetBuyLimit(), s.BuySide)
if err != nil {
return decimal.Decimal{}, decimal.Decimal{}, err
}
// global settings overrule individual currency settings
if amount.GreaterThan(portfolioAmount) {
amount = portfolioAmount
fee = portfolioFee
}
case gctorder.Sell, gctorder.Short:
// check size against currency specific settings
amount, fee, err = s.calculateSellSize(price, amountAvailable, cs.TakerFee, o.GetSellLimit(), cs.SellSide)
if err != nil {
return decimal.Decimal{}, decimal.Decimal{}, err
}
// check size against portfolio specific settings
portfolioAmount, portfolioFee, err = s.calculateSellSize(price, amountAvailable, cs.TakerFee, o.GetSellLimit(), s.SellSide)
if err != nil {
return decimal.Decimal{}, decimal.Decimal{}, err
}
// global settings overrule individual currency settings
if amount.GreaterThan(portfolioAmount) {
amount = portfolioAmount
fee = portfolioFee
}
default:
return decimal.Decimal{}, decimal.Decimal{}, fmt.Errorf("%w at %v for %v %v %v", errCannotAllocate, o.GetTime(), o.GetExchange(), o.GetAssetType(), o.Pair())
}
if amount.LessThanOrEqual(decimal.Zero) {
return decimal.Decimal{}, decimal.Decimal{}, fmt.Errorf("%w at %v for %v %v %v, no amount sized", errCannotAllocate, o.GetTime(), o.GetExchange(), o.GetAssetType(), o.Pair())
}
if o.GetAmount().IsPositive() && o.GetAmount().LessThanOrEqual(amount) {
// when an order amount is already set
// use the pre-set amount and calculate the fee
amount = o.GetAmount()
fee = o.GetAmount().Mul(price).Mul(cs.TakerFee)
}
return amount, fee, nil
}
// calculateBuySize respects config rules and calculates the amount of money
// that is allowed to be spent/sold for an event.
// As fee calculation occurs during the actual ordering process
// this can only attempt to factor the potential fee to remain under the max rules
func (s *Size) calculateBuySize(price, availableFunds, feeRate, buyLimit decimal.Decimal, minMaxSettings exchange.MinMax) (decimal.Decimal, error) {
func (s *Size) calculateBuySize(price, availableFunds, feeRate, buyLimit decimal.Decimal, minMaxSettings exchange.MinMax) (amount, fee decimal.Decimal, err error) {
if availableFunds.LessThanOrEqual(decimal.Zero) {
return decimal.Zero, errNoFunds
return decimal.Decimal{}, decimal.Decimal{}, errNoFunds
}
if price.IsZero() {
return decimal.Zero, nil
return decimal.Decimal{}, decimal.Decimal{}, nil
}
amount := availableFunds.Mul(decimal.NewFromInt(1).Sub(feeRate)).Div(price)
amount = availableFunds.Mul(decimal.NewFromInt(1).Sub(feeRate)).Div(price)
if !buyLimit.IsZero() &&
buyLimit.GreaterThanOrEqual(minMaxSettings.MinimumSize) &&
(buyLimit.LessThanOrEqual(minMaxSettings.MaximumSize) || minMaxSettings.MaximumSize.IsZero()) &&
@@ -92,9 +144,10 @@ func (s *Size) calculateBuySize(price, availableFunds, feeRate, buyLimit decimal
amount = minMaxSettings.MaximumTotal.Mul(decimal.NewFromInt(1).Sub(feeRate)).Div(price)
}
if amount.LessThan(minMaxSettings.MinimumSize) && minMaxSettings.MinimumSize.GreaterThan(decimal.Zero) {
return decimal.Zero, fmt.Errorf("%w. Sized: '%v' Minimum: '%v'", errLessThanMinimum, amount, minMaxSettings.MinimumSize)
return decimal.Decimal{}, decimal.Decimal{}, fmt.Errorf("%w. Sized: '%v' Minimum: '%v'", errLessThanMinimum, amount, minMaxSettings.MinimumSize)
}
return amount, nil
fee = amount.Mul(price).Mul(feeRate)
return amount, fee, nil
}
// calculateSellSize respects config rules and calculates the amount of money
@@ -103,15 +156,15 @@ func (s *Size) calculateBuySize(price, availableFunds, feeRate, buyLimit decimal
// eg BTC-USD baseAmount will be BTC to be sold
// As fee calculation occurs during the actual ordering process
// this can only attempt to factor the potential fee to remain under the max rules
func (s *Size) calculateSellSize(price, baseAmount, feeRate, sellLimit decimal.Decimal, minMaxSettings exchange.MinMax) (decimal.Decimal, error) {
func (s *Size) calculateSellSize(price, baseAmount, feeRate, sellLimit decimal.Decimal, minMaxSettings exchange.MinMax) (amount, fee decimal.Decimal, err error) {
if baseAmount.LessThanOrEqual(decimal.Zero) {
return decimal.Zero, errNoFunds
return decimal.Decimal{}, decimal.Decimal{}, errNoFunds
}
if price.IsZero() {
return decimal.Zero, nil
return decimal.Decimal{}, decimal.Decimal{}, nil
}
oneMFeeRate := decimal.NewFromInt(1).Sub(feeRate)
amount := baseAmount.Mul(oneMFeeRate)
amount = baseAmount.Mul(oneMFeeRate)
if !sellLimit.IsZero() &&
sellLimit.GreaterThanOrEqual(minMaxSettings.MinimumSize) &&
(sellLimit.LessThanOrEqual(minMaxSettings.MaximumSize) || minMaxSettings.MaximumSize.IsZero()) &&
@@ -125,8 +178,8 @@ func (s *Size) calculateSellSize(price, baseAmount, feeRate, sellLimit decimal.D
amount = minMaxSettings.MaximumTotal.Mul(oneMFeeRate).Div(price)
}
if amount.LessThan(minMaxSettings.MinimumSize) && minMaxSettings.MinimumSize.GreaterThan(decimal.Zero) {
return decimal.Zero, fmt.Errorf("%w. Sized: '%v' Minimum: '%v'", errLessThanMinimum, amount, minMaxSettings.MinimumSize)
return decimal.Decimal{}, decimal.Decimal{}, fmt.Errorf("%w. Sized: '%v' Minimum: '%v'", errLessThanMinimum, amount, minMaxSettings.MinimumSize)
}
return amount, nil
fee = amount.Mul(price).Mul(feeRate)
return amount, fee, nil
}

View File

@@ -1,20 +1,26 @@
package size
import (
"context"
"errors"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/ftx"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
func TestSizingAccuracy(t *testing.T) {
t.Parallel()
globalMinMax := exchange.MinMax{
MinimumSize: decimal.Zero,
MaximumSize: decimal.NewFromInt(1),
MaximumTotal: decimal.NewFromInt(10),
}
@@ -26,7 +32,7 @@ func TestSizingAccuracy(t *testing.T) {
availableFunds := decimal.NewFromInt(11)
feeRate := decimal.NewFromFloat(0.02)
buyLimit := decimal.NewFromInt(1)
amountWithoutFee, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
amountWithoutFee, _, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
if err != nil {
t.Error(err)
}
@@ -39,7 +45,6 @@ func TestSizingAccuracy(t *testing.T) {
func TestSizingOverMaxSize(t *testing.T) {
t.Parallel()
globalMinMax := exchange.MinMax{
MinimumSize: decimal.Zero,
MaximumSize: decimal.NewFromFloat(0.5),
MaximumTotal: decimal.NewFromInt(1337),
}
@@ -51,7 +56,7 @@ func TestSizingOverMaxSize(t *testing.T) {
availableFunds := decimal.NewFromInt(1338)
feeRate := decimal.NewFromFloat(0.02)
buyLimit := decimal.NewFromInt(1)
amount, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
amount, _, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
if err != nil {
t.Error(err)
}
@@ -75,7 +80,7 @@ func TestSizingUnderMinSize(t *testing.T) {
availableFunds := decimal.NewFromInt(1338)
feeRate := decimal.NewFromFloat(0.02)
buyLimit := decimal.NewFromInt(1)
_, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
_, _, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
if !errors.Is(err, errLessThanMinimum) {
t.Errorf("received: %v, expected: %v", err, errLessThanMinimum)
}
@@ -85,7 +90,6 @@ func TestMaximumBuySizeEqualZero(t *testing.T) {
t.Parallel()
globalMinMax := exchange.MinMax{
MinimumSize: decimal.NewFromInt(1),
MaximumSize: decimal.Zero,
MaximumTotal: decimal.NewFromInt(1437),
}
sizer := Size{
@@ -96,7 +100,7 @@ func TestMaximumBuySizeEqualZero(t *testing.T) {
availableFunds := decimal.NewFromInt(13380)
feeRate := decimal.NewFromFloat(0.02)
buyLimit := decimal.NewFromInt(1)
amount, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
amount, _, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
if amount != buyLimit || err != nil {
t.Errorf("expected: %v, received %v, err: %+v", buyLimit, amount, err)
}
@@ -105,7 +109,6 @@ func TestMaximumSellSizeEqualZero(t *testing.T) {
t.Parallel()
globalMinMax := exchange.MinMax{
MinimumSize: decimal.NewFromInt(1),
MaximumSize: decimal.Zero,
MaximumTotal: decimal.NewFromInt(1437),
}
sizer := Size{
@@ -116,7 +119,7 @@ func TestMaximumSellSizeEqualZero(t *testing.T) {
availableFunds := decimal.NewFromInt(13380)
feeRate := decimal.NewFromFloat(0.02)
sellLimit := decimal.NewFromInt(1)
amount, err := sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
amount, _, err := sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
if amount != sellLimit || err != nil {
t.Errorf("expected: %v, received %v, err: %+v", sellLimit, amount, err)
}
@@ -137,7 +140,7 @@ func TestSizingErrors(t *testing.T) {
availableFunds := decimal.Zero
feeRate := decimal.NewFromFloat(0.02)
buyLimit := decimal.NewFromInt(1)
_, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
_, _, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
if !errors.Is(err, errNoFunds) {
t.Errorf("received: %v, expected: %v", err, errNoFunds)
}
@@ -158,61 +161,111 @@ func TestCalculateSellSize(t *testing.T) {
availableFunds := decimal.Zero
feeRate := decimal.NewFromFloat(0.02)
sellLimit := decimal.NewFromInt(1)
_, err := sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
_, _, err := sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
if !errors.Is(err, errNoFunds) {
t.Errorf("received: %v, expected: %v", err, errNoFunds)
}
availableFunds = decimal.NewFromInt(1337)
_, err = sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
_, _, err = sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
if !errors.Is(err, errLessThanMinimum) {
t.Errorf("received: %v, expected: %v", err, errLessThanMinimum)
}
price = decimal.NewFromInt(12)
availableFunds = decimal.NewFromInt(1339)
_, err = sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
amount, fee, err := sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
if err != nil {
t.Error(err)
}
if !amount.Equal(sellLimit) {
t.Errorf("received '%v' expected '%v'", amount, sellLimit)
}
if !amount.Mul(price).Mul(feeRate).Equal(fee) {
t.Errorf("received '%v' expected '%v'", amount.Mul(price).Mul(feeRate), fee)
}
}
func TestSizeOrder(t *testing.T) {
t.Parallel()
s := Size{}
_, err := s.SizeOrder(nil, decimal.Zero, nil)
_, _, err := s.SizeOrder(nil, decimal.Zero, nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
o := &order.Order{}
o := &order.Order{
Base: &event.Base{
Offset: 1,
Exchange: "ftx",
Time: time.Now(),
CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
UnderlyingPair: currency.NewPair(currency.BTC, currency.USD),
AssetType: asset.Spot,
},
}
cs := &exchange.Settings{}
_, err = s.SizeOrder(o, decimal.Zero, cs)
_, _, err = s.SizeOrder(o, decimal.Zero, cs)
if !errors.Is(err, errNoFunds) {
t.Errorf("received: %v, expected: %v", err, errNoFunds)
}
_, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if !errors.Is(err, errCannotAllocate) {
t.Errorf("received: %v, expected: %v", err, errCannotAllocate)
}
o.Direction = gctorder.Buy
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if !errors.Is(err, errCannotAllocate) {
t.Errorf("received: %v, expected: %v", err, errCannotAllocate)
}
o.Direction = gctorder.Buy
o.Price = decimal.NewFromInt(1)
o.ClosePrice = decimal.NewFromInt(1)
s.BuySide.MaximumSize = decimal.NewFromInt(1)
s.BuySide.MinimumSize = decimal.NewFromInt(1)
_, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
}
o.Amount = decimal.NewFromInt(1)
o.Direction = gctorder.Sell
_, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
}
s.SellSide.MaximumSize = decimal.NewFromInt(1)
s.SellSide.MinimumSize = decimal.NewFromInt(1)
_, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
}
o.Direction = gctorder.ClosePosition
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
}
// spot futures sizing
o.FillDependentEvent = &signal.Signal{
Base: o.Base,
MatchesOrderAmount: true,
ClosePrice: decimal.NewFromInt(1337),
}
exch := ftx.FTX{}
err = exch.LoadCollateralWeightings(context.Background())
if err != nil {
t.Error(err)
}
cs.Exchange = &exch
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
}
o.ClosePrice = decimal.NewFromInt(1000000000)
o.Amount = decimal.NewFromInt(1000000000)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if !errors.Is(err, errCannotAllocate) {
t.Errorf("received: %v, expected: %v", err, errCannotAllocate)
}
}

View File

@@ -0,0 +1,304 @@
package statistics
import (
"errors"
"fmt"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/log"
)
// fSIL shorthand wrapper for FitStringToLimit
func fSIL(str string, limit int) string {
spacer := " "
return common.FitStringToLimit(str, spacer, limit, true)
}
// CalculateBiggestEventDrawdown calculates the biggest drawdown using a slice of DataEvents
func CalculateBiggestEventDrawdown(closePrices []common.DataEventHandler) (Swing, error) {
if len(closePrices) == 0 {
return Swing{}, fmt.Errorf("%w to calculate drawdowns", errReceivedNoData)
}
var swings []Swing
lowestPrice := closePrices[0].GetLowPrice()
highestPrice := closePrices[0].GetHighPrice()
lowestTime := closePrices[0].GetTime()
highestTime := closePrices[0].GetTime()
interval := closePrices[0].GetInterval()
for i := range closePrices {
currHigh := closePrices[i].GetHighPrice()
currLow := closePrices[i].GetLowPrice()
currTime := closePrices[i].GetTime()
if lowestPrice.GreaterThan(currLow) && !currLow.IsZero() {
lowestPrice = currLow
lowestTime = currTime
}
if highestPrice.LessThan(currHigh) {
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[i].GetInterval(), 0)
if err != nil {
return Swing{}, fmt.Errorf("cannot calculate max drawdown, date range error: %w", err)
}
if highestPrice.IsPositive() && lowestPrice.IsPositive() {
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100)),
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
}
// reset the drawdown
highestPrice = currHigh
highestTime = currTime
lowestPrice = currLow
lowestTime = currTime
}
}
if (len(swings) > 0 && swings[len(swings)-1].Lowest.Value != closePrices[len(closePrices)-1].GetLowPrice()) || swings == nil {
// need to close out the final drawdown
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[0].GetInterval(), 0)
if err != nil {
return Swing{}, fmt.Errorf("cannot close out max drawdown calculation: %w", err)
}
drawdownPercent := decimal.Zero
if highestPrice.GreaterThan(decimal.Zero) {
drawdownPercent = lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100))
}
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: drawdownPercent,
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
}
var maxDrawdown Swing
if len(swings) > 0 {
maxDrawdown = swings[0]
}
for i := range swings {
if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
maxDrawdown = swings[i]
}
}
return maxDrawdown, nil
}
// CalculateBiggestValueAtTimeDrawdown calculates the biggest drawdown using a slice of ValueAtTimes
func CalculateBiggestValueAtTimeDrawdown(closePrices []ValueAtTime, interval gctkline.Interval) (Swing, error) {
if len(closePrices) == 0 {
return Swing{}, fmt.Errorf("%w to calculate drawdowns", errReceivedNoData)
}
var swings []Swing
lowestPrice := closePrices[0].Value
highestPrice := closePrices[0].Value
lowestTime := closePrices[0].Time
highestTime := closePrices[0].Time
for i := range closePrices {
currHigh := closePrices[i].Value
currLow := closePrices[i].Value
currTime := closePrices[i].Time
if lowestPrice.GreaterThan(currLow) && !currLow.IsZero() {
lowestPrice = currLow
lowestTime = currTime
}
if highestPrice.LessThan(currHigh) && highestPrice.IsPositive() {
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, interval, 0)
if err != nil {
return Swing{}, err
}
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100)),
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
// reset the drawdown
highestPrice = currHigh
highestTime = currTime
lowestPrice = currLow
lowestTime = currTime
}
}
if (len(swings) > 0 && !swings[len(swings)-1].Lowest.Value.Equal(closePrices[len(closePrices)-1].Value)) || swings == nil {
// need to close out the final drawdown
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, interval, 0)
if err != nil {
log.Error(common.CurrencyStatistics, err)
}
drawdownPercent := decimal.Zero
if highestPrice.GreaterThan(decimal.Zero) {
drawdownPercent = lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100))
}
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: drawdownPercent,
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
}
var maxDrawdown Swing
if len(swings) > 0 {
maxDrawdown = swings[0]
}
for i := range swings {
if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
maxDrawdown = swings[i]
}
}
return maxDrawdown, nil
}
// CalculateRatios creates arithmetic and geometric ratios from funding or currency pair data
func CalculateRatios(benchmarkRates, returnsPerCandle []decimal.Decimal, riskFreeRatePerCandle decimal.Decimal, maxDrawdown *Swing, logMessage string) (arithmeticStats, geometricStats *Ratios, err error) {
var arithmeticBenchmarkAverage, geometricBenchmarkAverage decimal.Decimal
arithmeticBenchmarkAverage, err = gctmath.DecimalArithmeticMean(benchmarkRates)
if err != nil {
return nil, nil, err
}
geometricBenchmarkAverage, err = gctmath.DecimalFinancialGeometricMean(benchmarkRates)
if err != nil {
return nil, nil, err
}
riskFreeRateForPeriod := riskFreeRatePerCandle.Mul(decimal.NewFromInt(int64(len(benchmarkRates))))
var arithmeticReturnsPerCandle, geometricReturnsPerCandle, arithmeticSharpe, arithmeticSortino,
arithmeticInformation, arithmeticCalmar, geomSharpe, geomSortino, geomInformation, geomCalmar decimal.Decimal
arithmeticReturnsPerCandle, err = gctmath.DecimalArithmeticMean(returnsPerCandle)
if err != nil {
return nil, nil, err
}
geometricReturnsPerCandle, err = gctmath.DecimalFinancialGeometricMean(returnsPerCandle)
if err != nil {
return nil, nil, err
}
arithmeticSharpe, err = gctmath.DecimalSharpeRatio(returnsPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
if err != nil {
return nil, nil, err
}
arithmeticSortino, err = gctmath.DecimalSortinoRatio(returnsPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
if errors.Is(err, gctmath.ErrInexactConversion) {
log.Warnf(common.Statistics, "%s funding arithmetic sortino ratio %v", logMessage, err)
} else {
return nil, nil, err
}
}
arithmeticInformation, err = gctmath.DecimalInformationRatio(returnsPerCandle, benchmarkRates, arithmeticReturnsPerCandle, arithmeticBenchmarkAverage)
if err != nil {
return nil, nil, err
}
arithmeticCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, arithmeticReturnsPerCandle, riskFreeRateForPeriod)
if err != nil {
return nil, nil, err
}
arithmeticStats = &Ratios{}
if !arithmeticSharpe.IsZero() {
arithmeticStats.SharpeRatio = arithmeticSharpe
}
if !arithmeticSortino.IsZero() {
arithmeticStats.SortinoRatio = arithmeticSortino
}
if !arithmeticInformation.IsZero() {
arithmeticStats.InformationRatio = arithmeticInformation
}
if !arithmeticCalmar.IsZero() {
arithmeticStats.CalmarRatio = arithmeticCalmar
}
geomSharpe, err = gctmath.DecimalSharpeRatio(returnsPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
if err != nil {
return nil, nil, err
}
geomSortino, err = gctmath.DecimalSortinoRatio(returnsPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
if errors.Is(err, gctmath.ErrInexactConversion) {
log.Warnf(common.Statistics, "%s geometric sortino ratio %v", logMessage, err)
} else {
return nil, nil, err
}
}
geomInformation, err = gctmath.DecimalInformationRatio(returnsPerCandle, benchmarkRates, geometricReturnsPerCandle, geometricBenchmarkAverage)
if err != nil {
return nil, nil, err
}
geomCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, geometricReturnsPerCandle, riskFreeRateForPeriod)
if err != nil {
return nil, nil, err
}
geometricStats = &Ratios{}
if !arithmeticSharpe.IsZero() {
geometricStats.SharpeRatio = geomSharpe
}
if !arithmeticSortino.IsZero() {
geometricStats.SortinoRatio = geomSortino
}
if !arithmeticInformation.IsZero() {
geometricStats.InformationRatio = geomInformation
}
if !arithmeticCalmar.IsZero() {
geometricStats.CalmarRatio = geomCalmar
}
return arithmeticStats, geometricStats, nil
}

View File

@@ -2,19 +2,13 @@ package statistics
import (
"fmt"
"sort"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
// CalculateResults calculates all statistics for the exchange, asset, currency pair
@@ -24,23 +18,32 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
first := c.Events[0]
sep := fmt.Sprintf("%v %v %v |\t", first.DataEvent.GetExchange(), first.DataEvent.GetAssetType(), first.DataEvent.Pair())
firstPrice := first.DataEvent.GetClosePrice()
firstPrice := first.ClosePrice
last := c.Events[len(c.Events)-1]
lastPrice := last.DataEvent.GetClosePrice()
lastPrice := last.ClosePrice
for i := range last.Transactions.Orders {
if last.Transactions.Orders[i].Side == gctorder.Buy {
switch last.Transactions.Orders[i].Order.Side {
case gctorder.Buy, gctorder.Bid:
c.BuyOrders++
} else if last.Transactions.Orders[i].Side == gctorder.Sell {
case gctorder.Sell, gctorder.Ask:
c.SellOrders++
case gctorder.Long:
c.LongOrders++
case gctorder.Short:
c.ShortOrders++
}
}
for i := range c.Events {
price := c.Events[i].DataEvent.GetClosePrice()
if c.LowestClosePrice.IsZero() || price.LessThan(c.LowestClosePrice) {
c.LowestClosePrice = price
price := c.Events[i].ClosePrice
if price.LessThan(c.LowestClosePrice.Value) || !c.LowestClosePrice.Set {
c.LowestClosePrice.Value = price
c.LowestClosePrice.Time = c.Events[i].Time
c.LowestClosePrice.Set = true
}
if price.GreaterThan(c.HighestClosePrice) {
c.HighestClosePrice = price
if price.GreaterThan(c.HighestClosePrice.Value) {
c.HighestClosePrice.Value = price
c.HighestClosePrice.Time = c.Events[i].Time
c.HighestClosePrice.Set = true
}
}
@@ -51,7 +54,11 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
if first.Holdings.TotalValue.GreaterThan(decimal.Zero) {
c.StrategyMovement = last.Holdings.TotalValue.Sub(first.Holdings.TotalValue).Div(first.Holdings.TotalValue).Mul(oneHundred)
}
c.calculateHighestCommittedFunds()
c.analysePNLGrowth()
err = c.calculateHighestCommittedFunds()
if err != nil {
return err
}
returnsPerCandle := make([]decimal.Decimal, len(c.Events))
benchmarkRates := make([]decimal.Decimal, len(c.Events))
@@ -65,16 +72,16 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
if c.Events[i].SignalEvent != nil && c.Events[i].SignalEvent.GetDirection() == gctorder.MissingData {
c.ShowMissingDataWarning = true
}
if c.Events[i].DataEvent.GetClosePrice().IsZero() || c.Events[i-1].DataEvent.GetClosePrice().IsZero() {
if c.Events[i].ClosePrice.IsZero() || c.Events[i-1].ClosePrice.IsZero() {
// closing price for the current candle or previous candle is zero, use the previous
// benchmark rate to allow some consistency
c.ShowMissingDataWarning = true
benchmarkRates[i] = benchmarkRates[i-1]
continue
}
benchmarkRates[i] = c.Events[i].DataEvent.GetClosePrice().Sub(
c.Events[i-1].DataEvent.GetClosePrice()).Div(
c.Events[i-1].DataEvent.GetClosePrice())
benchmarkRates[i] = c.Events[i].ClosePrice.Sub(
c.Events[i-1].ClosePrice).Div(
c.Events[i-1].ClosePrice)
}
// remove the first entry as its zero and impacts
@@ -94,8 +101,9 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
return err
}
if last.Holdings.QuoteInitialFunds.GreaterThan(decimal.Zero) {
cagr, err := gctmath.DecimalCompoundAnnualGrowthRate(
if !last.Holdings.QuoteInitialFunds.IsZero() {
var cagr decimal.Decimal
cagr, err = gctmath.DecimalCompoundAnnualGrowthRate(
last.Holdings.QuoteInitialFunds,
last.Holdings.TotalValue,
decimal.NewFromFloat(intervalsPerYear),
@@ -104,305 +112,89 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
if err != nil {
errs = append(errs, err)
}
if !cagr.IsZero() {
c.CompoundAnnualGrowthRate = cagr
}
c.CompoundAnnualGrowthRate = cagr
}
c.IsStrategyProfitable = last.Holdings.TotalValue.GreaterThan(first.Holdings.TotalValue)
c.DoesPerformanceBeatTheMarket = c.StrategyMovement.GreaterThan(c.MarketMovement)
c.TotalFees = last.Holdings.TotalFees.Round(8)
c.TotalValueLostToVolumeSizing = last.Holdings.TotalValueLostToVolumeSizing.Round(2)
c.TotalValueLost = last.Holdings.TotalValueLost.Round(2)
c.TotalValueLostToSlippage = last.Holdings.TotalValueLostToSlippage.Round(2)
c.TotalAssetValue = last.Holdings.BaseValue.Round(8)
if last.PNL != nil {
c.UnrealisedPNL = last.PNL.GetUnrealisedPNL().PNL
c.RealisedPNL = last.PNL.GetRealisedPNL().PNL
}
if len(errs) > 0 {
return errs
}
return nil
}
// PrintResults outputs all calculated statistics to the command line
func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.Pair, usingExchangeLevelFunding bool) {
var errs gctcommon.Errors
sort.Slice(c.Events, func(i, j int) bool {
return c.Events[i].DataEvent.GetTime().Before(c.Events[j].DataEvent.GetTime())
})
last := c.Events[len(c.Events)-1]
first := c.Events[0]
c.StartingClosePrice = first.DataEvent.GetClosePrice()
c.EndingClosePrice = last.DataEvent.GetClosePrice()
c.TotalOrders = c.BuyOrders + c.SellOrders
last.Holdings.TotalValueLost = last.Holdings.TotalValueLostToSlippage.Add(last.Holdings.TotalValueLostToVolumeSizing)
sep := fmt.Sprintf("%v %v %v |\t", e, a, p)
currStr := fmt.Sprintf("------------------Stats for %v %v %v------------------------------------------", e, a, p)
log.Infof(log.BackTester, currStr[:61])
log.Infof(log.BackTester, "%s Highest committed funds: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestCommittedFunds.Value, 8, ".", ","), c.HighestCommittedFunds.Time)
log.Infof(log.BackTester, "%s Buy orders: %s", sep, convert.IntToHumanFriendlyString(c.BuyOrders, ","))
log.Infof(log.BackTester, "%s Buy value: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BoughtValue, 8, ".", ","))
log.Infof(log.BackTester, "%s Buy amount: %s %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BoughtAmount, 8, ".", ","), last.Holdings.Pair.Base)
log.Infof(log.BackTester, "%s Sell orders: %s", sep, convert.IntToHumanFriendlyString(c.SellOrders, ","))
log.Infof(log.BackTester, "%s Sell value: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.SoldValue, 8, ".", ","))
log.Infof(log.BackTester, "%s Sell amount: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.SoldAmount, 8, ".", ","))
log.Infof(log.BackTester, "%s Total orders: %s\n\n", sep, convert.IntToHumanFriendlyString(c.TotalOrders, ","))
log.Info(log.BackTester, "------------------Max Drawdown-------------------------------")
log.Infof(log.BackTester, "%s Highest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value, 8, ".", ","), c.MaxDrawdown.Highest.Time)
log.Infof(log.BackTester, "%s Lowest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Lowest.Value, 8, ".", ","), c.MaxDrawdown.Lowest.Time)
log.Infof(log.BackTester, "%s Calculated Drawdown: %s%%", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.DrawdownPercent, 8, ".", ","))
log.Infof(log.BackTester, "%s Difference: %s", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value.Sub(c.MaxDrawdown.Lowest.Value), 2, ".", ","))
log.Infof(log.BackTester, "%s Drawdown length: %s\n\n", sep, convert.IntToHumanFriendlyString(c.MaxDrawdown.IntervalDuration, ","))
if !usingExchangeLevelFunding {
log.Info(log.BackTester, "------------------Ratios------------------------------------------------")
log.Info(log.BackTester, "------------------Rates-------------------------------------------------")
log.Infof(log.BackTester, "%s Compound Annual Growth Rate: %s", sep, convert.DecimalToHumanFriendlyString(c.CompoundAnnualGrowthRate, 2, ".", ","))
log.Info(log.BackTester, "------------------Arithmetic--------------------------------------------")
if c.ShowMissingDataWarning {
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
func (c *CurrencyPairStatistic) calculateHighestCommittedFunds() error {
switch {
case c.Asset == asset.Spot:
for i := range c.Events {
if c.Events[i].Holdings.CommittedFunds.GreaterThan(c.HighestCommittedFunds.Value) || !c.HighestCommittedFunds.Set {
c.HighestCommittedFunds.Value = c.Events[i].Holdings.CommittedFunds
c.HighestCommittedFunds.Time = c.Events[i].Time
c.HighestCommittedFunds.Set = true
}
}
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, c.ArithmeticRatios.SharpeRatio.Round(4))
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, c.ArithmeticRatios.SortinoRatio.Round(4))
log.Infof(log.BackTester, "%s Information ratio: %v", sep, c.ArithmeticRatios.InformationRatio.Round(4))
log.Infof(log.BackTester, "%s Calmar ratio: %v", sep, c.ArithmeticRatios.CalmarRatio.Round(4))
log.Info(log.BackTester, "------------------Geometric--------------------------------------------")
if c.ShowMissingDataWarning {
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
}
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, c.GeometricRatios.SharpeRatio.Round(4))
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, c.GeometricRatios.SortinoRatio.Round(4))
log.Infof(log.BackTester, "%s Information ratio: %v", sep, c.GeometricRatios.InformationRatio.Round(4))
log.Infof(log.BackTester, "%s Calmar ratio: %v\n\n", sep, c.GeometricRatios.CalmarRatio.Round(4))
}
log.Info(log.BackTester, "------------------Results------------------------------------")
log.Infof(log.BackTester, "%s Starting Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.StartingClosePrice, 8, ".", ","))
log.Infof(log.BackTester, "%s Finishing Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.EndingClosePrice, 8, ".", ","))
log.Infof(log.BackTester, "%s Lowest Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.LowestClosePrice, 8, ".", ","))
log.Infof(log.BackTester, "%s Highest Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.HighestClosePrice, 8, ".", ","))
log.Infof(log.BackTester, "%s Market movement: %s%%", sep, convert.DecimalToHumanFriendlyString(c.MarketMovement, 2, ".", ","))
if !usingExchangeLevelFunding {
log.Infof(log.BackTester, "%s Strategy movement: %s%%", sep, convert.DecimalToHumanFriendlyString(c.StrategyMovement, 2, ".", ","))
log.Infof(log.BackTester, "%s Did it beat the market: %v", sep, c.StrategyMovement.GreaterThan(c.MarketMovement))
}
log.Infof(log.BackTester, "%s Value lost to volume sizing: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLostToVolumeSizing, 2, ".", ","))
log.Infof(log.BackTester, "%s Value lost to slippage: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLostToSlippage, 2, ".", ","))
log.Infof(log.BackTester, "%s Total Value lost: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLost, 2, ".", ","))
log.Infof(log.BackTester, "%s Total Fees: %s\n\n", sep, convert.DecimalToHumanFriendlyString(c.TotalFees, 8, ".", ","))
log.Infof(log.BackTester, "%s Final holdings value: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalAssetValue, 8, ".", ","))
if !usingExchangeLevelFunding {
// the following have no direct translation to individual exchange level funds as they
// combine base and quote values
log.Infof(log.BackTester, "%s Final funds: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.QuoteSize, 8, ".", ","))
log.Infof(log.BackTester, "%s Final holdings: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BaseSize, 8, ".", ","))
log.Infof(log.BackTester, "%s Final total value: %s\n\n", sep, convert.DecimalToHumanFriendlyString(last.Holdings.TotalValue, 8, ".", ","))
}
if len(errs) > 0 {
log.Info(log.BackTester, "------------------Errors-------------------------------------")
for i := range errs {
log.Error(log.BackTester, errs[i].Error())
case c.Asset.IsFutures():
for i := range c.Events {
valueAtTime := c.Events[i].Holdings.BaseSize.Mul(c.Events[i].ClosePrice)
if valueAtTime.GreaterThan(c.HighestCommittedFunds.Value) || !c.HighestCommittedFunds.Set {
c.HighestCommittedFunds.Value = valueAtTime
c.HighestCommittedFunds.Time = c.Events[i].Time
c.HighestCommittedFunds.Set = true
}
}
default:
return fmt.Errorf("%v %w", c.Asset, asset.ErrNotSupported)
}
return nil
}
// CalculateBiggestEventDrawdown calculates the biggest drawdown using a slice of DataEvents
func CalculateBiggestEventDrawdown(closePrices []common.DataEventHandler) (Swing, error) {
if len(closePrices) == 0 {
return Swing{}, fmt.Errorf("%w to calculate drawdowns", errReceivedNoData)
func (c *CurrencyPairStatistic) analysePNLGrowth() {
if !c.Asset.IsFutures() {
return
}
var swings []Swing
lowestPrice := closePrices[0].GetLowPrice()
highestPrice := closePrices[0].GetHighPrice()
lowestTime := closePrices[0].GetTime()
highestTime := closePrices[0].GetTime()
interval := closePrices[0].GetInterval()
for i := range closePrices {
currHigh := closePrices[i].GetHighPrice()
currLow := closePrices[i].GetLowPrice()
currTime := closePrices[i].GetTime()
if lowestPrice.GreaterThan(currLow) && !currLow.IsZero() {
lowestPrice = currLow
lowestTime = currTime
}
if highestPrice.LessThan(currHigh) {
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[i].GetInterval(), 0)
if err != nil {
log.Error(log.BackTester, err)
continue
}
if highestPrice.IsPositive() && lowestPrice.IsPositive() {
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100)),
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
}
// reset the drawdown
highestPrice = currHigh
highestTime = currTime
lowestPrice = currLow
lowestTime = currTime
}
}
if (len(swings) > 0 && swings[len(swings)-1].Lowest.Value != closePrices[len(closePrices)-1].GetLowPrice()) || swings == nil {
// need to close out the final drawdown
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[0].GetInterval(), 0)
if err != nil {
return Swing{}, err
}
drawdownPercent := decimal.Zero
if highestPrice.GreaterThan(decimal.Zero) {
drawdownPercent = lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100))
}
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: drawdownPercent,
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
}
var maxDrawdown Swing
if len(swings) > 0 {
maxDrawdown = swings[0]
}
for i := range swings {
if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
maxDrawdown = swings[i]
}
}
return maxDrawdown, nil
}
func (c *CurrencyPairStatistic) calculateHighestCommittedFunds() {
var lowestUnrealised, highestUnrealised, lowestRealised, highestRealised ValueAtTime
for i := range c.Events {
if c.Events[i].Holdings.BaseSize.Mul(c.Events[i].DataEvent.GetClosePrice()).GreaterThan(c.HighestCommittedFunds.Value) {
c.HighestCommittedFunds.Value = c.Events[i].Holdings.BaseSize.Mul(c.Events[i].DataEvent.GetClosePrice())
c.HighestCommittedFunds.Time = c.Events[i].Holdings.Timestamp
if c.Events[i].PNL == nil {
continue
}
unrealised := c.Events[i].PNL.GetUnrealisedPNL()
realised := c.Events[i].PNL.GetRealisedPNL()
if unrealised.PNL.LessThan(lowestUnrealised.Value) ||
(!lowestUnrealised.Set && !unrealised.PNL.IsZero()) {
lowestUnrealised.Value = unrealised.PNL
lowestUnrealised.Time = unrealised.Time
lowestUnrealised.Set = true
}
if unrealised.PNL.GreaterThan(highestUnrealised.Value) ||
(!highestUnrealised.Set && !unrealised.PNL.IsZero()) {
highestUnrealised.Value = unrealised.PNL
highestUnrealised.Time = unrealised.Time
highestUnrealised.Set = true
}
if realised.PNL.LessThan(lowestRealised.Value) ||
(!lowestRealised.Set && !realised.PNL.IsZero()) {
lowestRealised.Value = realised.PNL
lowestRealised.Time = realised.Time
lowestRealised.Set = true
}
if realised.PNL.GreaterThan(highestRealised.Value) ||
(!highestRealised.Set && !realised.PNL.IsZero()) {
highestRealised.Value = realised.PNL
highestRealised.Time = realised.Time
highestRealised.Set = true
}
}
}
// CalculateBiggestValueAtTimeDrawdown calculates the biggest drawdown using a slice of ValueAtTimes
func CalculateBiggestValueAtTimeDrawdown(closePrices []ValueAtTime, interval gctkline.Interval) (Swing, error) {
if len(closePrices) == 0 {
return Swing{}, fmt.Errorf("%w to calculate drawdowns", errReceivedNoData)
}
var swings []Swing
lowestPrice := closePrices[0].Value
highestPrice := closePrices[0].Value
lowestTime := closePrices[0].Time
highestTime := closePrices[0].Time
for i := range closePrices {
currHigh := closePrices[i].Value
currLow := closePrices[i].Value
currTime := closePrices[i].Time
if lowestPrice.GreaterThan(currLow) && !currLow.IsZero() {
lowestPrice = currLow
lowestTime = currTime
}
if highestPrice.LessThan(currHigh) && highestPrice.IsPositive() {
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, interval, 0)
if err != nil {
return Swing{}, err
}
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100)),
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
// reset the drawdown
highestPrice = currHigh
highestTime = currTime
lowestPrice = currLow
lowestTime = currTime
}
}
if (len(swings) > 0 && !swings[len(swings)-1].Lowest.Value.Equal(closePrices[len(closePrices)-1].Value)) || swings == nil {
// need to close out the final drawdown
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, interval, 0)
if err != nil {
log.Error(log.BackTester, err)
}
drawdownPercent := decimal.Zero
if highestPrice.GreaterThan(decimal.Zero) {
drawdownPercent = lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100))
}
if lowestTime.Equal(highestTime) {
// create distinction if the greatest drawdown occurs within the same candle
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
}
swings = append(swings, Swing{
Highest: ValueAtTime{
Time: highestTime,
Value: highestPrice,
},
Lowest: ValueAtTime{
Time: lowestTime,
Value: lowestPrice,
},
DrawdownPercent: drawdownPercent,
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
})
}
var maxDrawdown Swing
if len(swings) > 0 {
maxDrawdown = swings[0]
}
for i := range swings {
if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
maxDrawdown = swings[i]
}
}
return maxDrawdown, nil
c.LowestRealisedPNL = lowestRealised
c.LowestUnrealisedPNL = lowestUnrealised
c.HighestUnrealisedPNL = highestUnrealised
c.HighestRealisedPNL = highestRealised
}

View File

@@ -1,10 +1,12 @@
package statistics
import (
"errors"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
@@ -18,20 +20,26 @@ import (
func TestCalculateResults(t *testing.T) {
t.Parallel()
cs := CurrencyPairStatistic{}
a := asset.Spot
cs := CurrencyPairStatistic{
Asset: a,
}
tt1 := time.Now()
tt2 := time.Now().Add(gctkline.OneDay.Duration())
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
even := event.Base{
even := &event.Base{
Exchange: exch,
Time: tt1,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
Offset: 1,
}
ev := EventStore{
ev := DataAtOffset{
Offset: 1,
Time: tt1,
ClosePrice: decimal.NewFromInt(2000),
Holdings: holdings.Holding{
ChangeInTotalValuePercent: decimal.NewFromFloat(0.1333),
Timestamp: tt1,
@@ -44,14 +52,14 @@ func TestCalculateResults(t *testing.T) {
VolumeAdjustedPrice: decimal.NewFromInt(1338),
SlippageRate: decimal.NewFromInt(1338),
CostBasis: decimal.NewFromInt(1338),
Detail: &order.Detail{Side: order.Buy},
Order: &order.Detail{Side: order.Buy},
},
{
ClosePrice: decimal.NewFromInt(1337),
VolumeAdjustedPrice: decimal.NewFromInt(1337),
SlippageRate: decimal.NewFromInt(1337),
CostBasis: decimal.NewFromInt(1337),
Detail: &order.Detail{Side: order.Sell},
Order: &order.Detail{Side: order.Sell},
},
},
},
@@ -70,7 +78,11 @@ func TestCalculateResults(t *testing.T) {
}
even2 := even
even2.Time = tt2
ev2 := EventStore{
even2.Offset = 2
ev2 := DataAtOffset{
Offset: 2,
Time: tt2,
ClosePrice: decimal.NewFromInt(1337),
Holdings: holdings.Holding{
ChangeInTotalValuePercent: decimal.NewFromFloat(0.1337),
Timestamp: tt2,
@@ -83,14 +95,14 @@ func TestCalculateResults(t *testing.T) {
VolumeAdjustedPrice: decimal.NewFromInt(1338),
SlippageRate: decimal.NewFromInt(1338),
CostBasis: decimal.NewFromInt(1338),
Detail: &order.Detail{Side: order.Buy},
Order: &order.Detail{Side: order.Buy},
},
{
ClosePrice: decimal.NewFromInt(1337),
VolumeAdjustedPrice: decimal.NewFromInt(1337),
SlippageRate: decimal.NewFromInt(1337),
CostBasis: decimal.NewFromInt(1337),
Detail: &order.Detail{Side: order.Sell},
Order: &order.Detail{Side: order.Sell},
},
},
},
@@ -115,7 +127,7 @@ func TestCalculateResults(t *testing.T) {
t.Error(err)
}
if !cs.MarketMovement.Equal(decimal.NewFromFloat(-33.15)) {
t.Error("expected -33.15")
t.Errorf("expected -33.15 received '%v'", cs.MarketMovement)
}
ev3 := ev2
ev3.DataEvent = &kline.Kline{
@@ -128,12 +140,7 @@ func TestCalculateResults(t *testing.T) {
}
cs.Events = append(cs.Events, ev, ev3)
cs.Events[0].DataEvent = &kline.Kline{
Base: even2,
Open: decimal.Zero,
Close: decimal.Zero,
Low: decimal.Zero,
High: decimal.Zero,
Volume: decimal.Zero,
Base: even2,
}
err = cs.CalculateResults(decimal.NewFromFloat(0.03))
if err != nil {
@@ -141,12 +148,7 @@ func TestCalculateResults(t *testing.T) {
}
cs.Events[1].DataEvent = &kline.Kline{
Base: even2,
Open: decimal.Zero,
Close: decimal.Zero,
Low: decimal.Zero,
High: decimal.Zero,
Volume: decimal.Zero,
Base: even2,
}
err = cs.CalculateResults(decimal.NewFromFloat(0.03))
if err != nil {
@@ -161,14 +163,14 @@ func TestPrintResults(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
even := event.Base{
even := &event.Base{
Exchange: exch,
Time: tt1,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
}
ev := EventStore{
ev := DataAtOffset{
Holdings: holdings.Holding{
ChangeInTotalValuePercent: decimal.NewFromFloat(0.1333),
Timestamp: tt1,
@@ -181,14 +183,14 @@ func TestPrintResults(t *testing.T) {
VolumeAdjustedPrice: decimal.NewFromInt(1338),
SlippageRate: decimal.NewFromInt(1338),
CostBasis: decimal.NewFromInt(1338),
Detail: &order.Detail{Side: order.Buy},
Order: &order.Detail{Side: order.Buy},
},
{
ClosePrice: decimal.NewFromInt(1337),
VolumeAdjustedPrice: decimal.NewFromInt(1337),
SlippageRate: decimal.NewFromInt(1337),
CostBasis: decimal.NewFromInt(1337),
Detail: &order.Detail{Side: order.Sell},
Order: &order.Detail{Side: order.Sell},
},
},
},
@@ -207,7 +209,7 @@ func TestPrintResults(t *testing.T) {
}
even2 := even
even2.Time = tt2
ev2 := EventStore{
ev2 := DataAtOffset{
Holdings: holdings.Holding{
ChangeInTotalValuePercent: decimal.NewFromFloat(0.1337),
Timestamp: tt2,
@@ -220,14 +222,14 @@ func TestPrintResults(t *testing.T) {
VolumeAdjustedPrice: decimal.NewFromInt(1338),
SlippageRate: decimal.NewFromInt(1338),
CostBasis: decimal.NewFromInt(1338),
Detail: &order.Detail{Side: order.Buy},
Order: &order.Detail{Side: order.Buy},
},
{
ClosePrice: decimal.NewFromInt(1337),
VolumeAdjustedPrice: decimal.NewFromInt(1337),
SlippageRate: decimal.NewFromInt(1337),
CostBasis: decimal.NewFromInt(1337),
Detail: &order.Detail{Side: order.Sell},
Order: &order.Detail{Side: order.Sell},
},
},
},
@@ -251,8 +253,13 @@ func TestPrintResults(t *testing.T) {
func TestCalculateHighestCommittedFunds(t *testing.T) {
t.Parallel()
c := CurrencyPairStatistic{}
c.calculateHighestCommittedFunds()
c := CurrencyPairStatistic{
Asset: asset.Spot,
}
err := c.calculateHighestCommittedFunds()
if !errors.Is(err, nil) {
t.Error(err)
}
if !c.HighestCommittedFunds.Time.IsZero() {
t.Error("expected no time with not committed funds")
}
@@ -260,12 +267,88 @@ func TestCalculateHighestCommittedFunds(t *testing.T) {
tt2 := time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)
tt3 := time.Date(2021, 3, 1, 0, 0, 0, 0, time.UTC)
c.Events = append(c.Events,
EventStore{DataEvent: &kline.Kline{Close: decimal.NewFromInt(1337)}, Holdings: holdings.Holding{Timestamp: tt1, BaseSize: decimal.NewFromInt(10)}},
EventStore{DataEvent: &kline.Kline{Close: decimal.NewFromInt(1338)}, Holdings: holdings.Holding{Timestamp: tt2, BaseSize: decimal.NewFromInt(1337)}},
EventStore{DataEvent: &kline.Kline{Close: decimal.NewFromInt(1339)}, Holdings: holdings.Holding{Timestamp: tt3, BaseSize: decimal.NewFromInt(11)}},
DataAtOffset{DataEvent: &kline.Kline{Close: decimal.NewFromInt(1337)}, Time: tt1, Holdings: holdings.Holding{Timestamp: tt1, CommittedFunds: decimal.NewFromInt(10), BaseSize: decimal.NewFromInt(10)}},
DataAtOffset{DataEvent: &kline.Kline{Close: decimal.NewFromInt(1338)}, Time: tt2, Holdings: holdings.Holding{Timestamp: tt2, CommittedFunds: decimal.NewFromInt(1337), BaseSize: decimal.NewFromInt(1337)}},
DataAtOffset{DataEvent: &kline.Kline{Close: decimal.NewFromInt(1339)}, Time: tt3, Holdings: holdings.Holding{Timestamp: tt3, CommittedFunds: decimal.NewFromInt(11), BaseSize: decimal.NewFromInt(11)}},
)
c.calculateHighestCommittedFunds()
err = c.calculateHighestCommittedFunds()
if !errors.Is(err, nil) {
t.Error(err)
}
if c.HighestCommittedFunds.Time != tt2 {
t.Errorf("expected %v, received %v", tt2, c.HighestCommittedFunds.Time)
}
c.Asset = asset.Futures
c.HighestCommittedFunds = ValueAtTime{}
err = c.calculateHighestCommittedFunds()
if !errors.Is(err, nil) {
t.Error(err)
}
c.Asset = asset.Binary
err = c.calculateHighestCommittedFunds()
if !errors.Is(err, asset.ErrNotSupported) {
t.Error(err)
}
}
func TestAnalysePNLGrowth(t *testing.T) {
t.Parallel()
c := CurrencyPairStatistic{}
c.analysePNLGrowth()
if !c.HighestUnrealisedPNL.Value.IsZero() ||
!c.LowestUnrealisedPNL.Value.IsZero() ||
!c.LowestRealisedPNL.Value.IsZero() ||
!c.HighestRealisedPNL.Value.IsZero() {
t.Error("expected unset")
}
e := testExchange
a := asset.Futures
p := currency.NewPair(currency.BTC, currency.USDT)
c.Asset = asset.Futures
c.Events = append(c.Events,
DataAtOffset{PNL: &portfolio.PNLSummary{
Exchange: e,
Item: a,
Pair: p,
Offset: 0,
Result: order.PNLResult{
Time: time.Now(),
UnrealisedPNL: decimal.NewFromInt(1),
RealisedPNL: decimal.NewFromInt(2),
},
}},
)
c.analysePNLGrowth()
if !c.HighestRealisedPNL.Value.Equal(decimal.NewFromInt(2)) {
t.Errorf("received %v expected 2", c.HighestRealisedPNL.Value)
}
if !c.LowestUnrealisedPNL.Value.Equal(decimal.NewFromInt(1)) {
t.Errorf("received %v expected 1", c.LowestUnrealisedPNL.Value)
}
c.Events = append(c.Events,
DataAtOffset{PNL: &portfolio.PNLSummary{
Exchange: e,
Item: a,
Pair: p,
Offset: 0,
Result: order.PNLResult{
Time: time.Now(),
UnrealisedPNL: decimal.NewFromFloat(0.5),
RealisedPNL: decimal.NewFromInt(1),
},
}},
)
c.analysePNLGrowth()
if !c.HighestRealisedPNL.Value.Equal(decimal.NewFromInt(2)) {
t.Errorf("received %v expected 2", c.HighestRealisedPNL.Value)
}
if !c.LowestUnrealisedPNL.Value.Equal(decimal.NewFromFloat(0.5)) {
t.Errorf("received %v expected 0.5", c.LowestUnrealisedPNL.Value)
}
}

View File

@@ -7,12 +7,10 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/common/convert"
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/log"
)
// CalculateFundingStatistics calculates funding statistics for total USD strategy results
@@ -57,25 +55,21 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
LowestHoldingValue: ValueAtTime{},
RiskFreeRate: riskFreeRate,
}
for i := range response.Items {
usdStats.TotalOrders += response.Items[i].TotalOrders
usdStats.BuyOrders += response.Items[i].BuyOrders
usdStats.SellOrders += response.Items[i].SellOrders
}
for k, v := range report.USDTotalsOverTime {
if usdStats.HighestHoldingValue.Value.LessThan(v.USDValue) {
usdStats.HighestHoldingValue.Time = k
usdStats.HighestHoldingValue.Value = v.USDValue
for i := range report.USDTotalsOverTime {
if usdStats.HighestHoldingValue.Value.LessThan(report.USDTotalsOverTime[i].USDValue) {
usdStats.HighestHoldingValue.Time = report.USDTotalsOverTime[i].Time
usdStats.HighestHoldingValue.Value = report.USDTotalsOverTime[i].USDValue
}
if usdStats.LowestHoldingValue.Value.IsZero() {
usdStats.LowestHoldingValue.Time = k
usdStats.LowestHoldingValue.Value = v.USDValue
usdStats.LowestHoldingValue.Time = report.USDTotalsOverTime[i].Time
usdStats.LowestHoldingValue.Value = report.USDTotalsOverTime[i].USDValue
}
if usdStats.LowestHoldingValue.Value.GreaterThan(v.USDValue) && !usdStats.LowestHoldingValue.Value.IsZero() {
usdStats.LowestHoldingValue.Time = k
usdStats.LowestHoldingValue.Value = v.USDValue
if usdStats.LowestHoldingValue.Value.GreaterThan(report.USDTotalsOverTime[i].USDValue) && !usdStats.LowestHoldingValue.Value.IsZero() {
usdStats.LowestHoldingValue.Time = report.USDTotalsOverTime[i].Time
usdStats.LowestHoldingValue.Value = report.USDTotalsOverTime[i].USDValue
}
usdStats.HoldingValues = append(usdStats.HoldingValues, ValueAtTime{Time: k, Value: v.USDValue})
usdStats.HoldingValues = append(usdStats.HoldingValues, ValueAtTime{Time: report.USDTotalsOverTime[i].Time, Value: report.USDTotalsOverTime[i].USDValue})
}
sort.Slice(usdStats.HoldingValues, func(i, j int) bool {
return usdStats.HoldingValues[i].Time.Before(usdStats.HoldingValues[j].Time)
@@ -91,9 +85,7 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
usdStats.HoldingValues[0].Value).Mul(
decimal.NewFromInt(100))
}
usdStats.InitialHoldingValue = usdStats.HoldingValues[0]
usdStats.FinalHoldingValue = usdStats.HoldingValues[len(usdStats.HoldingValues)-1]
usdStats.HoldingValueDifference = usdStats.FinalHoldingValue.Value.Sub(usdStats.InitialHoldingValue.Value).Div(usdStats.InitialHoldingValue.Value).Mul(decimal.NewFromInt(100))
usdStats.HoldingValueDifference = report.FinalFunds.Sub(report.InitialFunds).Div(report.InitialFunds).Mul(decimal.NewFromInt(100))
riskFreeRatePerCandle := usdStats.RiskFreeRate.Div(decimal.NewFromFloat(interval.IntervalsPerYear()))
returnsPerCandle := make([]decimal.Decimal, len(usdStats.HoldingValues))
@@ -122,8 +114,25 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
return nil, err
}
for i := range response.Items {
var cagr decimal.Decimal
if response.Items[i].ReportItem.InitialFunds.IsZero() {
continue
}
cagr, err = gctmath.DecimalCompoundAnnualGrowthRate(
response.Items[i].ReportItem.InitialFunds,
response.Items[i].ReportItem.FinalFunds,
decimal.NewFromFloat(interval.IntervalsPerYear()),
decimal.NewFromInt(int64(len(usdStats.HoldingValues))),
)
if err != nil {
return nil, err
}
response.Items[i].CompoundAnnualGrowthRate = cagr
}
if !usdStats.HoldingValues[0].Value.IsZero() {
cagr, err := gctmath.DecimalCompoundAnnualGrowthRate(
var cagr decimal.Decimal
cagr, err = gctmath.DecimalCompoundAnnualGrowthRate(
usdStats.HoldingValues[0].Value,
usdStats.HoldingValues[len(usdStats.HoldingValues)-1].Value,
decimal.NewFromFloat(interval.IntervalsPerYear()),
@@ -132,9 +141,7 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
if err != nil {
return nil, err
}
if !cagr.IsZero() {
usdStats.CompoundAnnualGrowthRate = cagr
}
usdStats.CompoundAnnualGrowthRate = cagr
}
usdStats.DidStrategyMakeProfit = usdStats.HoldingValues[len(usdStats.HoldingValues)-1].Value.GreaterThan(usdStats.HoldingValues[0].Value)
usdStats.DidStrategyBeatTheMarket = usdStats.StrategyMovement.GreaterThan(usdStats.BenchmarkMarketMovement)
@@ -154,6 +161,7 @@ func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *f
if disableUSDTracking {
return item, nil
}
closePrices := reportItem.Snapshots
if len(closePrices) == 0 {
return nil, errMissingSnapshots
@@ -167,32 +175,68 @@ func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *f
Value: closePrices[len(closePrices)-1].USDClosePrice,
}
for i := range closePrices {
if closePrices[i].USDClosePrice.LessThan(item.LowestClosePrice.Value) || item.LowestClosePrice.Value.IsZero() {
if closePrices[i].USDClosePrice.LessThan(item.LowestClosePrice.Value) || !item.LowestClosePrice.Set {
item.LowestClosePrice.Value = closePrices[i].USDClosePrice
item.LowestClosePrice.Time = closePrices[i].Time
item.LowestClosePrice.Set = true
}
if closePrices[i].USDClosePrice.GreaterThan(item.HighestClosePrice.Value) || item.HighestClosePrice.Value.IsZero() {
if closePrices[i].USDClosePrice.GreaterThan(item.HighestClosePrice.Value) || !item.HighestClosePrice.Set {
item.HighestClosePrice.Value = closePrices[i].USDClosePrice
item.HighestClosePrice.Time = closePrices[i].Time
item.HighestClosePrice.Set = true
}
}
for i := range relatedStats {
if relatedStats[i].stat == nil {
return nil, fmt.Errorf("%w related stats", common.ErrNilArguments)
item.IsCollateral = reportItem.IsCollateral
if reportItem.Asset.IsFutures() {
var lowest, highest, initial, final ValueAtTime
initial.Value = closePrices[0].Available
initial.Time = closePrices[0].Time
final.Value = closePrices[len(closePrices)-1].Available
final.Time = closePrices[len(closePrices)-1].Time
for i := range closePrices {
if closePrices[i].Available.LessThan(lowest.Value) || !lowest.Set {
lowest.Value = closePrices[i].Available
lowest.Time = closePrices[i].Time
lowest.Set = true
}
if closePrices[i].Available.GreaterThan(highest.Value) || !lowest.Set {
highest.Value = closePrices[i].Available
highest.Time = closePrices[i].Time
highest.Set = true
}
}
if relatedStats[i].isBaseCurrency {
item.BuyOrders += relatedStats[i].stat.BuyOrders
item.SellOrders += relatedStats[i].stat.SellOrders
if reportItem.IsCollateral {
item.LowestCollateral = lowest
item.HighestCollateral = highest
item.InitialCollateral = initial
item.FinalCollateral = final
} else {
item.LowestHoldings = lowest
item.HighestHoldings = highest
item.InitialHoldings = initial
item.FinalHoldings = final
}
}
if !reportItem.IsCollateral {
for i := range relatedStats {
if relatedStats[i].stat == nil {
return nil, fmt.Errorf("%w related stats", common.ErrNilArguments)
}
if relatedStats[i].isBaseCurrency {
item.BuyOrders += relatedStats[i].stat.BuyOrders
item.SellOrders += relatedStats[i].stat.SellOrders
}
}
}
item.TotalOrders = item.BuyOrders + item.SellOrders
if !item.ReportItem.ShowInfinite {
if !item.ReportItem.ShowInfinite && !reportItem.IsCollateral {
if item.ReportItem.Snapshots[0].USDValue.IsZero() {
item.ReportItem.ShowInfinite = true
} else {
item.StrategyMovement = item.ReportItem.Snapshots[len(item.ReportItem.Snapshots)-1].USDValue.Sub(
item.ReportItem.Snapshots[0].USDValue).Div(
item.ReportItem.Snapshots[0].USDValue).Mul(
item.StrategyMovement = item.ReportItem.USDFinalFunds.Sub(
item.ReportItem.USDInitialFunds).Div(
item.ReportItem.USDInitialFunds).Mul(
decimal.NewFromInt(100))
}
}
@@ -203,7 +247,9 @@ func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *f
item.ReportItem.Snapshots[0].USDClosePrice).Mul(
decimal.NewFromInt(100))
}
item.DidStrategyBeatTheMarket = item.StrategyMovement.GreaterThan(item.MarketMovement)
if !reportItem.IsCollateral {
item.DidStrategyBeatTheMarket = item.StrategyMovement.GreaterThan(item.MarketMovement)
}
item.HighestCommittedFunds = ValueAtTime{}
for j := range item.ReportItem.Snapshots {
if item.ReportItem.Snapshots[j].USDValue.GreaterThan(item.HighestCommittedFunds.Value) {
@@ -213,93 +259,17 @@ func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *f
}
}
}
if item.ReportItem.USDPairCandle == nil {
if item.ReportItem.USDPairCandle == nil && !reportItem.IsCollateral {
return nil, fmt.Errorf("%w usd candles missing", errMissingSnapshots)
}
s := item.ReportItem.USDPairCandle.GetStream()
if len(s) == 0 {
return nil, fmt.Errorf("%w stream missing", errMissingSnapshots)
}
if reportItem.IsCollateral {
return item, nil
}
var err error
item.MaxDrawdown, err = CalculateBiggestEventDrawdown(s)
if err != nil {
return nil, err
}
return item, nil
}
// PrintResults outputs all calculated funding statistics to the command line
func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
if f.Report == nil {
return fmt.Errorf("%w requires report to be generated", common.ErrNilArguments)
}
log.Info(log.BackTester, "------------------Funding------------------------------------")
log.Info(log.BackTester, "------------------Funding Item Results-----------------------")
for i := range f.Report.Items {
sep := fmt.Sprintf("%v %v %v |\t", f.Report.Items[i].Exchange, f.Report.Items[i].Asset, f.Report.Items[i].Currency)
if !f.Report.Items[i].PairedWith.IsEmpty() {
log.Infof(log.BackTester, "%s Paired with: %v", sep, f.Report.Items[i].PairedWith)
}
log.Infof(log.BackTester, "%s Initial funds: %s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].InitialFunds, 8, ".", ","))
log.Infof(log.BackTester, "%s Final funds: %s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].FinalFunds, 8, ".", ","))
if !f.Report.DisableUSDTracking && f.Report.UsingExchangeLevelFunding {
log.Infof(log.BackTester, "%s Initial funds in USD: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].USDInitialFunds, 2, ".", ","))
log.Infof(log.BackTester, "%s Final funds in USD: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].USDFinalFunds, 2, ".", ","))
}
if f.Report.Items[i].ShowInfinite {
log.Infof(log.BackTester, "%s Difference: ∞%%", sep)
} else {
log.Infof(log.BackTester, "%s Difference: %s%%", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].Difference, 8, ".", ","))
}
if f.Report.Items[i].TransferFee.GreaterThan(decimal.Zero) {
log.Infof(log.BackTester, "%s Transfer fee: %s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].TransferFee, 8, ".", ","))
}
log.Info(log.BackTester, "")
}
if f.Report.DisableUSDTracking {
return nil
}
log.Info(log.BackTester, "------------------USD Tracking Totals------------------------")
sep := "USD Tracking Total |\t"
log.Infof(log.BackTester, "%s Initial value: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.InitialHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.InitialHoldingValue.Time)
log.Infof(log.BackTester, "%s Final value: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.FinalHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.FinalHoldingValue.Time)
log.Infof(log.BackTester, "%s Benchmark Market Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.BenchmarkMarketMovement, 8, ".", ","))
log.Infof(log.BackTester, "%s Strategy Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.StrategyMovement, 8, ".", ","))
log.Infof(log.BackTester, "%s Did strategy make a profit: %v", sep, f.TotalUSDStatistics.DidStrategyMakeProfit)
log.Infof(log.BackTester, "%s Did strategy beat the benchmark: %v", sep, f.TotalUSDStatistics.DidStrategyBeatTheMarket)
log.Infof(log.BackTester, "%s Buy Orders: %s", sep, convert.IntToHumanFriendlyString(f.TotalUSDStatistics.BuyOrders, ","))
log.Infof(log.BackTester, "%s Sell Orders: %s", sep, convert.IntToHumanFriendlyString(f.TotalUSDStatistics.SellOrders, ","))
log.Infof(log.BackTester, "%s Total Orders: %s", sep, convert.IntToHumanFriendlyString(f.TotalUSDStatistics.TotalOrders, ","))
log.Infof(log.BackTester, "%s Highest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.HighestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.HighestHoldingValue.Time)
log.Infof(log.BackTester, "%s Lowest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.LowestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.LowestHoldingValue.Time)
log.Info(log.BackTester, "------------------Ratios------------------------------------------------")
log.Info(log.BackTester, "------------------Rates-------------------------------------------------")
log.Infof(log.BackTester, "%s Risk free rate: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.RiskFreeRate.Mul(decimal.NewFromInt(100)), 2, ".", ","))
log.Infof(log.BackTester, "%s Compound Annual Growth Rate: %v%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.CompoundAnnualGrowthRate, 8, ".", ","))
if f.TotalUSDStatistics.ArithmeticRatios == nil || f.TotalUSDStatistics.GeometricRatios == nil {
return fmt.Errorf("%w missing ratio calculations", common.ErrNilArguments)
}
log.Info(log.BackTester, "------------------Arithmetic--------------------------------------------")
if wasAnyDataMissing {
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
}
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.SharpeRatio.Round(4))
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.SortinoRatio.Round(4))
log.Infof(log.BackTester, "%s Information ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.InformationRatio.Round(4))
log.Infof(log.BackTester, "%s Calmar ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.CalmarRatio.Round(4))
log.Info(log.BackTester, "------------------Geometric--------------------------------------------")
if wasAnyDataMissing {
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
}
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.SharpeRatio.Round(4))
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.SortinoRatio.Round(4))
log.Infof(log.BackTester, "%s Information ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.InformationRatio.Round(4))
log.Infof(log.BackTester, "%s Calmar ratio: %v\n\n", sep, f.TotalUSDStatistics.GeometricRatios.CalmarRatio.Round(4))
return nil
return item, err
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
@@ -20,7 +21,10 @@ func TestCalculateFundingStatistics(t *testing.T) {
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
}
f := funding.SetupFundingManager(true, true)
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, true)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
item, err := funding.CreateItem("binance", asset.Spot, currency.BTC, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
@@ -76,7 +80,10 @@ func TestCalculateFundingStatistics(t *testing.T) {
t.Errorf("received %v expected %v", err, errNoRelevantStatsFound)
}
f = funding.SetupFundingManager(true, false)
f, err = funding.SetupFundingManager(&engine.ExchangeManager{}, true, false)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
err = f.AddItem(item)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
@@ -127,9 +134,7 @@ func TestCalculateIndividualFundingStatistics(t *testing.T) {
{
USDValue: decimal.NewFromInt(1337),
},
{
USDValue: decimal.Zero,
},
{},
},
}
rs := []relatedCurrencyPairStatistics{
@@ -148,6 +153,8 @@ func TestCalculateIndividualFundingStatistics(t *testing.T) {
}
rs[0].stat = &CurrencyPairStatistic{}
ri.USDInitialFunds = decimal.NewFromInt(1000)
ri.USDFinalFunds = decimal.NewFromInt(1337)
_, err = CalculateIndividualFundingStatistics(false, ri, rs)
if !errors.Is(err, errMissingSnapshots) {
t.Errorf("received %v expected %v", err, errMissingSnapshots)
@@ -174,6 +181,18 @@ func TestCalculateIndividualFundingStatistics(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
ri.Asset = asset.Futures
_, err = CalculateIndividualFundingStatistics(false, ri, rs)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
ri.IsCollateral = true
_, err = CalculateIndividualFundingStatistics(false, ri, rs)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
}
func TestFundingStatisticsPrintResults(t *testing.T) {
@@ -183,7 +202,10 @@ func TestFundingStatisticsPrintResults(t *testing.T) {
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
}
funds := funding.SetupFundingManager(true, true)
funds, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, true)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
item1, err := funding.CreateItem("test", asset.Spot, currency.BTC, decimal.NewFromInt(1337), decimal.NewFromFloat(0.04))
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)

View File

@@ -0,0 +1,383 @@
package statistics
import (
"fmt"
"sort"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
limit12 = 12
limit14 = 14
limit10 = 10
)
// addReason basic helper to append event reason if one is there
func addReason(reason, msg string) string {
if reason != "" {
msg += "\tReason: " + reason
}
return msg
}
// PrintTotalResults outputs all results to the CMD
func (s *Statistic) PrintTotalResults() {
log.Info(common.Statistics, common.ColourH1+"------------------Strategy-----------------------------------"+common.ColourDefault)
log.Infof(common.Statistics, "Strategy Name: %v", s.StrategyName)
log.Infof(common.Statistics, "Strategy Nickname: %v", s.StrategyNickname)
log.Infof(common.Statistics, "Strategy Goal: %v\n\n", s.StrategyGoal)
log.Info(common.Statistics, common.ColourH2+"------------------Total Results------------------------------"+common.ColourDefault)
log.Info(common.Statistics, common.ColourH3+"------------------Orders-------------------------------------"+common.ColourDefault)
log.Infof(common.Statistics, "Total buy orders: %v", convert.IntToHumanFriendlyString(s.TotalBuyOrders, ","))
log.Infof(common.Statistics, "Total sell orders: %v", convert.IntToHumanFriendlyString(s.TotalSellOrders, ","))
log.Infof(common.Statistics, "Total long orders: %v", convert.IntToHumanFriendlyString(s.TotalLongOrders, ","))
log.Infof(common.Statistics, "Total short orders: %v", convert.IntToHumanFriendlyString(s.TotalShortOrders, ","))
log.Infof(common.Statistics, "Total orders: %v\n\n", convert.IntToHumanFriendlyString(s.TotalOrders, ","))
if s.BiggestDrawdown != nil {
log.Info(common.Statistics, common.ColourH3+"------------------Biggest Drawdown-----------------------"+common.ColourDefault)
log.Infof(common.Statistics, "Exchange: %v Asset: %v Currency: %v", s.BiggestDrawdown.Exchange, s.BiggestDrawdown.Asset, s.BiggestDrawdown.Pair)
log.Infof(common.Statistics, "Highest Price: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Highest.Value, 8, ".", ","))
log.Infof(common.Statistics, "Highest Price Time: %v", s.BiggestDrawdown.MaxDrawdown.Highest.Time)
log.Infof(common.Statistics, "Lowest Price: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Lowest.Value, 8, ".", ","))
log.Infof(common.Statistics, "Lowest Price Time: %v", s.BiggestDrawdown.MaxDrawdown.Lowest.Time)
log.Infof(common.Statistics, "Calculated Drawdown: %s%%", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.DrawdownPercent, 2, ".", ","))
log.Infof(common.Statistics, "Difference: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Highest.Value.Sub(s.BiggestDrawdown.MaxDrawdown.Lowest.Value), 8, ".", ","))
log.Infof(common.Statistics, "Drawdown length: %v candles\n\n", convert.IntToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.IntervalDuration, ","))
}
if s.BestMarketMovement != nil && s.BestStrategyResults != nil {
log.Info(common.Statistics, common.ColourH4+"------------------Orders----------------------------------"+common.ColourDefault)
log.Infof(common.Statistics, "Best performing market movement: %v %v %v %v%%", s.BestMarketMovement.Exchange, s.BestMarketMovement.Asset, s.BestMarketMovement.Pair, convert.DecimalToHumanFriendlyString(s.BestMarketMovement.MarketMovement, 2, ".", ","))
log.Infof(common.Statistics, "Best performing strategy movement: %v %v %v %v%%\n\n", s.BestStrategyResults.Exchange, s.BestStrategyResults.Asset, s.BestStrategyResults.Pair, convert.DecimalToHumanFriendlyString(s.BestStrategyResults.StrategyMovement, 2, ".", ","))
}
}
// PrintAllEventsChronologically outputs all event details in the CMD
// rather than separated by exchange, asset and currency pair, it's
// grouped by time to allow a clearer picture of events
func (s *Statistic) PrintAllEventsChronologically() {
var results []eventOutputHolder
log.Info(common.Statistics, common.ColourH1+"------------------Events-------------------------------------"+common.ColourDefault)
var errs gctcommon.Errors
colour := common.ColourDefault
for exch, x := range s.ExchangeAssetPairStatistics {
for a, y := range x {
for pair, currencyStatistic := range y {
for i := range currencyStatistic.Events {
switch {
case currencyStatistic.Events[i].FillEvent != nil:
direction := currencyStatistic.Events[i].FillEvent.GetDirection()
if direction == order.CouldNotBuy ||
direction == order.CouldNotSell ||
direction == order.MissingData ||
direction == order.DoNothing ||
direction == order.TransferredFunds ||
direction == order.UnknownSide {
if direction == order.DoNothing {
colour = common.ColourDarkGrey
}
msg := fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection: %v",
currencyStatistic.Events[i].FillEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].FillEvent.Pair().String(), limit14),
currencyStatistic.Events[i].FillEvent.GetClosePrice().Round(8),
currencyStatistic.Events[i].FillEvent.GetDirection())
msg = addReason(currencyStatistic.Events[i].FillEvent.GetConcatReasons(), msg)
msg += common.ColourDefault
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(), msg)
} else {
// successful order!
colour = common.ColourSuccess
if currencyStatistic.Events[i].FillEvent.IsLiquidated() {
colour = common.ColourError
}
msg := fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection %v\tOrder placed: Amount: %v\tFee: %v\tTotal: %v",
currencyStatistic.Events[i].FillEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].FillEvent.Pair().String(), limit14),
currencyStatistic.Events[i].FillEvent.GetPurchasePrice().Round(8),
currencyStatistic.Events[i].FillEvent.GetDirection(),
currencyStatistic.Events[i].FillEvent.GetAmount().Round(8),
currencyStatistic.Events[i].FillEvent.GetExchangeFee(),
currencyStatistic.Events[i].FillEvent.GetTotal().Round(8))
msg = addReason(currencyStatistic.Events[i].FillEvent.GetConcatReasons(), msg)
msg += common.ColourDefault
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(), msg)
}
case currencyStatistic.Events[i].SignalEvent != nil:
msg := fmt.Sprintf("%v %v%v%v| Price: $%v",
currencyStatistic.Events[i].SignalEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].SignalEvent.Pair().String(), limit14),
currencyStatistic.Events[i].SignalEvent.GetClosePrice().Round(8))
msg = addReason(currencyStatistic.Events[i].SignalEvent.GetConcatReasons(), msg)
msg += common.ColourDefault
results = addEventOutputToTime(results, currencyStatistic.Events[i].SignalEvent.GetTime(), msg)
case currencyStatistic.Events[i].DataEvent != nil:
msg := fmt.Sprintf("%v %v%v%v| Price: $%v",
currencyStatistic.Events[i].DataEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].DataEvent.Pair().String(), limit14),
currencyStatistic.Events[i].DataEvent.GetClosePrice().Round(8))
msg = addReason(currencyStatistic.Events[i].DataEvent.GetConcatReasons(), msg)
msg += common.ColourDefault
results = addEventOutputToTime(results, currencyStatistic.Events[i].DataEvent.GetTime(), msg)
default:
errs = append(errs, fmt.Errorf(common.ColourError+"%v%v%v unexpected data received %+v"+common.ColourDefault, exch, a, fSIL(pair.String(), limit14), currencyStatistic.Events[i]))
}
}
}
}
}
sort.Slice(results, func(i, j int) bool {
b1 := results[i]
b2 := results[j]
return b1.Time.Before(b2.Time)
})
for i := range results {
for j := range results[i].Events {
log.Info(common.Statistics, results[i].Events[j])
}
}
if len(errs) > 0 {
log.Info(common.Statistics, common.ColourError+"------------------Errors-------------------------------------"+common.ColourDefault)
for i := range errs {
log.Error(common.Statistics, errs[i].Error())
}
}
}
// PrintResults outputs all calculated statistics to the command line
func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.Pair, usingExchangeLevelFunding bool) {
var errs gctcommon.Errors
sort.Slice(c.Events, func(i, j int) bool {
return c.Events[i].Time.Before(c.Events[j].Time)
})
last := c.Events[len(c.Events)-1]
first := c.Events[0]
c.StartingClosePrice.Value = first.DataEvent.GetClosePrice()
c.StartingClosePrice.Time = first.Time
c.EndingClosePrice.Value = last.DataEvent.GetClosePrice()
c.EndingClosePrice.Time = last.Time
c.TotalOrders = c.BuyOrders + c.SellOrders + c.ShortOrders + c.LongOrders
last.Holdings.TotalValueLost = last.Holdings.TotalValueLostToSlippage.Add(last.Holdings.TotalValueLostToVolumeSizing)
sep := fmt.Sprintf("%v %v %v |\t", fSIL(e, limit12), fSIL(a.String(), limit10), fSIL(p.String(), limit14))
currStr := fmt.Sprintf(common.ColourH1+"------------------Stats for %v %v %v------------------------------------------------------"+common.ColourDefault, e, a, p)
log.Infof(common.CurrencyStatistics, currStr[:70])
if a.IsFutures() {
log.Infof(common.CurrencyStatistics, "%s Long orders: %s", sep, convert.IntToHumanFriendlyString(c.LongOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Short orders: %s", sep, convert.IntToHumanFriendlyString(c.ShortOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Highest Unrealised PNL: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestUnrealisedPNL.Value, 8, ".", ","), c.HighestUnrealisedPNL.Time)
log.Infof(common.CurrencyStatistics, "%s Lowest Unrealised PNL: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.LowestUnrealisedPNL.Value, 8, ".", ","), c.LowestUnrealisedPNL.Time)
log.Infof(common.CurrencyStatistics, "%s Highest Realised PNL: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestRealisedPNL.Value, 8, ".", ","), c.HighestRealisedPNL.Time)
log.Infof(common.CurrencyStatistics, "%s Lowest Realised PNL: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.LowestRealisedPNL.Value, 8, ".", ","), c.LowestRealisedPNL.Time)
log.Infof(common.CurrencyStatistics, "%s Highest committed funds: %s %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestCommittedFunds.Value, 8, ".", ","), c.UnderlyingPair.Quote, c.HighestCommittedFunds.Time)
} else {
log.Infof(common.CurrencyStatistics, "%s Buy orders: %s", sep, convert.IntToHumanFriendlyString(c.BuyOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Buy amount: %s %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BoughtAmount, 8, ".", ","), last.Holdings.Pair.Base)
log.Infof(common.CurrencyStatistics, "%s Sell orders: %s", sep, convert.IntToHumanFriendlyString(c.SellOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Sell amount: %s %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.SoldAmount, 8, ".", ","), last.Holdings.Pair.Base)
log.Infof(common.CurrencyStatistics, "%s Highest committed funds: %s %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestCommittedFunds.Value, 8, ".", ","), last.Holdings.Pair.Quote, c.HighestCommittedFunds.Time)
}
log.Infof(common.CurrencyStatistics, "%s Total orders: %s", sep, convert.IntToHumanFriendlyString(c.TotalOrders, ","))
log.Info(common.CurrencyStatistics, common.ColourH2+"------------------Max Drawdown-------------------------------"+common.ColourDefault)
log.Infof(common.CurrencyStatistics, "%s Highest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value, 8, ".", ","), c.MaxDrawdown.Highest.Time)
log.Infof(common.CurrencyStatistics, "%s Lowest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Lowest.Value, 8, ".", ","), c.MaxDrawdown.Lowest.Time)
log.Infof(common.CurrencyStatistics, "%s Calculated Drawdown: %s%%", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.DrawdownPercent, 8, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Difference: %s", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value.Sub(c.MaxDrawdown.Lowest.Value), 2, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Drawdown length: %s", sep, convert.IntToHumanFriendlyString(c.MaxDrawdown.IntervalDuration, ","))
if !usingExchangeLevelFunding {
log.Info(common.CurrencyStatistics, common.ColourH2+"------------------Ratios------------------------------------------------"+common.ColourDefault)
log.Info(common.CurrencyStatistics, common.ColourH3+"------------------Rates-------------------------------------------------"+common.ColourDefault)
log.Infof(common.CurrencyStatistics, "%s Compound Annual Growth Rate: %s", sep, convert.DecimalToHumanFriendlyString(c.CompoundAnnualGrowthRate, 2, ".", ","))
log.Info(common.CurrencyStatistics, common.ColourH4+"------------------Arithmetic--------------------------------------------"+common.ColourDefault)
if c.ShowMissingDataWarning {
log.Infoln(common.CurrencyStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.CurrencyStatistics, "Ratio calculations will be skewed")
}
log.Infof(common.CurrencyStatistics, "%s Sharpe ratio: %v", sep, c.ArithmeticRatios.SharpeRatio.Round(4))
log.Infof(common.CurrencyStatistics, "%s Sortino ratio: %v", sep, c.ArithmeticRatios.SortinoRatio.Round(4))
log.Infof(common.CurrencyStatistics, "%s Information ratio: %v", sep, c.ArithmeticRatios.InformationRatio.Round(4))
log.Infof(common.CurrencyStatistics, "%s Calmar ratio: %v", sep, c.ArithmeticRatios.CalmarRatio.Round(4))
log.Info(common.CurrencyStatistics, common.ColourH4+"------------------Geometric--------------------------------------------"+common.ColourDefault)
if c.ShowMissingDataWarning {
log.Infoln(common.CurrencyStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.CurrencyStatistics, "Ratio calculations will be skewed")
}
log.Infof(common.CurrencyStatistics, "%s Sharpe ratio: %v", sep, c.GeometricRatios.SharpeRatio.Round(4))
log.Infof(common.CurrencyStatistics, "%s Sortino ratio: %v", sep, c.GeometricRatios.SortinoRatio.Round(4))
log.Infof(common.CurrencyStatistics, "%s Information ratio: %v", sep, c.GeometricRatios.InformationRatio.Round(4))
log.Infof(common.CurrencyStatistics, "%s Calmar ratio: %v", sep, c.GeometricRatios.CalmarRatio.Round(4))
}
log.Info(common.CurrencyStatistics, common.ColourH2+"------------------Results------------------------------------"+common.ColourDefault)
log.Infof(common.CurrencyStatistics, "%s Starting Close Price: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.StartingClosePrice.Value, 8, ".", ","), c.StartingClosePrice.Time)
log.Infof(common.CurrencyStatistics, "%s Finishing Close Price: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.EndingClosePrice.Value, 8, ".", ","), c.EndingClosePrice.Time)
log.Infof(common.CurrencyStatistics, "%s Lowest Close Price: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.LowestClosePrice.Value, 8, ".", ","), c.LowestClosePrice.Time)
log.Infof(common.CurrencyStatistics, "%s Highest Close Price: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestClosePrice.Value, 8, ".", ","), c.HighestClosePrice.Time)
log.Infof(common.CurrencyStatistics, "%s Market movement: %s%%", sep, convert.DecimalToHumanFriendlyString(c.MarketMovement, 2, ".", ","))
if !usingExchangeLevelFunding {
log.Infof(common.CurrencyStatistics, "%s Strategy movement: %s%%", sep, convert.DecimalToHumanFriendlyString(c.StrategyMovement, 2, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Did it beat the market: %v", sep, c.StrategyMovement.GreaterThan(c.MarketMovement))
}
log.Infof(common.CurrencyStatistics, "%s Value lost to volume sizing: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLostToVolumeSizing, 2, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Value lost to slippage: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLostToSlippage, 2, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Total Value lost: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLost, 2, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Total Fees: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalFees, 8, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Final holdings value: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalAssetValue, 8, ".", ","))
if !usingExchangeLevelFunding {
// the following have no direct translation to individual exchange level funds as they
// combine base and quote values
log.Infof(common.CurrencyStatistics, "%s Final funds: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.QuoteSize, 8, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Final holdings: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BaseSize, 8, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Final total value: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.TotalValue, 8, ".", ","))
}
if last.PNL != nil {
var unrealised, realised portfolio.BasicPNLResult
unrealised = last.PNL.GetUnrealisedPNL()
realised = last.PNL.GetRealisedPNL()
log.Infof(common.CurrencyStatistics, "%s Final Unrealised PNL: %s", sep, convert.DecimalToHumanFriendlyString(unrealised.PNL, 8, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Final Realised PNL: %s", sep, convert.DecimalToHumanFriendlyString(realised.PNL, 8, ".", ","))
}
if len(errs) > 0 {
log.Info(common.CurrencyStatistics, common.ColourError+"------------------Errors-------------------------------------"+common.ColourDefault)
for i := range errs {
log.Error(common.CurrencyStatistics, errs[i].Error())
}
}
}
// PrintResults outputs all calculated funding statistics to the command line
func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
if f.Report == nil {
return fmt.Errorf("%w requires report to be generated", common.ErrNilArguments)
}
var spotResults, futuresResults []FundingItemStatistics
for i := range f.Items {
if f.Items[i].ReportItem.Asset.IsFutures() {
futuresResults = append(futuresResults, f.Items[i])
} else {
spotResults = append(spotResults, f.Items[i])
}
}
if len(spotResults) > 0 || len(futuresResults) > 0 {
log.Info(common.FundingStatistics, common.ColourH1+"------------------Funding------------------------------------"+common.ColourDefault)
}
if len(spotResults) > 0 {
log.Info(common.FundingStatistics, common.ColourH2+"------------------Funding Spot Item Results------------------"+common.ColourDefault)
for i := range spotResults {
sep := fmt.Sprintf("%v%v%v| ", fSIL(spotResults[i].ReportItem.Exchange, limit12), fSIL(spotResults[i].ReportItem.Asset.String(), limit10), fSIL(spotResults[i].ReportItem.Currency.String(), limit14))
if !spotResults[i].ReportItem.PairedWith.IsEmpty() {
log.Infof(common.FundingStatistics, "%s Paired with: %v", sep, spotResults[i].ReportItem.PairedWith)
}
log.Infof(common.FundingStatistics, "%s Initial funds: %s", sep, convert.DecimalToHumanFriendlyString(spotResults[i].ReportItem.InitialFunds, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Final funds: %s", sep, convert.DecimalToHumanFriendlyString(spotResults[i].ReportItem.FinalFunds, 8, ".", ","))
if !f.Report.DisableUSDTracking && f.Report.UsingExchangeLevelFunding {
log.Infof(common.FundingStatistics, "%s Initial funds in USD: $%s", sep, convert.DecimalToHumanFriendlyString(spotResults[i].ReportItem.USDInitialFunds, 2, ".", ","))
log.Infof(common.FundingStatistics, "%s Final funds in USD: $%s", sep, convert.DecimalToHumanFriendlyString(spotResults[i].ReportItem.USDFinalFunds, 2, ".", ","))
}
if spotResults[i].ReportItem.ShowInfinite {
log.Infof(common.FundingStatistics, "%s Difference: ∞%%", sep)
} else {
log.Infof(common.FundingStatistics, "%s Difference: %s%%", sep, convert.DecimalToHumanFriendlyString(spotResults[i].ReportItem.Difference, 8, ".", ","))
}
if spotResults[i].ReportItem.TransferFee.GreaterThan(decimal.Zero) {
log.Infof(common.FundingStatistics, "%s Transfer fee: %s", sep, convert.DecimalToHumanFriendlyString(spotResults[i].ReportItem.TransferFee, 8, ".", ","))
}
if i != len(spotResults)-1 {
log.Info(common.FundingStatistics, "")
}
}
}
if len(futuresResults) > 0 {
log.Info(common.FundingStatistics, common.ColourH2+"------------------Funding Futures Item Results---------------"+common.ColourDefault)
for i := range futuresResults {
sep := fmt.Sprintf("%v%v%v| ", fSIL(futuresResults[i].ReportItem.Exchange, limit12), fSIL(futuresResults[i].ReportItem.Asset.String(), limit10), fSIL(futuresResults[i].ReportItem.Currency.String(), limit14))
log.Infof(common.FundingStatistics, "%s Is Collateral: %v", sep, futuresResults[i].IsCollateral)
if futuresResults[i].IsCollateral {
log.Infof(common.FundingStatistics, "%s Initial Collateral: %v %v at %v", sep, futuresResults[i].InitialCollateral.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].InitialCollateral.Time)
log.Infof(common.FundingStatistics, "%s Final Collateral: %v %v at %v", sep, futuresResults[i].FinalCollateral.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].FinalCollateral.Time)
log.Infof(common.FundingStatistics, "%s Lowest Collateral: %v %v at %v", sep, futuresResults[i].LowestCollateral.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].LowestCollateral.Time)
log.Infof(common.FundingStatistics, "%s Highest Collateral: %v %v at %v", sep, futuresResults[i].HighestCollateral.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].HighestCollateral.Time)
} else {
if !futuresResults[i].ReportItem.PairedWith.IsEmpty() {
log.Infof(common.FundingStatistics, "%s Collateral currency: %v", sep, futuresResults[i].ReportItem.PairedWith)
}
log.Infof(common.FundingStatistics, "%s Lowest Contract Holdings: %v %v at %v", sep, futuresResults[i].LowestHoldings.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].LowestHoldings.Time)
log.Infof(common.FundingStatistics, "%s Highest Contract Holdings: %v %v at %v", sep, futuresResults[i].HighestHoldings.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].HighestHoldings.Time)
log.Infof(common.FundingStatistics, "%s Initial Contract Holdings: %v %v at %v", sep, futuresResults[i].InitialHoldings.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].InitialHoldings.Time)
log.Infof(common.FundingStatistics, "%s Final Contract Holdings: %v %v at %v", sep, futuresResults[i].FinalHoldings.Value, futuresResults[i].ReportItem.Currency, futuresResults[i].FinalHoldings.Time)
}
if i != len(futuresResults)-1 {
log.Info(common.FundingStatistics, "")
}
}
}
if f.Report.DisableUSDTracking {
return nil
}
log.Info(common.FundingStatistics, common.ColourH2+"------------------USD Tracking Totals------------------------"+common.ColourDefault)
sep := "USD Tracking Total |\t"
log.Infof(common.FundingStatistics, "%s Initial value: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.InitialFunds, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Final value: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.FinalFunds, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Benchmark Market Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.BenchmarkMarketMovement, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Strategy Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.StrategyMovement, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Did strategy make a profit: %v", sep, f.TotalUSDStatistics.DidStrategyMakeProfit)
log.Infof(common.FundingStatistics, "%s Did strategy beat the benchmark: %v", sep, f.TotalUSDStatistics.DidStrategyBeatTheMarket)
log.Infof(common.FundingStatistics, "%s Highest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.HighestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.HighestHoldingValue.Time)
log.Infof(common.FundingStatistics, "%s Lowest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.LowestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.LowestHoldingValue.Time)
log.Info(common.FundingStatistics, common.ColourH3+"------------------Ratios------------------------------------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.ColourH4+"------------------Rates-------------------------------------------------"+common.ColourDefault)
log.Infof(common.FundingStatistics, "%s Risk free rate: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.RiskFreeRate.Mul(decimal.NewFromInt(100)), 2, ".", ","))
log.Infof(common.FundingStatistics, "%s Compound Annual Growth Rate: %v%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.CompoundAnnualGrowthRate, 8, ".", ","))
if f.TotalUSDStatistics.ArithmeticRatios == nil || f.TotalUSDStatistics.GeometricRatios == nil {
return fmt.Errorf("%w missing ratio calculations", common.ErrNilArguments)
}
log.Info(common.FundingStatistics, common.ColourH4+"------------------Arithmetic--------------------------------------------"+common.ColourDefault)
if wasAnyDataMissing {
log.Infoln(common.FundingStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.FundingStatistics, "Ratio calculations will be skewed")
}
log.Infof(common.FundingStatistics, "%s Sharpe ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.SharpeRatio.Round(4))
log.Infof(common.FundingStatistics, "%s Sortino ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.SortinoRatio.Round(4))
log.Infof(common.FundingStatistics, "%s Information ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.InformationRatio.Round(4))
log.Infof(common.FundingStatistics, "%s Calmar ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.CalmarRatio.Round(4))
log.Info(common.FundingStatistics, common.ColourH4+"------------------Geometric--------------------------------------------"+common.ColourDefault)
if wasAnyDataMissing {
log.Infoln(common.FundingStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.FundingStatistics, "Ratio calculations will be skewed")
}
log.Infof(common.FundingStatistics, "%s Sharpe ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.SharpeRatio.Round(4))
log.Infof(common.FundingStatistics, "%s Sortino ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.SortinoRatio.Round(4))
log.Infof(common.FundingStatistics, "%s Information ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.InformationRatio.Round(4))
log.Infof(common.FundingStatistics, "%s Calmar ratio: %v\n\n", sep, f.TotalUSDStatistics.GeometricRatios.CalmarRatio.Round(4))
return nil
}

View File

@@ -2,24 +2,18 @@ package statistics
import (
"encoding/json"
"errors"
"fmt"
"sort"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -39,22 +33,26 @@ func (s *Statistic) SetupEventForTime(ev common.DataEventHandler) error {
s.setupMap(ex, a)
lookup := s.ExchangeAssetPairStatistics[ex][a][p]
if lookup == nil {
lookup = &CurrencyPairStatistic{}
lookup = &CurrencyPairStatistic{
Exchange: ev.GetExchange(),
Asset: ev.GetAssetType(),
Currency: ev.Pair(),
UnderlyingPair: ev.GetUnderlyingPair(),
}
}
for i := range lookup.Events {
if lookup.Events[i].DataEvent.GetTime().Equal(ev.GetTime()) &&
lookup.Events[i].DataEvent.GetExchange() == ev.GetExchange() &&
lookup.Events[i].DataEvent.GetAssetType() == ev.GetAssetType() &&
lookup.Events[i].DataEvent.Pair().Equal(ev.Pair()) &&
lookup.Events[i].DataEvent.GetOffset() == ev.GetOffset() {
if lookup.Events[i].Offset == ev.GetOffset() {
return ErrAlreadyProcessed
}
}
lookup.Events = append(lookup.Events,
EventStore{
DataAtOffset{
DataEvent: ev,
Offset: ev.GetOffset(),
Time: ev.GetTime(),
},
)
s.ExchangeAssetPairStatistics[ex][a][p] = lookup
return nil
@@ -89,12 +87,12 @@ func (s *Statistic) SetEventForOffset(ev common.EventHandler) error {
return fmt.Errorf("%w for %v %v %v to set signal event", errCurrencyStatisticsUnset, exch, a, p)
}
for i := len(lookup.Events) - 1; i >= 0; i-- {
if lookup.Events[i].DataEvent.GetOffset() == offset {
if lookup.Events[i].Offset == offset {
return applyEventAtOffset(ev, lookup, i)
}
}
return nil
return fmt.Errorf("%w for event %v %v %v at offset %v", errNoRelevantStatsFound, exch, a, p, ev.GetOffset())
}
func applyEventAtOffset(ev common.EventHandler, lookup *CurrencyPairStatistic, i int) error {
@@ -110,6 +108,10 @@ func applyEventAtOffset(ev common.EventHandler, lookup *CurrencyPairStatistic, i
default:
return fmt.Errorf("unknown event type received: %v", ev)
}
lookup.Events[i].Time = ev.GetTime()
lookup.Events[i].ClosePrice = ev.GetClosePrice()
lookup.Events[i].Offset = ev.GetOffset()
return nil
}
@@ -123,12 +125,34 @@ func (s *Statistic) AddHoldingsForTime(h *holdings.Holding) error {
return fmt.Errorf("%w for %v %v %v to set holding event", errCurrencyStatisticsUnset, h.Exchange, h.Asset, h.Pair)
}
for i := len(lookup.Events) - 1; i >= 0; i-- {
if lookup.Events[i].DataEvent.GetOffset() == h.Offset {
if lookup.Events[i].Offset == h.Offset {
lookup.Events[i].Holdings = *h
break
return nil
}
}
return nil
return fmt.Errorf("%v %v %v %w %v", h.Exchange, h.Asset, h.Pair, errNoDataAtOffset, h.Offset)
}
// AddPNLForTime stores PNL data for tracking purposes
func (s *Statistic) AddPNLForTime(pnl *portfolio.PNLSummary) error {
if pnl == nil {
return fmt.Errorf("%w requires PNL", common.ErrNilArguments)
}
if s.ExchangeAssetPairStatistics == nil {
return errExchangeAssetPairStatsUnset
}
lookup := s.ExchangeAssetPairStatistics[pnl.Exchange][pnl.Item][pnl.Pair]
if lookup == nil {
return fmt.Errorf("%w for %v %v %v to set pnl", errCurrencyStatisticsUnset, pnl.Exchange, pnl.Item, pnl.Pair)
}
for i := len(lookup.Events) - 1; i >= 0; i-- {
if lookup.Events[i].Offset == pnl.Offset {
lookup.Events[i].PNL = pnl
lookup.Events[i].Holdings.BaseSize = pnl.Result.Exposure
return nil
}
}
return fmt.Errorf("%v %v %v %w %v", pnl.Exchange, pnl.Item, pnl.Pair, errNoDataAtOffset, pnl.Offset)
}
// AddComplianceSnapshotForTime adds the compliance snapshot to the statistics at the time period
@@ -147,19 +171,18 @@ func (s *Statistic) AddComplianceSnapshotForTime(c compliance.Snapshot, e fill.E
return fmt.Errorf("%w for %v %v %v to set compliance snapshot", errCurrencyStatisticsUnset, exch, a, p)
}
for i := len(lookup.Events) - 1; i >= 0; i-- {
if lookup.Events[i].DataEvent.GetOffset() == e.GetOffset() {
if lookup.Events[i].Offset == e.GetOffset() {
lookup.Events[i].Transactions = c
break
return nil
}
}
return nil
return fmt.Errorf("%v %v %v %w %v", e.GetExchange(), e.GetAssetType(), e.Pair(), errNoDataAtOffset, e.GetOffset())
}
// CalculateAllResults calculates the statistics of all exchange asset pair holdings,
// orders, ratios and drawdowns
func (s *Statistic) CalculateAllResults() error {
log.Info(log.BackTester, "calculating backtesting results")
log.Info(common.Statistics, "calculating backtesting results")
s.PrintAllEventsChronologically()
currCount := 0
var finalResults []FinalResultsHolder
@@ -169,16 +192,19 @@ func (s *Statistic) CalculateAllResults() error {
for pair, stats := range assetMap {
currCount++
last := stats.Events[len(stats.Events)-1]
if last.PNL != nil {
s.HasCollateral = true
}
err = stats.CalculateResults(s.RiskFreeRate)
if err != nil {
log.Error(log.BackTester, err)
log.Error(common.Statistics, err)
}
stats.PrintResults(exchangeName, assetItem, pair, s.FundManager.IsUsingExchangeLevelFunding())
stats.FinalHoldings = last.Holdings
stats.InitialHoldings = stats.Events[0].Holdings
stats.FinalOrders = last.Transactions
s.StartDate = stats.Events[0].DataEvent.GetTime()
s.EndDate = last.DataEvent.GetTime()
s.StartDate = stats.Events[0].Time
s.EndDate = last.Time
stats.PrintResults(exchangeName, assetItem, pair, s.FundManager.IsUsingExchangeLevelFunding())
finalResults = append(finalResults, FinalResultsHolder{
Exchange: exchangeName,
@@ -188,8 +214,11 @@ func (s *Statistic) CalculateAllResults() error {
MarketMovement: stats.MarketMovement,
StrategyMovement: stats.StrategyMovement,
})
s.TotalLongOrders += stats.LongOrders
s.TotalShortOrders += stats.ShortOrders
s.TotalBuyOrders += stats.BuyOrders
s.TotalSellOrders += stats.SellOrders
s.TotalOrders += stats.TotalOrders
if stats.ShowMissingDataWarning {
s.WasAnyDataMissing = true
}
@@ -204,8 +233,6 @@ func (s *Statistic) CalculateAllResults() error {
if err != nil {
return err
}
s.TotalOrders = s.TotalBuyOrders + s.TotalSellOrders
if currCount > 1 {
s.BiggestDrawdown = s.GetTheBiggestDrawdownAcrossCurrencies(finalResults)
s.BestMarketMovement = s.GetBestMarketPerformer(finalResults)
@@ -216,48 +243,16 @@ func (s *Statistic) CalculateAllResults() error {
return nil
}
// PrintTotalResults outputs all results to the CMD
func (s *Statistic) PrintTotalResults() {
log.Info(log.BackTester, "------------------Strategy-----------------------------------")
log.Infof(log.BackTester, "Strategy Name: %v", s.StrategyName)
log.Infof(log.BackTester, "Strategy Nickname: %v", s.StrategyNickname)
log.Infof(log.BackTester, "Strategy Goal: %v\n\n", s.StrategyGoal)
log.Info(log.BackTester, "------------------Total Results------------------------------")
log.Info(log.BackTester, "------------------Orders-------------------------------------")
log.Infof(log.BackTester, "Total buy orders: %v", convert.IntToHumanFriendlyString(s.TotalBuyOrders, ","))
log.Infof(log.BackTester, "Total sell orders: %v", convert.IntToHumanFriendlyString(s.TotalSellOrders, ","))
log.Infof(log.BackTester, "Total orders: %v\n\n", convert.IntToHumanFriendlyString(s.TotalOrders, ","))
if s.BiggestDrawdown != nil {
log.Info(log.BackTester, "------------------Biggest Drawdown-----------------------")
log.Infof(log.BackTester, "Exchange: %v Asset: %v Currency: %v", s.BiggestDrawdown.Exchange, s.BiggestDrawdown.Asset, s.BiggestDrawdown.Pair)
log.Infof(log.BackTester, "Highest Price: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Highest.Value, 8, ".", ","))
log.Infof(log.BackTester, "Highest Price Time: %v", s.BiggestDrawdown.MaxDrawdown.Highest.Time)
log.Infof(log.BackTester, "Lowest Price: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Lowest.Value, 8, ".", ","))
log.Infof(log.BackTester, "Lowest Price Time: %v", s.BiggestDrawdown.MaxDrawdown.Lowest.Time)
log.Infof(log.BackTester, "Calculated Drawdown: %s%%", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.DrawdownPercent, 2, ".", ","))
log.Infof(log.BackTester, "Difference: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Highest.Value.Sub(s.BiggestDrawdown.MaxDrawdown.Lowest.Value), 8, ".", ","))
log.Infof(log.BackTester, "Drawdown length: %v\n\n", convert.IntToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.IntervalDuration, ","))
}
if s.BestMarketMovement != nil && s.BestStrategyResults != nil {
log.Info(log.BackTester, "------------------Orders----------------------------------")
log.Infof(log.BackTester, "Best performing market movement: %v %v %v %v%%", s.BestMarketMovement.Exchange, s.BestMarketMovement.Asset, s.BestMarketMovement.Pair, convert.DecimalToHumanFriendlyString(s.BestMarketMovement.MarketMovement, 2, ".", ","))
log.Infof(log.BackTester, "Best performing strategy movement: %v %v %v %v%%\n\n", s.BestStrategyResults.Exchange, s.BestStrategyResults.Asset, s.BestStrategyResults.Pair, convert.DecimalToHumanFriendlyString(s.BestStrategyResults.StrategyMovement, 2, ".", ","))
}
}
// GetBestMarketPerformer returns the best final market movement
func (s *Statistic) GetBestMarketPerformer(results []FinalResultsHolder) *FinalResultsHolder {
result := &FinalResultsHolder{}
var result FinalResultsHolder
for i := range results {
if results[i].MarketMovement.GreaterThan(result.MarketMovement) || result.MarketMovement.IsZero() {
result = &results[i]
break
result = results[i]
}
}
return result
return &result
}
// GetBestStrategyPerformer returns the best performing strategy result
@@ -295,94 +290,6 @@ func addEventOutputToTime(events []eventOutputHolder, t time.Time, message strin
return events
}
// PrintAllEventsChronologically outputs all event details in the CMD
// rather than separated by exchange, asset and currency pair, it's
// grouped by time to allow a clearer picture of events
func (s *Statistic) PrintAllEventsChronologically() {
var results []eventOutputHolder
log.Info(log.BackTester, "------------------Events-------------------------------------")
var errs gctcommon.Errors
for exch, x := range s.ExchangeAssetPairStatistics {
for a, y := range x {
for pair, currencyStatistic := range y {
for i := range currencyStatistic.Events {
switch {
case currencyStatistic.Events[i].FillEvent != nil:
direction := currencyStatistic.Events[i].FillEvent.GetDirection()
if direction == gctorder.CouldNotBuy ||
direction == gctorder.CouldNotSell ||
direction == gctorder.DoNothing ||
direction == gctorder.MissingData ||
direction == gctorder.TransferredFunds ||
direction == gctorder.UnknownSide {
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(),
fmt.Sprintf("%v %v %v %v | Price: $%v - Direction: %v - Reason: %s",
currencyStatistic.Events[i].FillEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
currencyStatistic.Events[i].FillEvent.GetExchange(),
currencyStatistic.Events[i].FillEvent.GetAssetType(),
currencyStatistic.Events[i].FillEvent.Pair(),
currencyStatistic.Events[i].FillEvent.GetClosePrice().Round(8),
currencyStatistic.Events[i].FillEvent.GetDirection(),
currencyStatistic.Events[i].FillEvent.GetReason()))
} else {
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(),
fmt.Sprintf("%v %v %v %v | Price: $%v - Amount: %v - Fee: $%v - Total: $%v - Direction %v - Reason: %s",
currencyStatistic.Events[i].FillEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
currencyStatistic.Events[i].FillEvent.GetExchange(),
currencyStatistic.Events[i].FillEvent.GetAssetType(),
currencyStatistic.Events[i].FillEvent.Pair(),
currencyStatistic.Events[i].FillEvent.GetPurchasePrice().Round(8),
currencyStatistic.Events[i].FillEvent.GetAmount().Round(8),
currencyStatistic.Events[i].FillEvent.GetExchangeFee().Round(8),
currencyStatistic.Events[i].FillEvent.GetTotal().Round(8),
currencyStatistic.Events[i].FillEvent.GetDirection(),
currencyStatistic.Events[i].FillEvent.GetReason(),
))
}
case currencyStatistic.Events[i].SignalEvent != nil:
results = addEventOutputToTime(results, currencyStatistic.Events[i].SignalEvent.GetTime(),
fmt.Sprintf("%v %v %v %v | Price: $%v - Reason: %v",
currencyStatistic.Events[i].SignalEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
currencyStatistic.Events[i].SignalEvent.GetExchange(),
currencyStatistic.Events[i].SignalEvent.GetAssetType(),
currencyStatistic.Events[i].SignalEvent.Pair(),
currencyStatistic.Events[i].SignalEvent.GetPrice().Round(8),
currencyStatistic.Events[i].SignalEvent.GetReason()))
case currencyStatistic.Events[i].DataEvent != nil:
results = addEventOutputToTime(results, currencyStatistic.Events[i].DataEvent.GetTime(),
fmt.Sprintf("%v %v %v %v | Price: $%v - Reason: %v",
currencyStatistic.Events[i].DataEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
currencyStatistic.Events[i].DataEvent.GetExchange(),
currencyStatistic.Events[i].DataEvent.GetAssetType(),
currencyStatistic.Events[i].DataEvent.Pair(),
currencyStatistic.Events[i].DataEvent.GetClosePrice().Round(8),
currencyStatistic.Events[i].DataEvent.GetReason()))
default:
errs = append(errs, fmt.Errorf("%v %v %v unexpected data received %+v", exch, a, pair, currencyStatistic.Events[i]))
}
}
}
}
}
sort.Slice(results, func(i, j int) bool {
b1 := results[i]
b2 := results[j]
return b1.Time.Before(b2.Time)
})
for i := range results {
for j := range results[i].Events {
log.Info(log.BackTester, results[i].Events[j])
}
}
if len(errs) > 0 {
log.Info(log.BackTester, "------------------Errors-------------------------------------")
for i := range errs {
log.Error(log.BackTester, errs[i].Error())
}
}
}
// SetStrategyName sets the name for statistical identification
func (s *Statistic) SetStrategyName(name string) {
s.StrategyName = name
@@ -397,101 +304,3 @@ func (s *Statistic) Serialise() (string, error) {
return string(resp), nil
}
// CalculateRatios creates arithmetic and geometric ratios from funding or currency pair data
func CalculateRatios(benchmarkRates, returnsPerCandle []decimal.Decimal, riskFreeRatePerCandle decimal.Decimal, maxDrawdown *Swing, logMessage string) (arithmeticStats, geometricStats *Ratios, err error) {
var arithmeticBenchmarkAverage, geometricBenchmarkAverage decimal.Decimal
arithmeticBenchmarkAverage, err = gctmath.DecimalArithmeticMean(benchmarkRates)
if err != nil {
return nil, nil, err
}
geometricBenchmarkAverage, err = gctmath.DecimalFinancialGeometricMean(benchmarkRates)
if err != nil {
return nil, nil, err
}
riskFreeRateForPeriod := riskFreeRatePerCandle.Mul(decimal.NewFromInt(int64(len(benchmarkRates))))
var arithmeticReturnsPerCandle, geometricReturnsPerCandle, arithmeticSharpe, arithmeticSortino,
arithmeticInformation, arithmeticCalmar, geomSharpe, geomSortino, geomInformation, geomCalmar decimal.Decimal
arithmeticReturnsPerCandle, err = gctmath.DecimalArithmeticMean(returnsPerCandle)
if err != nil {
return nil, nil, err
}
geometricReturnsPerCandle, err = gctmath.DecimalFinancialGeometricMean(returnsPerCandle)
if err != nil {
return nil, nil, err
}
arithmeticSharpe, err = gctmath.DecimalSharpeRatio(returnsPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
if err != nil {
return nil, nil, err
}
arithmeticSortino, err = gctmath.DecimalSortinoRatio(returnsPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
if errors.Is(err, gctmath.ErrInexactConversion) {
log.Warnf(log.BackTester, "%s funding arithmetic sortino ratio %v", logMessage, err)
} else {
return nil, nil, err
}
}
arithmeticInformation, err = gctmath.DecimalInformationRatio(returnsPerCandle, benchmarkRates, arithmeticReturnsPerCandle, arithmeticBenchmarkAverage)
if err != nil {
return nil, nil, err
}
arithmeticCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, arithmeticReturnsPerCandle, riskFreeRateForPeriod)
if err != nil {
return nil, nil, err
}
arithmeticStats = &Ratios{}
if !arithmeticSharpe.IsZero() {
arithmeticStats.SharpeRatio = arithmeticSharpe
}
if !arithmeticSortino.IsZero() {
arithmeticStats.SortinoRatio = arithmeticSortino
}
if !arithmeticInformation.IsZero() {
arithmeticStats.InformationRatio = arithmeticInformation
}
if !arithmeticCalmar.IsZero() {
arithmeticStats.CalmarRatio = arithmeticCalmar
}
geomSharpe, err = gctmath.DecimalSharpeRatio(returnsPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
if err != nil {
return nil, nil, err
}
geomSortino, err = gctmath.DecimalSortinoRatio(returnsPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
if errors.Is(err, gctmath.ErrInexactConversion) {
log.Warnf(log.BackTester, "%s geometric sortino ratio %v", logMessage, err)
} else {
return nil, nil, err
}
}
geomInformation, err = gctmath.DecimalInformationRatio(returnsPerCandle, benchmarkRates, geometricReturnsPerCandle, geometricBenchmarkAverage)
if err != nil {
return nil, nil, err
}
geomCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, geometricReturnsPerCandle, riskFreeRateForPeriod)
if err != nil {
return nil, nil, err
}
geometricStats = &Ratios{}
if !arithmeticSharpe.IsZero() {
geometricStats.SharpeRatio = geomSharpe
}
if !arithmeticSortino.IsZero() {
geometricStats.SortinoRatio = geomSortino
}
if !arithmeticInformation.IsZero() {
geometricStats.InformationRatio = geomInformation
}
if !arithmeticCalmar.IsZero() {
geometricStats.CalmarRatio = geomCalmar
}
return arithmeticStats, geometricStats, nil
}

View File

@@ -15,7 +15,9 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -53,7 +55,7 @@ func TestAddDataEventForTime(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
@@ -94,19 +96,20 @@ func TestAddSignalEventForTime(t *testing.T) {
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
err = s.SetEventForOffset(&signal.Signal{})
b := &event.Base{}
err = s.SetEventForOffset(&signal.Signal{
Base: b,
})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
}
b.Exchange = exch
b.Time = tt
b.Interval = gctkline.OneDay
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Base: b,
Open: eleet,
Close: eleet,
Low: eleet,
@@ -117,13 +120,7 @@ func TestAddSignalEventForTime(t *testing.T) {
t.Error(err)
}
err = s.SetEventForOffset(&signal.Signal{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Base: b,
ClosePrice: eleet,
Direction: gctorder.Buy,
})
@@ -149,19 +146,20 @@ func TestAddExchangeEventForTime(t *testing.T) {
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
err = s.SetEventForOffset(&order.Order{})
b := &event.Base{}
err = s.SetEventForOffset(&order.Order{
Base: b,
})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
}
b.Exchange = exch
b.Time = tt
b.Interval = gctkline.OneDay
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Base: b,
Open: eleet,
Close: eleet,
Low: eleet,
@@ -172,20 +170,14 @@ func TestAddExchangeEventForTime(t *testing.T) {
t.Error(err)
}
err = s.SetEventForOffset(&order.Order{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
ID: "elite",
Direction: gctorder.Buy,
Status: gctorder.New,
Price: eleet,
Amount: eleet,
OrderType: gctorder.Stop,
Leverage: eleet,
Base: b,
ID: "elite",
Direction: gctorder.Buy,
Status: gctorder.New,
ClosePrice: eleet,
Amount: eleet,
OrderType: gctorder.Stop,
Leverage: eleet,
})
if err != nil {
t.Error(err)
@@ -209,19 +201,22 @@ func TestAddFillEventForTime(t *testing.T) {
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
err = s.SetEventForOffset(&fill.Fill{})
b := &event.Base{}
err = s.SetEventForOffset(&fill.Fill{
Base: b,
})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
}
b.Exchange = exch
b.Time = tt
b.Interval = gctkline.OneDay
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Base: b,
Open: eleet,
Close: eleet,
Low: eleet,
@@ -232,13 +227,7 @@ func TestAddFillEventForTime(t *testing.T) {
t.Error(err)
}
err = s.SetEventForOffset(&fill.Fill{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Base: b,
Direction: gctorder.Buy,
Amount: eleet,
ClosePrice: eleet,
@@ -270,7 +259,7 @@ func TestAddHoldingsForTime(t *testing.T) {
}
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
@@ -295,14 +284,10 @@ func TestAddHoldingsForTime(t *testing.T) {
BaseSize: eleet,
BaseValue: eleet,
SoldAmount: eleet,
SoldValue: eleet,
BoughtAmount: eleet,
BoughtValue: eleet,
QuoteSize: eleet,
TotalValueDifference: eleet,
ChangeInTotalValuePercent: eleet,
BoughtValueDifference: eleet,
SoldValueDifference: eleet,
PositionsValueDifference: eleet,
TotalValue: eleet,
TotalFees: eleet,
@@ -333,19 +318,18 @@ func TestAddComplianceSnapshotForTime(t *testing.T) {
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
err = s.AddComplianceSnapshotForTime(compliance.Snapshot{}, &fill.Fill{})
b := &event.Base{}
err = s.AddComplianceSnapshotForTime(compliance.Snapshot{}, &fill.Fill{Base: b})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
}
b.Exchange = exch
b.Time = tt
b.Interval = gctkline.OneDay
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Base: b,
Open: eleet,
Close: eleet,
Low: eleet,
@@ -358,13 +342,7 @@ func TestAddComplianceSnapshotForTime(t *testing.T) {
err = s.AddComplianceSnapshotForTime(compliance.Snapshot{
Timestamp: tt,
}, &fill.Fill{
Base: event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Base: b,
})
if err != nil {
t.Error(err)
@@ -515,7 +493,7 @@ func TestPrintAllEventsChronologically(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
@@ -533,7 +511,7 @@ func TestPrintAllEventsChronologically(t *testing.T) {
}
err = s.SetEventForOffset(&fill.Fill{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
@@ -553,7 +531,7 @@ func TestPrintAllEventsChronologically(t *testing.T) {
}
err = s.SetEventForOffset(&signal.Signal{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
@@ -589,12 +567,13 @@ func TestCalculateTheResults(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
Offset: 1,
},
Open: eleet,
Close: eleet,
@@ -606,12 +585,13 @@ func TestCalculateTheResults(t *testing.T) {
t.Error(err)
}
err = s.SetEventForOffset(&signal.Signal{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
Offset: 1,
},
OpenPrice: eleet,
HighPrice: eleet,
@@ -624,12 +604,13 @@ func TestCalculateTheResults(t *testing.T) {
t.Error(err)
}
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p2,
AssetType: a,
Offset: 2,
},
Open: eleeb,
Close: eleeb,
@@ -642,12 +623,13 @@ func TestCalculateTheResults(t *testing.T) {
}
err = s.SetEventForOffset(&signal.Signal{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p2,
AssetType: a,
Offset: 2,
},
OpenPrice: eleet,
HighPrice: eleet,
@@ -661,12 +643,13 @@ func TestCalculateTheResults(t *testing.T) {
}
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt2,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
Offset: 3,
},
Open: eleeb,
Close: eleeb,
@@ -678,12 +661,13 @@ func TestCalculateTheResults(t *testing.T) {
t.Error(err)
}
err = s.SetEventForOffset(&signal.Signal{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt2,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
Offset: 3,
},
OpenPrice: eleeb,
HighPrice: eleeb,
@@ -697,12 +681,13 @@ func TestCalculateTheResults(t *testing.T) {
}
err = s.SetupEventForTime(&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt2,
Interval: gctkline.OneDay,
CurrencyPair: p2,
AssetType: a,
Offset: 4,
},
Open: eleeb,
Close: eleeb,
@@ -714,12 +699,13 @@ func TestCalculateTheResults(t *testing.T) {
t.Error(err)
}
err = s.SetEventForOffset(&signal.Signal{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt2,
Interval: gctkline.OneDay,
CurrencyPair: p2,
AssetType: a,
Offset: 4,
},
OpenPrice: eleeb,
HighPrice: eleeb,
@@ -737,7 +723,10 @@ func TestCalculateTheResults(t *testing.T) {
s.ExchangeAssetPairStatistics[exch][a][p2].Events[1].Holdings.QuoteInitialFunds = eleet
s.ExchangeAssetPairStatistics[exch][a][p2].Events[1].Holdings.TotalValue = eleeet
funds := funding.SetupFundingManager(false, false)
funds, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
pBase, err := funding.CreateItem(exch, a, p.Base, eleeet, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
@@ -781,7 +770,10 @@ func TestCalculateTheResults(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, errMissingSnapshots)
}
funds = funding.SetupFundingManager(false, true)
funds, err = funding.SetupFundingManager(&engine.ExchangeManager{}, false, true)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = funds.AddPair(pair)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
@@ -797,7 +789,7 @@ func TestCalculateTheResults(t *testing.T) {
}
}
func TestCalculateMaxDrawdown(t *testing.T) {
func TestCalculateBiggestEventDrawdown(t *testing.T) {
tt1 := time.Now().Add(-gctkline.OneDay.Duration() * 7).Round(gctkline.OneDay.Duration())
exch := testExchange
a := asset.Spot
@@ -805,7 +797,7 @@ func TestCalculateMaxDrawdown(t *testing.T) {
var events []common.DataEventHandler
for i := int64(0); i < 100; i++ {
tt1 = tt1.Add(gctkline.OneDay.Duration())
even := event.Base{
even := &event.Base{
Exchange: exch,
Time: tt1,
Interval: gctkline.OneDay,
@@ -831,7 +823,7 @@ func TestCalculateMaxDrawdown(t *testing.T) {
}
tt1 = tt1.Add(gctkline.OneDay.Duration())
even := event.Base{
even := &event.Base{
Exchange: exch,
Time: tt1,
Interval: gctkline.OneDay,
@@ -846,7 +838,7 @@ func TestCalculateMaxDrawdown(t *testing.T) {
})
tt1 = tt1.Add(gctkline.OneDay.Duration())
even = event.Base{
even = &event.Base{
Exchange: exch,
Time: tt1,
Interval: gctkline.OneDay,
@@ -861,7 +853,7 @@ func TestCalculateMaxDrawdown(t *testing.T) {
})
tt1 = tt1.Add(gctkline.OneDay.Duration())
even = event.Base{
even = &event.Base{
Exchange: exch,
Time: tt1,
Interval: gctkline.OneDay,
@@ -887,6 +879,24 @@ func TestCalculateMaxDrawdown(t *testing.T) {
if resp.Highest.Value != decimal.NewFromInt(1337) && !resp.Lowest.Value.Equal(decimal.NewFromInt(1238)) {
t.Error("unexpected max drawdown")
}
// bogus scenario
bogusEvent := []common.DataEventHandler{
&kline.Kline{
Base: &event.Base{
Exchange: exch,
CurrencyPair: p,
AssetType: a,
},
Close: decimal.NewFromInt(1339),
High: decimal.NewFromInt(1339),
Low: decimal.NewFromInt(1339),
},
}
_, err = CalculateBiggestEventDrawdown(bogusEvent)
if !errors.Is(err, gctcommon.ErrDateUnset) {
t.Errorf("received %v expected %v", err, gctcommon.ErrDateUnset)
}
}
func TestCalculateBiggestValueAtTimeDrawdown(t *testing.T) {

View File

@@ -6,6 +6,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
@@ -26,6 +27,7 @@ var (
errMissingSnapshots = errors.New("funding report item missing USD snapshots")
errNoRelevantStatsFound = errors.New("no relevant currency pair statistics found")
errReceivedNoData = errors.New("received no data")
errNoDataAtOffset = errors.New("no data found at offset")
)
// Statistic holds all statistical information for a backtester run, from drawdowns to ratios.
@@ -41,15 +43,17 @@ type Statistic struct {
RiskFreeRate decimal.Decimal `json:"risk-free-rate"`
ExchangeAssetPairStatistics map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic `json:"exchange-asset-pair-statistics"`
TotalBuyOrders int64 `json:"total-buy-orders"`
TotalLongOrders int64 `json:"total-long-orders"`
TotalShortOrders int64 `json:"total-short-orders"`
TotalSellOrders int64 `json:"total-sell-orders"`
TotalOrders int64 `json:"total-orders"`
BiggestDrawdown *FinalResultsHolder `json:"biggest-drawdown,omitempty"`
BestStrategyResults *FinalResultsHolder `json:"best-start-results,omitempty"`
BestMarketMovement *FinalResultsHolder `json:"best-market-movement,omitempty"`
CurrencyPairStatistics []CurrencyPairStatistic `json:"currency-pair-statistics"` // as ExchangeAssetPairStatistics cannot be rendered via json.Marshall, we append all result to this slice instead
WasAnyDataMissing bool `json:"was-any-data-missing"`
FundingStatistics *FundingStatistics `json:"funding-statistics"`
FundManager funding.IFundingManager `json:"-"`
HasCollateral bool `json:"has-collateral"`
}
// FinalResultsHolder holds important stats about a currency's performance
@@ -72,6 +76,7 @@ type Handler interface {
CalculateAllResults() error
Reset()
Serialise() (string, error)
AddPNLForTime(*portfolio.PNLSummary) error
}
// Results holds some statistics on results
@@ -113,33 +118,51 @@ type CurrencyStats interface {
SortinoRatio(decimal.Decimal) decimal.Decimal
}
// EventStore is used to hold all event information
// DataAtOffset is used to hold all event information
// at a time interval
type EventStore struct {
type DataAtOffset struct {
Offset int64
ClosePrice decimal.Decimal
Time time.Time
Holdings holdings.Holding
Transactions compliance.Snapshot
DataEvent common.DataEventHandler
SignalEvent signal.Event
OrderEvent order.Event
FillEvent fill.Event
PNL portfolio.IPNL
}
// CurrencyPairStatistic Holds all events and statistics relevant to an exchange, asset type and currency pair
type CurrencyPairStatistic struct {
Exchange string
Asset asset.Item
Currency currency.Pair
UnderlyingPair currency.Pair `json:"linked-spot-currency"`
ShowMissingDataWarning bool `json:"-"`
IsStrategyProfitable bool `json:"is-strategy-profitable"`
DoesPerformanceBeatTheMarket bool `json:"does-performance-beat-the-market"`
BuyOrders int64 `json:"buy-orders"`
LongOrders int64 `json:"long-orders"`
ShortOrders int64 `json:"short-orders"`
SellOrders int64 `json:"sell-orders"`
TotalOrders int64 `json:"total-orders"`
StartingClosePrice decimal.Decimal `json:"starting-close-price"`
EndingClosePrice decimal.Decimal `json:"ending-close-price"`
LowestClosePrice decimal.Decimal `json:"lowest-close-price"`
HighestClosePrice decimal.Decimal `json:"highest-close-price"`
StartingClosePrice ValueAtTime `json:"starting-close-price"`
EndingClosePrice ValueAtTime `json:"ending-close-price"`
LowestClosePrice ValueAtTime `json:"lowest-close-price"`
HighestClosePrice ValueAtTime `json:"highest-close-price"`
HighestUnrealisedPNL ValueAtTime `json:"highest-unrealised-pnl"`
LowestUnrealisedPNL ValueAtTime `json:"lowest-unrealised-pnl"`
HighestRealisedPNL ValueAtTime `json:"highest-realised-pnl"`
LowestRealisedPNL ValueAtTime `json:"lowest-realised-pnl"`
MarketMovement decimal.Decimal `json:"market-movement"`
StrategyMovement decimal.Decimal `json:"strategy-movement"`
UnrealisedPNL decimal.Decimal `json:"unrealised-pnl"`
RealisedPNL decimal.Decimal `json:"realised-pnl"`
CompoundAnnualGrowthRate decimal.Decimal `json:"compound-annual-growth-rate"`
TotalAssetValue decimal.Decimal
TotalFees decimal.Decimal
@@ -147,7 +170,7 @@ type CurrencyPairStatistic struct {
TotalValueLostToSlippage decimal.Decimal
TotalValueLost decimal.Decimal
Events []EventStore `json:"-"`
Events []DataAtOffset `json:"-"`
MaxDrawdown Swing `json:"max-drawdown,omitempty"`
HighestCommittedFunds ValueAtTime `json:"highest-committed-funds"`
@@ -178,6 +201,7 @@ type Swing struct {
type ValueAtTime struct {
Time time.Time `json:"time"`
Value decimal.Decimal `json:"value"`
Set bool `json:"-"`
}
type relatedCurrencyPairStatistics struct {
@@ -210,22 +234,28 @@ type FundingItemStatistics struct {
TotalOrders int64
MaxDrawdown Swing
HighestCommittedFunds ValueAtTime
// CollateralPair stats
IsCollateral bool
InitialCollateral ValueAtTime
FinalCollateral ValueAtTime
HighestCollateral ValueAtTime
LowestCollateral ValueAtTime
// Contracts
LowestHoldings ValueAtTime
HighestHoldings ValueAtTime
InitialHoldings ValueAtTime
FinalHoldings ValueAtTime
}
// TotalFundingStatistics holds values for overal statistics for funding items
// TotalFundingStatistics holds values for overall statistics for funding items
type TotalFundingStatistics struct {
HoldingValues []ValueAtTime
InitialHoldingValue ValueAtTime
FinalHoldingValue ValueAtTime
HighestHoldingValue ValueAtTime
LowestHoldingValue ValueAtTime
BenchmarkMarketMovement decimal.Decimal
StrategyMovement decimal.Decimal
RiskFreeRate decimal.Decimal
CompoundAnnualGrowthRate decimal.Decimal
BuyOrders int64
SellOrders int64
TotalOrders int64
MaxDrawdown Swing
GeometricRatios *Ratios
ArithmeticRatios *Ratios

View File

@@ -3,7 +3,6 @@ package base
import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
)
@@ -23,15 +22,7 @@ func (s *Strategy) GetBaseData(d data.Handler) (signal.Signal, error) {
return signal.Signal{}, common.ErrNilEvent
}
return signal.Signal{
Base: event.Base{
Offset: latest.GetOffset(),
Exchange: latest.GetExchange(),
Time: latest.GetTime(),
CurrencyPair: latest.Pair(),
AssetType: latest.GetAssetType(),
Interval: latest.GetInterval(),
Reason: latest.GetReason(),
},
Base: latest.GetBase(),
ClosePrice: latest.GetClosePrice(),
HighPrice: latest.GetHighPrice(),
OpenPrice: latest.GetOpenPrice(),

View File

@@ -17,6 +17,7 @@ import (
)
func TestGetBase(t *testing.T) {
t.Parallel()
s := Strategy{}
_, err := s.GetBaseData(nil)
if !errors.Is(err, common.ErrNilArguments) {
@@ -33,7 +34,7 @@ func TestGetBase(t *testing.T) {
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&kline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
@@ -59,6 +60,7 @@ func TestGetBase(t *testing.T) {
}
func TestSetSimultaneousProcessing(t *testing.T) {
t.Parallel()
s := Strategy{}
is := s.UsingSimultaneousProcessing()
if is {
@@ -70,3 +72,27 @@ func TestSetSimultaneousProcessing(t *testing.T) {
t.Error("expected true")
}
}
func TestUsingExchangeLevelFunding(t *testing.T) {
t.Parallel()
s := &Strategy{}
if s.UsingExchangeLevelFunding() {
t.Error("expected false")
}
s.usingExchangeLevelFunding = true
if !s.UsingExchangeLevelFunding() {
t.Error("expected true")
}
}
func TestSetExchangeLevelFunding(t *testing.T) {
t.Parallel()
s := &Strategy{}
s.SetExchangeLevelFunding(true)
if !s.UsingExchangeLevelFunding() {
t.Error("expected true")
}
if !s.UsingExchangeLevelFunding() {
t.Error("expected true")
}
}

View File

@@ -3,14 +3,16 @@ package base
import "errors"
var (
// ErrCustomSettingsUnsupported used when custom settings are found in the start config when they shouldn't be
// ErrCustomSettingsUnsupported used when custom settings are found in the strategy config when they shouldn't be
ErrCustomSettingsUnsupported = errors.New("custom settings not supported")
// ErrSimultaneousProcessingNotSupported used when strategy does not support simultaneous processing
// but start config is set to use it
// but strategy config is set to use it
ErrSimultaneousProcessingNotSupported = errors.New("does not support simultaneous processing and could not be loaded")
// ErrStrategyNotFound used when strategy specified in start config does not exist
ErrStrategyNotFound = errors.New("not found. Please ensure the strategy-settings field 'name' is spelled properly in your .start config")
// ErrInvalidCustomSettings used when bad custom settings are found in the start config
// ErrSimultaneousProcessingOnly is raised when a strategy is improperly configured
ErrSimultaneousProcessingOnly = errors.New("this strategy only supports simultaneous processing")
// ErrStrategyNotFound used when strategy specified in strategy config does not exist
ErrStrategyNotFound = errors.New("not found. Please ensure the strategy-settings field 'name' is spelled properly in your .strat config") // nolint:misspell // its shorthand for strategy
// ErrInvalidCustomSettings used when bad custom settings are found in the strategy config
ErrInvalidCustomSettings = errors.New("invalid custom settings in config")
// ErrTooMuchBadData used when there is too much missing data
ErrTooMuchBadData = errors.New("backtesting cannot continue as there is too much invalid data. Please review your dataset")

View File

@@ -1,8 +1,6 @@
package dollarcostaverage
import (
"fmt"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
@@ -37,7 +35,7 @@ func (s *Strategy) Description() string {
// OnSignal handles a data event and returns what action the strategy believes should occur
// For dollarcostaverage, this means returning a buy signal on every event
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) (signal.Event, error) {
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) (signal.Event, error) {
if d == nil {
return nil, common.ErrNilEvent
}
@@ -48,7 +46,7 @@ func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer, _ portfol
if !d.HasDataAtTime(d.Latest().GetTime()) {
es.SetDirection(order.MissingData)
es.AppendReason(fmt.Sprintf("missing data at %v, cannot perform any actions", d.Latest().GetTime()))
es.AppendReasonf("missing data at %v, cannot perform any actions", d.Latest().GetTime())
return &es, nil
}
@@ -66,7 +64,7 @@ func (s *Strategy) SupportsSimultaneousProcessing() bool {
// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
// For dollarcostaverage, the strategy is always "buy", so it uses the OnSignal function
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) ([]signal.Event, error) {
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) ([]signal.Event, error) {
var resp []signal.Event
var errs gctcommon.Errors
for i := range d {

View File

@@ -56,7 +56,7 @@ func TestOnSignal(t *testing.T) {
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: dInsert,
Interval: gctkline.OneDay,
@@ -134,7 +134,7 @@ func TestOnSignals(t *testing.T) {
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: event.Base{
Base: &event.Base{
Offset: 1,
Exchange: exch,
Time: dInsert,

View File

@@ -0,0 +1,68 @@
# GoCryptoTrader Backtester: Ftxcashandcarry package
<img src="/backtester/common/backtester.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/ftxcashandcarry)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This ftxcashandcarry package is part of the GoCryptoTrader codebase.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## FTX Cash and carry strategy overview
### Description
Cash and carry is a strategy which takes advantage of the difference in pricing between a long-dated futures contract and a SPOT asset.
By default, this cash and carry strategy will, upon the first data event, purchase BTC-USD SPOT asset from FTX exchange and then, once filled, raise a SHORT for BTC-20210924 FUTURES contract.
On the last event, the strategy will close the SHORT position by raising a LONG of the same contract amount, thereby netting the difference in prices
### Requirements
- At this time of writing, this strategy is only compatible with FTX
- This strategy *requires* `Simultaneous Signal Processing` aka [use-simultaneous-signal-processing](/backtester/config/README.md).
- This strategy *requires* `Exchange Level Funding` aka [use-exchange-level-funding](/backtester/config/README.md).
### Creating a strategy config
- The long-dated futures contract will need to be part of the `currency-settings` of the contract
- Funding for purchasing SPOT assets will need to be part of `funding-settings`
- See the [example config](./config/examples/ftx-cash-carry.strat)
### Customisation
This strategy does support strategy customisation in the following ways:
| Field | Description | Example |
| --- | ------- | --- |
| openShortDistancePercentage | If there is no short position open, and the difference between FUTURES and SPOT pricing goes above this this percentage threshold, raise a SHORT order of the FUTURES contract | 10 |
| closeShortDistancePercentage | If there is an open SHORT position on a FUTURES contract, and the difference in FUTURES and SPOT pricing goes below this percentage threshold, close the SHORT position | 1 |
### External Resources
- [This](https://ftxcashandcarry.com/) is a very informative site on describing what a cash and carry trade will look like
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
Please feel free to submit any pull requests or suggest any desired features to be added.
When submitting a PR, please abide by our coding guidelines:
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
+ Pull requests need to be based on and opened against the `master` branch.
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,228 @@
package ftxcashandcarry
import (
"errors"
"fmt"
"strings"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// Name returns the name of the strategy
func (s *Strategy) Name() string {
return Name
}
// Description describes the strategy
func (s *Strategy) Description() string {
return description
}
// OnSignal handles a data event and returns what action the strategy believes should occur
// For rsi, this means returning a buy signal when rsi is at or below a certain level, and a
// sell signal when it is at or above a certain level
func (s *Strategy) OnSignal(data.Handler, funding.IFundingTransferer, portfolio.Handler) (signal.Event, error) {
return nil, base.ErrSimultaneousProcessingOnly
}
// SupportsSimultaneousProcessing this strategy only supports simultaneous signal processing
func (s *Strategy) SupportsSimultaneousProcessing() bool {
return true
}
type cashCarrySignals struct {
spotSignal data.Handler
futureSignal data.Handler
}
var errNotSetup = errors.New("sent incomplete signals")
// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTransferer, p portfolio.Handler) ([]signal.Event, error) {
if len(d) == 0 {
return nil, errNoSignals
}
if f == nil {
return nil, fmt.Errorf("%w missing funding transferred", common.ErrNilArguments)
}
if p == nil {
return nil, fmt.Errorf("%w missing portfolio handler", common.ErrNilArguments)
}
var response []signal.Event
sortedSignals, err := sortSignals(d)
if err != nil {
return nil, err
}
for _, v := range sortedSignals {
pos, err := p.GetPositions(v.futureSignal.Latest())
if err != nil {
return nil, err
}
spotSignal, err := s.GetBaseData(v.spotSignal)
if err != nil {
return nil, err
}
futuresSignal, err := s.GetBaseData(v.futureSignal)
if err != nil {
return nil, err
}
spotSignal.SetDirection(order.DoNothing)
futuresSignal.SetDirection(order.DoNothing)
fp := v.futureSignal.Latest().GetClosePrice()
sp := v.spotSignal.Latest().GetClosePrice()
diffBetweenFuturesSpot := fp.Sub(sp).Div(sp).Mul(decimal.NewFromInt(100))
futuresSignal.AppendReasonf("Futures Spot Difference: %v%%", diffBetweenFuturesSpot)
if len(pos) > 0 && pos[len(pos)-1].Status == order.Open {
futuresSignal.AppendReasonf("Unrealised PNL: %v %v", pos[len(pos)-1].UnrealisedPNL, pos[len(pos)-1].CollateralCurrency)
}
if f.HasExchangeBeenLiquidated(&spotSignal) || f.HasExchangeBeenLiquidated(&futuresSignal) {
spotSignal.AppendReason("cannot transact, has been liquidated")
futuresSignal.AppendReason("cannot transact, has been liquidated")
response = append(response, &spotSignal, &futuresSignal)
continue
}
signals, err := s.createSignals(pos, &spotSignal, &futuresSignal, diffBetweenFuturesSpot, v.futureSignal.IsLastEvent())
if err != nil {
return nil, err
}
response = append(response, signals...)
}
return response, nil
}
// createSignals creates signals based on the relationships between
// futures and spot signals
func (s *Strategy) createSignals(pos []order.PositionStats, spotSignal, futuresSignal *signal.Signal, diffBetweenFuturesSpot decimal.Decimal, isLastEvent bool) ([]signal.Event, error) {
if spotSignal == nil {
return nil, fmt.Errorf("%w missing spot signal", common.ErrNilArguments)
}
if futuresSignal == nil {
return nil, fmt.Errorf("%w missing futures signal", common.ErrNilArguments)
}
var response []signal.Event
switch {
case len(pos) == 0,
pos[len(pos)-1].Status == order.Closed &&
diffBetweenFuturesSpot.GreaterThan(s.openShortDistancePercentage):
// check to see if order is appropriate to action
spotSignal.SetPrice(spotSignal.ClosePrice)
spotSignal.AppendReasonf("Signalling purchase of %v", spotSignal.Pair())
// first the spot purchase
spotSignal.SetDirection(order.Buy)
// second the futures purchase, using the newly acquired asset
// as collateral to short
futuresSignal.SetDirection(order.Short)
futuresSignal.SetPrice(futuresSignal.ClosePrice)
futuresSignal.AppendReason("Shorting to perform cash and carry")
futuresSignal.CollateralCurrency = spotSignal.CurrencyPair.Base
futuresSignal.MatchesOrderAmount = true
spotSignal.AppendReasonf("Signalling shorting of %v after spot order placed", futuresSignal.Pair())
// set the FillDependentEvent to use the futures signal
// as the futures signal relies on a completed spot order purchase
// to use as collateral
spotSignal.FillDependentEvent = futuresSignal
// only appending spotSignal as futuresSignal will be raised later
response = append(response, spotSignal)
case pos[len(pos)-1].Status == order.Open &&
isLastEvent:
// closing positions on last event
spotSignal.SetDirection(order.ClosePosition)
spotSignal.AppendReason("Selling asset on last event")
futuresSignal.SetDirection(order.ClosePosition)
futuresSignal.AppendReason("Closing position on last event")
response = append(response, futuresSignal, spotSignal)
case pos[len(pos)-1].Status == order.Open &&
diffBetweenFuturesSpot.LessThanOrEqual(s.closeShortDistancePercentage):
// closing positions when custom threshold met
spotSignal.SetDirection(order.ClosePosition)
spotSignal.AppendReasonf("Closing position. Met threshold of %v", s.closeShortDistancePercentage)
futuresSignal.SetDirection(order.ClosePosition)
futuresSignal.AppendReasonf("Closing position. Met threshold %v", s.closeShortDistancePercentage)
response = append(response, futuresSignal, spotSignal)
default:
response = append(response, spotSignal, futuresSignal)
}
return response, nil
}
// sortSignals links spot and futures signals in order to create cash
// and carry signals
func sortSignals(d []data.Handler) (map[currency.Pair]cashCarrySignals, error) {
if len(d) == 0 {
return nil, errNoSignals
}
var response = make(map[currency.Pair]cashCarrySignals, len(d))
for i := range d {
l := d[i].Latest()
if !strings.EqualFold(l.GetExchange(), exchangeName) {
return nil, fmt.Errorf("%w, received '%v'", errOnlyFTXSupported, l.GetExchange())
}
a := l.GetAssetType()
switch {
case a == asset.Spot:
entry := response[l.Pair().Format("", false)]
entry.spotSignal = d[i]
response[l.Pair().Format("", false)] = entry
case a.IsFutures():
u := l.GetUnderlyingPair()
entry := response[u.Format("", false)]
entry.futureSignal = d[i]
response[u.Format("", false)] = entry
default:
return nil, errFuturesOnly
}
}
// validate that each set of signals is matched
for _, v := range response {
if v.futureSignal == nil {
return nil, fmt.Errorf("%w missing future signal", errNotSetup)
}
if v.spotSignal == nil {
return nil, fmt.Errorf("%w missing spot signal", errNotSetup)
}
}
return response, nil
}
// SetCustomSettings can override default settings
func (s *Strategy) SetCustomSettings(customSettings map[string]interface{}) error {
for k, v := range customSettings {
switch k {
case openShortDistancePercentageString:
osdp, ok := v.(float64)
if !ok || osdp <= 0 {
return fmt.Errorf("%w provided openShortDistancePercentage value could not be parsed: %v", base.ErrInvalidCustomSettings, v)
}
s.openShortDistancePercentage = decimal.NewFromFloat(osdp)
case closeShortDistancePercentageString:
csdp, ok := v.(float64)
if !ok || csdp <= 0 {
return fmt.Errorf("%w provided closeShortDistancePercentage value could not be parsed: %v", base.ErrInvalidCustomSettings, v)
}
s.closeShortDistancePercentage = decimal.NewFromFloat(csdp)
default:
return fmt.Errorf("%w unrecognised custom setting key %v with value %v. Cannot apply", base.ErrInvalidCustomSettings, k, v)
}
}
return nil
}
// SetDefaults sets default values for overridable custom settings
func (s *Strategy) SetDefaults() {
s.openShortDistancePercentage = decimal.Zero
s.closeShortDistancePercentage = decimal.Zero
}

View File

@@ -0,0 +1,420 @@
package ftxcashandcarry
import (
"errors"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
datakline "github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
eventkline "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
func TestName(t *testing.T) {
t.Parallel()
d := Strategy{}
if n := d.Name(); n != Name {
t.Errorf("expected %v", Name)
}
}
func TestDescription(t *testing.T) {
t.Parallel()
d := Strategy{}
if n := d.Description(); n != description {
t.Errorf("expected %v", description)
}
}
func TestSupportsSimultaneousProcessing(t *testing.T) {
t.Parallel()
s := Strategy{}
if !s.SupportsSimultaneousProcessing() {
t.Error("expected true")
}
}
func TestSetCustomSettings(t *testing.T) {
t.Parallel()
s := Strategy{}
err := s.SetCustomSettings(nil)
if err != nil {
t.Error(err)
}
float14 := float64(14)
mappalopalous := make(map[string]interface{})
mappalopalous[openShortDistancePercentageString] = float14
mappalopalous[closeShortDistancePercentageString] = float14
err = s.SetCustomSettings(mappalopalous)
if err != nil {
t.Error(err)
}
mappalopalous[openShortDistancePercentageString] = "14"
err = s.SetCustomSettings(mappalopalous)
if !errors.Is(err, base.ErrInvalidCustomSettings) {
t.Errorf("received: %v, expected: %v", err, base.ErrInvalidCustomSettings)
}
mappalopalous[closeShortDistancePercentageString] = float14
mappalopalous[openShortDistancePercentageString] = "14"
err = s.SetCustomSettings(mappalopalous)
if !errors.Is(err, base.ErrInvalidCustomSettings) {
t.Errorf("received: %v, expected: %v", err, base.ErrInvalidCustomSettings)
}
mappalopalous[closeShortDistancePercentageString] = float14
mappalopalous["lol"] = float14
err = s.SetCustomSettings(mappalopalous)
if !errors.Is(err, base.ErrInvalidCustomSettings) {
t.Errorf("received: %v, expected: %v", err, base.ErrInvalidCustomSettings)
}
}
func TestOnSignal(t *testing.T) {
t.Parallel()
s := Strategy{
openShortDistancePercentage: decimal.NewFromInt(14),
}
_, err := s.OnSignal(nil, nil, nil)
if !errors.Is(err, base.ErrSimultaneousProcessingOnly) {
t.Errorf("received: %v, expected: %v", err, base.ErrSimultaneousProcessingOnly)
}
}
func TestSetDefaults(t *testing.T) {
t.Parallel()
s := Strategy{}
s.SetDefaults()
if !s.openShortDistancePercentage.Equal(decimal.NewFromInt(0)) {
t.Errorf("expected 5, received %v", s.openShortDistancePercentage)
}
if !s.closeShortDistancePercentage.Equal(decimal.NewFromInt(0)) {
t.Errorf("expected 5, received %v", s.closeShortDistancePercentage)
}
}
func TestSortSignals(t *testing.T) {
t.Parallel()
dInsert := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
exch := "ftx"
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: &event.Base{
Exchange: exch,
Time: dInsert,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
},
Open: decimal.NewFromInt(1337),
Close: decimal.NewFromInt(1337),
Low: decimal.NewFromInt(1337),
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
d.Next()
da := &datakline.DataFromKline{
Item: gctkline.Item{},
Base: d,
RangeHolder: &gctkline.IntervalRangeHolder{},
}
_, err := sortSignals([]data.Handler{da})
if !errors.Is(err, errNotSetup) {
t.Errorf("received: %v, expected: %v", err, errNotSetup)
}
d2 := data.Base{}
d2.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: &event.Base{
Exchange: exch,
Time: dInsert,
Interval: gctkline.OneDay,
CurrencyPair: currency.NewPair(currency.DOGE, currency.XRP),
AssetType: asset.Futures,
UnderlyingPair: p,
},
Open: decimal.NewFromInt(1337),
Close: decimal.NewFromInt(1337),
Low: decimal.NewFromInt(1337),
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
d2.Next()
da2 := &datakline.DataFromKline{
Item: gctkline.Item{},
Base: d2,
RangeHolder: &gctkline.IntervalRangeHolder{},
}
_, err = sortSignals([]data.Handler{da, da2})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
func TestCreateSignals(t *testing.T) {
t.Parallel()
s := Strategy{}
var expectedError = common.ErrNilArguments
_, err := s.createSignals(nil, nil, nil, decimal.Zero, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
spotSignal := &signal.Signal{
Base: &event.Base{AssetType: asset.Spot},
}
_, err = s.createSignals(nil, spotSignal, nil, decimal.Zero, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
// case len(pos) == 0:
expectedError = nil
futuresSignal := &signal.Signal{
Base: &event.Base{AssetType: asset.Futures},
}
resp, err := s.createSignals(nil, spotSignal, futuresSignal, decimal.Zero, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
if len(resp) != 1 {
t.Errorf("received '%v' expected '%v", len(resp), 1)
}
if resp[0].GetAssetType() != asset.Spot {
t.Errorf("received '%v' expected '%v", resp[0].GetAssetType(), asset.Spot)
}
// case len(pos) > 0 && pos[len(pos)-1].Status == order.Open &&
// diffBetweenFuturesSpot.LessThanOrEqual(s.closeShortDistancePercentage):
pos := []gctorder.PositionStats{
{
Status: gctorder.Open,
},
}
resp, err = s.createSignals(pos, spotSignal, futuresSignal, decimal.Zero, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
if len(resp) != 2 {
t.Errorf("received '%v' expected '%v", len(resp), 2)
}
caseTested := false
for i := range resp {
if resp[i].GetAssetType().IsFutures() {
if resp[i].GetDirection() != gctorder.ClosePosition {
t.Errorf("received '%v' expected '%v", resp[i].GetDirection(), gctorder.ClosePosition)
}
caseTested = true
}
}
if !caseTested {
t.Fatal("unhandled issue in test scenario")
}
// case len(pos) > 0 &&
// pos[len(pos)-1].Status == order.Open &&
// isLastEvent:
resp, err = s.createSignals(pos, spotSignal, futuresSignal, decimal.Zero, true)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
if len(resp) != 2 {
t.Errorf("received '%v' expected '%v", len(resp), 2)
}
caseTested = false
for i := range resp {
if resp[i].GetAssetType().IsFutures() {
if resp[i].GetDirection() != gctorder.ClosePosition {
t.Errorf("received '%v' expected '%v", resp[i].GetDirection(), gctorder.ClosePosition)
}
caseTested = true
}
}
if !caseTested {
t.Fatal("unhandled issue in test scenario")
}
// case len(pos) > 0 &&
// pos[len(pos)-1].Status == order.Closed &&
// diffBetweenFuturesSpot.GreaterThan(s.openShortDistancePercentage):
pos[0].Status = gctorder.Closed
resp, err = s.createSignals(pos, spotSignal, futuresSignal, decimal.NewFromInt(1337), true)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
if len(resp) != 1 {
t.Errorf("received '%v' expected '%v", len(resp), 1)
}
caseTested = false
for i := range resp {
if resp[i].GetAssetType() == asset.Spot {
if resp[i].GetDirection() != gctorder.Buy {
t.Errorf("received '%v' expected '%v", resp[i].GetDirection(), gctorder.Buy)
}
if resp[i].GetFillDependentEvent() == nil {
t.Errorf("received '%v' expected '%v'", nil, "fill dependent event")
}
caseTested = true
}
}
if !caseTested {
t.Fatal("unhandled issue in test scenario")
}
// default:
pos[0].Status = gctorder.UnknownStatus
resp, err = s.createSignals(pos, spotSignal, futuresSignal, decimal.NewFromInt(1337), true)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
if len(resp) != 2 {
t.Errorf("received '%v' expected '%v", len(resp), 2)
}
}
// funderino overrides default implementation
type funderino struct {
funding.FundManager
hasBeenLiquidated bool
}
// HasExchangeBeenLiquidated overrides default implementation
func (f funderino) HasExchangeBeenLiquidated(_ common.EventHandler) bool {
return f.hasBeenLiquidated
}
// portfolerino overrides default implementation
type portfolerino struct {
portfolio.Portfolio
}
// GetPositions overrides default implementation
func (p portfolerino) GetPositions(common.EventHandler) ([]gctorder.PositionStats, error) {
return []gctorder.PositionStats{
{
Exchange: exchangeName,
Asset: asset.Spot,
Pair: currency.NewPair(currency.BTC, currency.USD),
Underlying: currency.BTC,
CollateralCurrency: currency.USD,
},
}, nil
}
func TestOnSimultaneousSignals(t *testing.T) {
t.Parallel()
s := Strategy{}
var expectedError = errNoSignals
_, err := s.OnSimultaneousSignals(nil, nil, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
expectedError = common.ErrNilArguments
cp := currency.NewPair(currency.BTC, currency.USD)
d := &datakline.DataFromKline{
Base: data.Base{},
Item: gctkline.Item{
Exchange: exchangeName,
Asset: asset.Spot,
Pair: cp,
UnderlyingPair: currency.NewPair(currency.BTC, currency.USD),
},
}
tt := time.Now()
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: &event.Base{
Exchange: exchangeName,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: cp,
AssetType: asset.Spot,
},
Open: decimal.NewFromInt(1337),
Close: decimal.NewFromInt(1337),
Low: decimal.NewFromInt(1337),
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
d.Next()
signals := []data.Handler{
d,
}
f := &funderino{}
_, err = s.OnSimultaneousSignals(signals, f, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
p := &portfolerino{}
expectedError = errNotSetup
_, err = s.OnSimultaneousSignals(signals, f, p)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
expectedError = nil
d2 := &datakline.DataFromKline{
Base: data.Base{},
Item: gctkline.Item{
Exchange: exchangeName,
Asset: asset.Futures,
Pair: cp,
UnderlyingPair: cp,
},
}
d2.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: &event.Base{
Exchange: exchangeName,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: cp,
AssetType: asset.Futures,
UnderlyingPair: cp,
},
Open: decimal.NewFromInt(1337),
Close: decimal.NewFromInt(1337),
Low: decimal.NewFromInt(1337),
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
d2.Next()
signals = []data.Handler{
d,
d2,
}
resp, err := s.OnSimultaneousSignals(signals, f, p)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
if len(resp) != 2 {
t.Errorf("received '%v' expected '%v", len(resp), 2)
}
f.hasBeenLiquidated = true
resp, err = s.OnSimultaneousSignals(signals, f, p)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
}
if len(resp) != 2 {
t.Fatalf("received '%v' expected '%v", len(resp), 2)
}
if resp[0].GetDirection() != gctorder.DoNothing {
t.Errorf("received '%v' expected '%v", resp[0].GetDirection(), gctorder.DoNothing)
}
}

View File

@@ -0,0 +1,30 @@
package ftxcashandcarry
import (
"errors"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
)
const (
// Name is the strategy name
Name = "ftx-cash-carry"
description = `A cash and carry trade (or basis trading) consists in taking advantage of the premium of a futures contract over the spot price. For example if Ethereum Futures are trading well above its Spot price (contango) you could perform an arbitrage and take advantage of this opportunity.`
exchangeName = "ftx"
openShortDistancePercentageString = "openShortDistancePercentage"
closeShortDistancePercentageString = "closeShortDistancePercentage"
)
var (
errFuturesOnly = errors.New("can only work with futures")
errOnlyFTXSupported = errors.New("only FTX supported for this strategy")
errNoSignals = errors.New("no data signals to process")
)
// Strategy is an implementation of the Handler interface
type Strategy struct {
base.Strategy
openShortDistancePercentage decimal.Decimal
closeShortDistancePercentage decimal.Decimal
}

View File

@@ -47,7 +47,7 @@ func (s *Strategy) Description() string {
// OnSignal handles a data event and returns what action the strategy believes should occur
// For rsi, this means returning a buy signal when rsi is at or below a certain level, and a
// sell signal when it is at or above a certain level
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) (signal.Event, error) {
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) (signal.Event, error) {
if d == nil {
return nil, common.ErrNilEvent
}
@@ -73,7 +73,7 @@ func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer, _ portfol
latestRSIValue := decimal.NewFromFloat(rsi[len(rsi)-1])
if !d.HasDataAtTime(d.Latest().GetTime()) {
es.SetDirection(order.MissingData)
es.AppendReason(fmt.Sprintf("missing data at %v, cannot perform any actions. RSI %v", d.Latest().GetTime(), latestRSIValue))
es.AppendReasonf("missing data at %v, cannot perform any actions. RSI %v", d.Latest().GetTime(), latestRSIValue)
return &es, nil
}
@@ -85,7 +85,7 @@ func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer, _ portfol
default:
es.SetDirection(order.DoNothing)
}
es.AppendReason(fmt.Sprintf("RSI at %v", latestRSIValue))
es.AppendReasonf("RSI at %v", latestRSIValue)
return &es, nil
}
@@ -99,7 +99,7 @@ func (s *Strategy) SupportsSimultaneousProcessing() bool {
// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) ([]signal.Event, error) {
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) ([]signal.Event, error) {
var resp []signal.Event
var errs gctcommon.Errors
for i := range d {

View File

@@ -97,7 +97,7 @@ func TestOnSignal(t *testing.T) {
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: event.Base{
Base: &event.Base{
Offset: 3,
Exchange: exch,
Time: dInsert,
@@ -179,7 +179,7 @@ func TestOnSignals(t *testing.T) {
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: dInsert,
Interval: gctkline.OneDay,

View File

@@ -6,6 +6,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/ftxcashandcarry"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/rsi"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/top2bottom2"
)
@@ -38,5 +39,6 @@ func GetStrategies() []Handler {
new(dollarcostaverage.Strategy),
new(rsi.Strategy),
new(top2bottom2.Strategy),
new(ftxcashandcarry.Strategy),
}
}

View File

@@ -11,8 +11,8 @@ import (
type Handler interface {
Name() string
Description() string
OnSignal(data.Handler, funding.IFundTransferer, portfolio.Handler) (signal.Event, error)
OnSimultaneousSignals([]data.Handler, funding.IFundTransferer, portfolio.Handler) ([]signal.Event, error)
OnSignal(data.Handler, funding.IFundingTransferer, portfolio.Handler) (signal.Event, error)
OnSimultaneousSignals([]data.Handler, funding.IFundingTransferer, portfolio.Handler) ([]signal.Event, error)
UsingSimultaneousProcessing() bool
SupportsSimultaneousProcessing() bool
SetSimultaneousProcessing(bool)

View File

@@ -53,7 +53,7 @@ func (s *Strategy) Description() string {
// OnSignal handles a data event and returns what action the strategy believes should occur
// however,this complex strategy cannot function on an individual basis
func (s *Strategy) OnSignal(_ data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) (signal.Event, error) {
func (s *Strategy) OnSignal(_ data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) (signal.Event, error) {
return nil, errStrategyOnlySupportsSimultaneousProcessing
}
@@ -67,7 +67,7 @@ func (s *Strategy) SupportsSimultaneousProcessing() bool {
type mfiFundEvent struct {
event signal.Event
mfi decimal.Decimal
funds funding.IPairReader
funds funding.IFundReader
}
// ByPrice used for sorting orders by order date
@@ -88,7 +88,7 @@ func sortByMFI(o *[]mfiFundEvent, reverse bool) {
// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundTransferer, _ portfolio.Handler) ([]signal.Event, error) {
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTransferer, _ portfolio.Handler) ([]signal.Event, error) {
if len(d) < 4 {
return nil, errStrategyCurrencyRequirements
}
@@ -137,13 +137,13 @@ func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundTransf
latestMFI := decimal.NewFromFloat(mfi[len(mfi)-1])
if !d[i].HasDataAtTime(d[i].Latest().GetTime()) {
es.SetDirection(order.MissingData)
es.AppendReason(fmt.Sprintf("missing data at %v, cannot perform any actions. MFI %v", d[i].Latest().GetTime(), latestMFI))
es.AppendReasonf("missing data at %v, cannot perform any actions. MFI %v", d[i].Latest().GetTime(), latestMFI)
resp = append(resp, &es)
continue
}
es.SetDirection(order.DoNothing)
es.AppendReason(fmt.Sprintf("MFI at %v", latestMFI))
es.AppendReasonf("MFI at %v", latestMFI)
funds, err := f.GetFundingForEvent(&es)
if err != nil {
@@ -152,7 +152,7 @@ func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundTransf
mfiFundEvents = append(mfiFundEvents, mfiFundEvent{
event: &es,
mfi: latestMFI,
funds: funds,
funds: funds.FundReader(),
})
}

View File

@@ -111,7 +111,7 @@ func TestOnSignals(t *testing.T) {
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
Base: event.Base{
Base: &event.Base{
Exchange: exch,
Time: dInsert,
Interval: gctkline.OneDay,
@@ -166,10 +166,11 @@ func TestSelectTopAndBottomPerformers(t *testing.T) {
if err != nil {
t.Error(err)
}
b := &event.Base{}
fundEvents := []mfiFundEvent{
{
event: &signal.Signal{
Base: b,
ClosePrice: decimal.NewFromInt(99),
Direction: order.DoNothing,
},
@@ -177,6 +178,7 @@ func TestSelectTopAndBottomPerformers(t *testing.T) {
},
{
event: &signal.Signal{
Base: b,
ClosePrice: decimal.NewFromInt(98),
Direction: order.DoNothing,
},
@@ -184,6 +186,7 @@ func TestSelectTopAndBottomPerformers(t *testing.T) {
},
{
event: &signal.Signal{
Base: b,
ClosePrice: decimal.NewFromInt(1),
Direction: order.DoNothing,
},
@@ -191,6 +194,7 @@ func TestSelectTopAndBottomPerformers(t *testing.T) {
},
{
event: &signal.Signal{
Base: b,
ClosePrice: decimal.NewFromInt(2),
Direction: order.DoNothing,
},
@@ -198,6 +202,7 @@ func TestSelectTopAndBottomPerformers(t *testing.T) {
},
{
event: &signal.Signal{
Base: b,
ClosePrice: decimal.NewFromInt(50),
Direction: order.DoNothing,
},
@@ -214,15 +219,15 @@ func TestSelectTopAndBottomPerformers(t *testing.T) {
for i := range resp {
switch resp[i].GetDirection() {
case order.Buy:
if !resp[i].GetPrice().Equal(decimal.NewFromInt(1)) && !resp[i].GetPrice().Equal(decimal.NewFromInt(2)) {
if !resp[i].GetClosePrice().Equal(decimal.NewFromInt(1)) && !resp[i].GetClosePrice().Equal(decimal.NewFromInt(2)) {
t.Error("expected 1 or 2")
}
case order.Sell:
if !resp[i].GetPrice().Equal(decimal.NewFromInt(99)) && !resp[i].GetPrice().Equal(decimal.NewFromInt(98)) {
if !resp[i].GetClosePrice().Equal(decimal.NewFromInt(99)) && !resp[i].GetClosePrice().Equal(decimal.NewFromInt(98)) {
t.Error("expected 99 or 98")
}
case order.DoNothing:
if !resp[i].GetPrice().Equal(decimal.NewFromInt(50)) {
if !resp[i].GetClosePrice().Equal(decimal.NewFromInt(50)) {
t.Error("expected 50")
}
}

View File

@@ -1,6 +1,8 @@
package event
import (
"fmt"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/currency"
@@ -33,9 +35,14 @@ func (b *Base) Pair() currency.Pair {
return b.CurrencyPair
}
// GetUnderlyingPair returns the currency pair
func (b *Base) GetUnderlyingPair() currency.Pair {
return b.UnderlyingPair
}
// GetExchange returns the exchange
func (b *Base) GetExchange() string {
return b.Exchange
return strings.ToLower(b.Exchange)
}
// GetAssetType returns the asset type
@@ -50,14 +57,26 @@ func (b *Base) GetInterval() kline.Interval {
// AppendReason adds reasoning for a decision being made
func (b *Base) AppendReason(y string) {
if b.Reason == "" {
b.Reason = y
} else {
b.Reason = y + ". " + b.Reason
}
b.Reasons = append(b.Reasons, y)
}
// GetReason returns the why
func (b *Base) GetReason() string {
return b.Reason
// AppendReasonf adds reasoning for a decision being made
// but with formatting
func (b *Base) AppendReasonf(y string, addons ...interface{}) {
y = fmt.Sprintf(y, addons...)
b.Reasons = append(b.Reasons, y)
}
// GetConcatReasons returns the why
func (b *Base) GetConcatReasons() string {
return strings.Join(b.Reasons, ". ")
}
// GetReasons returns each individual reason
func (b *Base) GetReasons() []string {
return b.Reasons
}
func (b *Base) GetBase() *Base {
return b
}

View File

@@ -10,17 +10,37 @@ import (
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
func TestEvent_AppendWhy(t *testing.T) {
func TestGetConcatReasons(t *testing.T) {
t.Parallel()
e := &Base{}
e.AppendReason("test")
y := e.GetReason()
y := e.GetConcatReasons()
if !strings.Contains(y, "test") {
t.Error("expected test")
}
e.AppendReason("test")
y = e.GetConcatReasons()
if y != "test. test" {
t.Error("expected 'test. test'")
}
}
func TestEvent_GetAssetType(t *testing.T) {
func TestGetReasons(t *testing.T) {
t.Parallel()
e := &Base{}
e.AppendReason("test")
y := e.GetReasons()
if !strings.Contains(y[0], "test") {
t.Error("expected test")
}
e.AppendReason("test2")
y = e.GetReasons()
if y[1] != "test2" {
t.Error("expected 'test2'")
}
}
func TestGetAssetType(t *testing.T) {
t.Parallel()
e := &Base{
AssetType: asset.Spot,
@@ -30,7 +50,7 @@ func TestEvent_GetAssetType(t *testing.T) {
}
}
func TestEvent_GetExchange(t *testing.T) {
func TestGetExchange(t *testing.T) {
t.Parallel()
e := &Base{
Exchange: "test",
@@ -40,7 +60,7 @@ func TestEvent_GetExchange(t *testing.T) {
}
}
func TestEvent_GetInterval(t *testing.T) {
func TestGetInterval(t *testing.T) {
t.Parallel()
e := &Base{
Interval: gctkline.OneMin,
@@ -50,7 +70,7 @@ func TestEvent_GetInterval(t *testing.T) {
}
}
func TestEvent_GetTime(t *testing.T) {
func TestGetTime(t *testing.T) {
t.Parallel()
tt := time.Now()
e := &Base{
@@ -62,7 +82,7 @@ func TestEvent_GetTime(t *testing.T) {
}
}
func TestEvent_IsEvent(t *testing.T) {
func TestIsEvent(t *testing.T) {
t.Parallel()
e := &Base{}
if y := e.IsEvent(); !y {
@@ -70,7 +90,7 @@ func TestEvent_IsEvent(t *testing.T) {
}
}
func TestEvent_Pair(t *testing.T) {
func TestPair(t *testing.T) {
t.Parallel()
e := &Base{
CurrencyPair: currency.NewPair(currency.BTC, currency.USDT),
@@ -80,3 +100,57 @@ func TestEvent_Pair(t *testing.T) {
t.Error("expected currency")
}
}
func TestGetOffset(t *testing.T) {
t.Parallel()
b := Base{
Offset: 1337,
}
if b.GetOffset() != 1337 {
t.Error("expected 1337")
}
}
func TestSetOffset(t *testing.T) {
t.Parallel()
b := Base{
Offset: 1337,
}
b.SetOffset(1339)
if b.Offset != 1339 {
t.Error("expected 1339")
}
}
func TestAppendReasonf(t *testing.T) {
t.Parallel()
b := Base{}
b.AppendReasonf("%v", "hello moto")
if b.GetConcatReasons() != "hello moto" {
t.Errorf("expected hello moto, received '%v'", b.GetConcatReasons())
}
b.AppendReasonf("%v %v", "hello", "moto")
if b.GetConcatReasons() != "hello moto. hello moto" {
t.Errorf("expected 'hello moto. hello moto', received '%v'", b.GetConcatReasons())
}
}
func TestGetBase(t *testing.T) {
t.Parallel()
b1 := &Base{
Exchange: "hello",
}
if b1.Exchange != b1.GetBase().Exchange {
t.Errorf("expected '%v' received '%v'", b1.Exchange, b1.GetBase().Exchange)
}
}
func TestGetUnderlyingPair(t *testing.T) {
t.Parallel()
b1 := &Base{
UnderlyingPair: currency.NewPair(currency.BTC, currency.USDT),
}
if !b1.UnderlyingPair.Equal(b1.GetUnderlyingPair()) {
t.Errorf("expected '%v' received '%v'", b1.UnderlyingPair, b1.GetUnderlyingPair())
}
}

View File

@@ -12,11 +12,12 @@ import (
// Data, fill, order events all contain the base event and store important and
// consistent information
type Base struct {
Offset int64 `json:"-"`
Exchange string `json:"exchange"`
Time time.Time `json:"timestamp"`
Interval kline.Interval `json:"interval-size"`
CurrencyPair currency.Pair `json:"pair"`
AssetType asset.Item `json:"asset"`
Reason string `json:"reason"`
Offset int64 `json:"-"`
Exchange string `json:"exchange"`
Time time.Time `json:"timestamp"`
Interval kline.Interval `json:"interval-size"`
CurrencyPair currency.Pair `json:"pair"`
UnderlyingPair currency.Pair `json:"underlying"`
AssetType asset.Item `json:"asset"`
Reasons []string `json:"reasons"`
}

View File

@@ -2,6 +2,7 @@ package fill
import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -64,3 +65,15 @@ func (f *Fill) GetOrder() *order.Detail {
func (f *Fill) GetSlippageRate() decimal.Decimal {
return f.Slippage
}
// GetFillDependentEvent returns the fill dependent event
// to raise after a prerequisite event has been completed
func (f *Fill) GetFillDependentEvent() signal.Event {
return f.FillDependentEvent
}
// IsLiquidated highlights if the fill event
// was a result of liquidation
func (f *Fill) IsLiquidated() bool {
return f.Liquidated
}

View File

@@ -4,6 +4,7 @@ import (
"testing"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -89,3 +90,40 @@ func TestGetSlippageRate(t *testing.T) {
t.Error("expected 1")
}
}
func TestGetTotal(t *testing.T) {
t.Parallel()
f := Fill{}
f.Total = decimal.NewFromInt(1337)
e := f.GetTotal()
if !e.Equal(decimal.NewFromInt(1337)) {
t.Error("expected 1337")
}
}
func TestGetFillDependentEvent(t *testing.T) {
t.Parallel()
f := Fill{}
if f.GetFillDependentEvent() != nil {
t.Error("expected nil")
}
f.FillDependentEvent = &signal.Signal{
Amount: decimal.NewFromInt(1337),
}
e := f.GetFillDependentEvent()
if !e.GetAmount().Equal(decimal.NewFromInt(1337)) {
t.Error("expected 1337")
}
}
func TestIsLiquidated(t *testing.T) {
t.Parallel()
f := Fill{}
if f.IsLiquidated() {
t.Error("expected false")
}
f.Liquidated = true
if !f.IsLiquidated() {
t.Error("expected true")
}
}

View File

@@ -4,12 +4,13 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// Fill is an event that details the events from placing an order
type Fill struct {
event.Base
*event.Base
Direction order.Side `json:"side"`
Amount decimal.Decimal `json:"amount"`
ClosePrice decimal.Decimal `json:"close-price"`
@@ -19,6 +20,8 @@ type Fill struct {
ExchangeFee decimal.Decimal `json:"exchange-fee"`
Slippage decimal.Decimal `json:"slippage"`
Order *order.Detail `json:"-"`
FillDependentEvent signal.Event
Liquidated bool
}
// Event holds all functions required to handle a fill event
@@ -36,4 +39,6 @@ type Event interface {
GetExchangeFee() decimal.Decimal
SetExchangeFee(decimal.Decimal)
GetOrder() *order.Detail
GetFillDependentEvent() signal.Event
IsLiquidated() bool
}

View File

@@ -1,6 +1,9 @@
package kline
import "github.com/shopspring/decimal"
import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/currency"
)
// GetClosePrice returns the closing price of a kline
func (k *Kline) GetClosePrice() decimal.Decimal {
@@ -21,3 +24,8 @@ func (k *Kline) GetLowPrice() decimal.Decimal {
func (k *Kline) GetOpenPrice() decimal.Decimal {
return k.Open
}
// GetUnderlyingPair returns the open price of a kline
func (k *Kline) GetUnderlyingPair() currency.Pair {
return k.UnderlyingPair
}

View File

@@ -4,6 +4,8 @@ import (
"testing"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/currency"
)
func TestClose(t *testing.T) {
@@ -45,3 +47,15 @@ func TestOpen(t *testing.T) {
t.Error("expected decimal.NewFromInt(1337)")
}
}
func TestGetUnderlyingPair(t *testing.T) {
t.Parallel()
k := Kline{
Base: &event.Base{
UnderlyingPair: currency.NewPair(currency.USD, currency.DOGE),
},
}
if !k.GetUnderlyingPair().Equal(k.Base.UnderlyingPair) {
t.Errorf("expected '%v'", k.Base.UnderlyingPair)
}
}

View File

@@ -8,7 +8,7 @@ import (
// Kline holds kline data and an event to be processed as
// a common.DataEventHandler type
type Kline struct {
event.Base
*event.Base
Open decimal.Decimal
Close decimal.Decimal
Low decimal.Decimal

View File

@@ -2,6 +2,7 @@ package order
import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -81,3 +82,24 @@ func (o *Order) SetLeverage(l decimal.Decimal) {
func (o *Order) GetAllocatedFunds() decimal.Decimal {
return o.AllocatedFunds
}
// GetFillDependentEvent returns the fill dependent event
// so it can be added the event queue
func (o *Order) GetFillDependentEvent() signal.Event {
return o.FillDependentEvent
}
// IsClosingPosition returns whether position is being closed
func (o *Order) IsClosingPosition() bool {
return o.ClosingPosition
}
// IsLiquidating returns whether position is being liquidated
func (o *Order) IsLiquidating() bool {
return o.LiquidatingPosition
}
// GetClosePrice returns the close price
func (o *Order) GetClosePrice() decimal.Decimal {
return o.ClosePrice
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/currency"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -39,10 +40,10 @@ func TestSetAmount(t *testing.T) {
}
}
func TestPair(t *testing.T) {
func TestIsEmpty(t *testing.T) {
t.Parallel()
o := Order{
Base: event.Base{
Base: &event.Base{
CurrencyPair: currency.NewPair(currency.BTC, currency.USDT),
},
}
@@ -84,3 +85,87 @@ func TestGetFunds(t *testing.T) {
t.Error("expected decimal.NewFromInt(1337)")
}
}
func TestOpen(t *testing.T) {
t.Parallel()
k := Order{
ClosePrice: decimal.NewFromInt(1337),
}
if !k.GetClosePrice().Equal(decimal.NewFromInt(1337)) {
t.Error("expected decimal.NewFromInt(1337)")
}
}
func TestIsLiquidating(t *testing.T) {
t.Parallel()
k := Order{}
if k.IsLiquidating() {
t.Error("expected false")
}
k.LiquidatingPosition = true
if !k.IsLiquidating() {
t.Error("expected true")
}
}
func TestGetBuyLimit(t *testing.T) {
t.Parallel()
k := Order{
BuyLimit: decimal.NewFromInt(1337),
}
if !k.GetBuyLimit().Equal(decimal.NewFromInt(1337)) {
t.Errorf("received '%v' expected '%v'", k.GetBuyLimit(), decimal.NewFromInt(1337))
}
}
func TestGetSellLimit(t *testing.T) {
t.Parallel()
k := Order{
SellLimit: decimal.NewFromInt(1337),
}
if !k.GetSellLimit().Equal(decimal.NewFromInt(1337)) {
t.Errorf("received '%v' expected '%v'", k.GetSellLimit(), decimal.NewFromInt(1337))
}
}
func TestPair(t *testing.T) {
t.Parallel()
cp := currency.NewPair(currency.BTC, currency.USDT)
k := Order{
Base: &event.Base{
CurrencyPair: cp,
},
}
if !k.Pair().Equal(cp) {
t.Errorf("received '%v' expected '%v'", k.Pair(), cp)
}
}
func TestGetStatus(t *testing.T) {
t.Parallel()
k := Order{
Status: gctorder.UnknownStatus,
}
if k.GetStatus() != gctorder.UnknownStatus {
t.Errorf("received '%v' expected '%v'", k.GetStatus(), gctorder.UnknownStatus)
}
}
func TestGetFillDependentEvent(t *testing.T) {
t.Parallel()
k := Order{
FillDependentEvent: &signal.Signal{Amount: decimal.NewFromInt(1337)},
}
if !k.GetFillDependentEvent().GetAmount().Equal(decimal.NewFromInt(1337)) {
t.Errorf("received '%v' expected '%v'", k.GetFillDependentEvent(), decimal.NewFromInt(1337))
}
}
func TestIsClosingPosition(t *testing.T) {
t.Parallel()
k := Order{
ClosingPosition: true,
}
if !k.IsClosingPosition() {
t.Errorf("received '%v' expected '%v'", k.IsClosingPosition(), true)
}
}

View File

@@ -4,28 +4,33 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// Order contains all details for an order event
type Order struct {
event.Base
ID string
Direction order.Side
Status order.Status
Price decimal.Decimal
Amount decimal.Decimal
OrderType order.Type
Leverage decimal.Decimal
AllocatedFunds decimal.Decimal
BuyLimit decimal.Decimal
SellLimit decimal.Decimal
*event.Base
ID string
Direction order.Side
Status order.Status
ClosePrice decimal.Decimal
Amount decimal.Decimal
OrderType order.Type
Leverage decimal.Decimal
AllocatedFunds decimal.Decimal
BuyLimit decimal.Decimal
SellLimit decimal.Decimal
FillDependentEvent signal.Event
ClosingPosition bool
LiquidatingPosition bool
}
// Event inherits common event interfaces along with extra functions related to handling orders
type Event interface {
common.EventHandler
common.Directioner
GetClosePrice() decimal.Decimal
GetBuyLimit() decimal.Decimal
GetSellLimit() decimal.Decimal
SetAmount(decimal.Decimal)
@@ -36,4 +41,7 @@ type Event interface {
GetID() string
IsLeveraged() bool
GetAllocatedFunds() decimal.Decimal
GetFillDependentEvent() signal.Event
IsClosingPosition() bool
IsLiquidating() bool
}

View File

@@ -46,8 +46,8 @@ func (s *Signal) Pair() currency.Pair {
return s.CurrencyPair
}
// GetPrice returns the price
func (s *Signal) GetPrice() decimal.Decimal {
// GetClosePrice returns the price
func (s *Signal) GetClosePrice() decimal.Decimal {
return s.ClosePrice
}
@@ -55,3 +55,40 @@ func (s *Signal) GetPrice() decimal.Decimal {
func (s *Signal) SetPrice(f decimal.Decimal) {
s.ClosePrice = f
}
// GetAmount retrieves the order amount
func (s *Signal) GetAmount() decimal.Decimal {
return s.Amount
}
// SetAmount sets the order amount
func (s *Signal) SetAmount(d decimal.Decimal) {
s.Amount = d
}
// GetUnderlyingPair returns the underlying currency pair
func (s *Signal) GetUnderlyingPair() currency.Pair {
return s.UnderlyingPair
}
// GetFillDependentEvent returns the fill dependent event
// so it can be added to the event queue
func (s *Signal) GetFillDependentEvent() Event {
return s.FillDependentEvent
}
// GetCollateralCurrency returns the collateral currency
func (s *Signal) GetCollateralCurrency() currency.Code {
return s.CollateralCurrency
}
// IsNil says if the event is nil
func (s *Signal) IsNil() bool {
return s == nil
}
// MatchOrderAmount ensures an order must match
// its set amount or fail
func (s *Signal) MatchOrderAmount() bool {
return s.MatchesOrderAmount
}

View File

@@ -4,6 +4,8 @@ import (
"testing"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/currency"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -30,7 +32,7 @@ func TestSetPrice(t *testing.T) {
ClosePrice: decimal.NewFromInt(1),
}
s.SetPrice(decimal.NewFromInt(1337))
if !s.GetPrice().Equal(decimal.NewFromInt(1337)) {
if !s.GetClosePrice().Equal(decimal.NewFromInt(1337)) {
t.Error("expected decimal.NewFromInt(1337)")
}
}
@@ -56,3 +58,99 @@ func TestSetSellLimit(t *testing.T) {
t.Errorf("expected 20, received %v", s.GetSellLimit())
}
}
func TestGetAmount(t *testing.T) {
t.Parallel()
s := Signal{
Amount: decimal.NewFromInt(1337),
}
if !s.GetAmount().Equal(decimal.NewFromInt(1337)) {
t.Error("expected decimal.NewFromInt(1337)")
}
}
func TestSetAmount(t *testing.T) {
t.Parallel()
s := Signal{}
s.SetAmount(decimal.NewFromInt(1337))
if !s.GetAmount().Equal(decimal.NewFromInt(1337)) {
t.Error("expected decimal.NewFromInt(1337)")
}
}
func TestGetUnderlyingPair(t *testing.T) {
t.Parallel()
s := Signal{
Base: &event.Base{
UnderlyingPair: currency.NewPair(currency.USD, currency.DOGE),
},
}
if !s.GetUnderlyingPair().Equal(s.Base.UnderlyingPair) {
t.Errorf("expected '%v'", s.Base.UnderlyingPair)
}
}
func TestPair(t *testing.T) {
t.Parallel()
s := Signal{
Base: &event.Base{
CurrencyPair: currency.NewPair(currency.USD, currency.DOGE),
},
}
if !s.Pair().Equal(s.Base.CurrencyPair) {
t.Errorf("expected '%v'", s.Base.CurrencyPair)
}
}
func TestGetFillDependentEvent(t *testing.T) {
t.Parallel()
s := Signal{}
if a := s.GetFillDependentEvent(); a != nil {
t.Error("expected nil")
}
s.FillDependentEvent = &Signal{
Amount: decimal.NewFromInt(1337),
}
e := s.GetFillDependentEvent()
if !e.GetAmount().Equal(decimal.NewFromInt(1337)) {
t.Error("expected 1337")
}
}
func TestGetCollateralCurrency(t *testing.T) {
t.Parallel()
s := Signal{}
c := s.GetCollateralCurrency()
if !c.IsEmpty() {
t.Error("expected empty currency")
}
s.CollateralCurrency = currency.BTC
c = s.GetCollateralCurrency()
if !c.Equal(currency.BTC) {
t.Error("expected empty currency")
}
}
func TestIsNil(t *testing.T) {
t.Parallel()
s := &Signal{}
if s.IsNil() {
t.Error("expected false")
}
s = nil
if !s.IsNil() {
t.Error("expected true")
}
}
func TestMatchOrderAmount(t *testing.T) {
t.Parallel()
s := &Signal{}
if s.MatchOrderAmount() {
t.Error("expected false")
}
s.MatchesOrderAmount = true
if !s.MatchOrderAmount() {
t.Error("expected true")
}
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -13,21 +14,51 @@ type Event interface {
common.EventHandler
common.Directioner
GetPrice() decimal.Decimal
GetClosePrice() decimal.Decimal
IsSignal() bool
GetSellLimit() decimal.Decimal
GetBuyLimit() decimal.Decimal
GetAmount() decimal.Decimal
GetFillDependentEvent() Event
GetCollateralCurrency() currency.Code
SetAmount(decimal.Decimal)
MatchOrderAmount() bool
IsNil() bool
}
// Signal contains everything needed for a strategy to raise a signal event
type Signal struct {
event.Base
*event.Base
OpenPrice decimal.Decimal
HighPrice decimal.Decimal
LowPrice decimal.Decimal
ClosePrice decimal.Decimal
Volume decimal.Decimal
BuyLimit decimal.Decimal
SellLimit decimal.Decimal
Direction order.Side
// BuyLimit sets a maximum buy from the strategy
// it differs from amount as it is more a suggestion
// use Amount if you wish to have a fillOrKill style amount
BuyLimit decimal.Decimal
// SellLimit sets a maximum sell from the strategy
// it differs from amount as it is more a suggestion
// use Amount if you wish to have a fillOrKill style amount
SellLimit decimal.Decimal
// Amount set the amount when you wish to allow
// a strategy to dictate order quantities
// if the amount is not allowed by the portfolio manager
// the order will not be placed
Amount decimal.Decimal
Direction order.Side
// FillDependentEvent ensures that an order can only be placed
// if there is corresponding collateral in the selected currency
// this enabled cash and carry strategies for example
FillDependentEvent Event
// CollateralCurrency is an optional paramater
// when using futures to limit the collateral available
// to a singular currency
// eg with $5000 usd and 1 BTC, specifying BTC ensures
// the USD value won't be utilised when sizing an order
CollateralCurrency currency.Code
// MatchOrderAmount flags to other event handlers
// that the order amount must match the set Amount property
MatchesOrderAmount bool
}

View File

@@ -36,6 +36,9 @@ A funding item holds the initial funding, current funding, reserved funding and
### What is a funding Pair?
A funding Pair consists of two funding Items, the Base and Quote. If Exchange Level Funding is disabled, the Base and Quote are linked to each other and the funds cannot be shared with other Pairs or Items. If Exchange Level Funding is enabled, the pair can access the same funds as every other currency that shares the exchange and asset type.
### What is a collateral Pair?
A collateral Pair consists of two funding Items, the Contract and Collateral. These are exclusive to FUTURES asset type and help track how much money there is, along with how many contract holdings there are
### What does Exchange Level Funding mean?
Exchange level funding allows funds to be shared during a backtesting run. If the strategy contains the two pairs BTC-USDT and BNB-USDT and the strategy sells 3 BTC for $100,000 USDT, then BNB-USDT can use that $100,000 USDT to make a purchase of $20,000 BNB.
It is restricted to an exchange and asset type, so BTC used in spot, cannot be used in a futures contract (futures backtesting is not currently supported). However, the funding manager can transfer funds between exchange and asset types.
@@ -67,6 +70,11 @@ No. The already existing `CurrencySettings` will populate the funding manager wi
| Name | The strategy to use | `rsi` |
| UsesSimultaneousProcessing | This denotes whether multiple currencies are processed simultaneously with the strategy function `OnSimultaneousSignals`. Eg If you have multiple CurrencySettings and only wish to purchase BTC-USDT when XRP-DOGE is 1337, this setting is useful as you can analyse both signal events to output a purchase call for BTC | `true` |
| CustomSettings | This is a map where you can enter custom settings for a strategy. The RSI strategy allows for customisation of the upper, lower and length variables to allow you to change them from 70, 30 and 14 respectively to 69, 36, 12 | `"custom-settings": { "rsi-high": 70, "rsi-low": 30, "rsi-period": 14 } ` |
#### Funding Settings
| Key | Description | Example |
| --- | ------- | --- |
| UseExchangeLevelFunding | This allows shared exchange funds to be used in your strategy. Requires `UsesSimultaneousProcessing` to be set to `true` to use | `false` |
| ExchangeLevelFunding | This is a list of funding definitions if `UseExchangeLevelFunding` is set to true | See below table |

Some files were not shown because too many files have changed in this diff Show More