Files
gocryptotrader/backtester/engine/backtest_test.go
Scott 017cdf1384 Backtester: Live trading upgrades (#1023)
* Modifications for a smoother live run

* Fixes data appending

* Successfully allows multi-currency live trading. Adds multiple currencies to live DCA strategy

* Attempting to get cash and carry working

* Poor attempts at sorting out data and appending it properly with USD in mind

* =designs new live data handler

* Updates cash and carry strat to work

* adds test coverage. begins closeallpositions function

* Updates cash and carry to work live

* New kline.Event type. Cancels orders on close. Rn types

* =Fixes USD funding issue

* =fixes tests

* fixes tests AGAIN

* adds coverage to close all orders

* crummy tests, should override

* more tests

* more tests

* more coverage

* removes scourge of currency.Pair maps. More tests

* missed currency stuff

* Fixes USD data issue & collateral issue. Needs to close ALL orders

* Now triggers updates on the very first data entry

* All my problems are solved now????

* fixes tests, extends coverage

* there is some really funky candle stuff going on

* my brain is melting

* better shutdown management, fixes freezing bug

* fixes data duplication issues, adds retries to requests

* reduces logging, adds verbose options

* expands coverage over all new functionality

* fixes fun bug from curr == curr to curr.Equal(curr)

* fixes setup issues and tests

* starts adding external wallet amounts for funding

* more setup for assets

* setup live fund calcs and placing orders

* successfully performs automated cash and carry

* merge fixes

* funding properly set at all times

* fixes some bugs, need to address currencystatistics still

* adds 'appeneded' trait, attempts to fix some stats

* fixes stat bugs, adds cool new fetchfees feature

* fixes terrible processing bugs

* tightens realorder stats, sadly loses some live stats

* this actually sets everything correctly for bothcd ..cd ..cd ..cd ..cd ..!

* fix tests

* coverage

* beautiful new test coverage

* docs

* adds new fee getter delayer

* commits from the correct directory

* Lint

* adds verbose to fund manager

* Fix bug in t2b2 strat. Update dca live config. Docs

* go mod tidy

* update buf

* buf + test improvement

* Post merge fixes

* fixes surprise offset bug

* fix sizing restrictions for cash and carry

* fix server lints

* merge fixes

* test fixesss

* lintle fixles

* slowloris

* rn run to task, bug fixes, close all on close

* rpc lint and fixes

* bugfix: order manager not processing orders properly

* somewhat addresses nits

* absolutely broken end of day commit

* absolutely massive knockon effects from nits

* massive knockon effects continue

* fixes things

* address remaining nits

* jk now fixes things

* addresses the easier nits

* more nit fixers

* more niterinos addressederinos

* refactors holdings and does some nits

* so buf

* addresses some nits, fixes holdings bugs

* cleanup

* attempts to fix alert chans to prevent many chans waiting?

* terrible code, will revert

* to be reviewed in detail tomorrow

* Fixes up channel system

* smashes those nits

* fixes extra candles, fixes collateral bug, tests

* fixes data races, introduces reflection

* more checks n tests

* Fixes cash and carry issues. Fixes more cool bugs

* fixes ~typer~ typo

* replace spot strats from ftx to binance

* fixes all the tests I just destroyed

* removes example path, rm verbose

* 1) what 2) removes FTX references from the Backtester

* renamed, non-working strategies

* Removes FTX references almost as fast as sbf removes funds

* regen docs, add contrib names,sort contrib names

* fixes merge renamings

* Addresses nits. Fixes setting API credentials. Fixes Binance limit retrieval

* Fixes live order bugs with real orders and without

* Apply suggestions from code review

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/engine/live.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/engine/live.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/config/strategyconfigbuilder/main.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* updates docs

* even better docs

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
2023-01-05 13:03:17 +11:00

2014 lines
56 KiB
Go

package engine
import (
"context"
"errors"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/gofrs/uuid"
"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/binancecashandcarry"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
evkline "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
"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/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/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
const testExchange = "binance"
var leet = decimal.NewFromInt(1337)
func TestSetupFromConfig(t *testing.T) {
t.Parallel()
bt, err := NewBacktester()
if !errors.Is(err, nil) {
t.Errorf("received %v, expected %v", err, nil)
}
err = bt.SetupFromConfig(nil, "", "", false)
if !errors.Is(err, errNilConfig) {
t.Errorf("received %v, expected %v", err, errNilConfig)
}
cfg := &config.Config{}
err = bt.SetupFromConfig(cfg, "", "", false)
if !errors.Is(err, base.ErrStrategyNotFound) {
t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
}
cfg.CurrencySettings = []config.CurrencySettings{
{
ExchangeName: testExchange,
Base: currency.BTC,
Quote: currency.NewCode("0624"),
Asset: asset.Spot,
},
{
ExchangeName: testExchange,
Base: currency.BTC,
Quote: currency.NewCode("0624"),
Asset: asset.USDTMarginedFutures,
},
}
err = bt.SetupFromConfig(cfg, "", "", false)
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 = currency.BTC
cfg.CurrencySettings[0].Quote = currency.USDT
cfg.DataSettings.APIData = &config.APIData{}
err = bt.SetupFromConfig(cfg, "", "", false)
if err != nil && !strings.Contains(err.Error(), "unrecognised dataType") {
t.Error(err)
}
cfg.DataSettings.DataType = common.CandleStr
err = bt.SetupFromConfig(cfg, "", "", false)
if !errors.Is(err, errIntervalUnset) {
t.Errorf("received: %v, expected: %v", err, errIntervalUnset)
}
cfg.DataSettings.Interval = gctkline.OneMin
cfg.CurrencySettings[0].MakerFee = &decimal.Zero
cfg.CurrencySettings[0].TakerFee = &decimal.Zero
err = bt.SetupFromConfig(cfg, "", "", false)
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 = bt.SetupFromConfig(cfg, "", "", false)
if !errors.Is(err, gctcommon.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNotYetImplemented)
}
cfg.FundingSettings.UseExchangeLevelFunding = true
cfg.FundingSettings.ExchangeLevelFunding = []config.ExchangeLevelFunding{
{
ExchangeName: testExchange,
Asset: asset.Spot,
Currency: currency.BTC,
InitialFunds: leet,
TransferFee: leet,
},
{
ExchangeName: testExchange,
Asset: asset.USDTMarginedFutures,
Currency: currency.BTC,
InitialFunds: leet,
TransferFee: leet,
},
}
err = bt.SetupFromConfig(cfg, "", "", false)
if !errors.Is(err, gctcommon.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNotYetImplemented)
}
}
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,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
APIData: &config.APIData{
StartDate: time.Now().Add(-time.Minute * 5),
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 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,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
MakerFee: &decimal.Zero,
TakerFee: &decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
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 TestLoadDataDatabase(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: testExchange,
Asset: asset.Spot,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
MakerFee: &decimal.Zero,
TakerFee: &decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
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(testExchange)
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 TestLoadDataLive(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &fakeReport{},
Funding: &funding.FundManager{},
DataHolder: &data.HandlerHolder{},
Statistic: &fakeStats{},
exchangeManager: engine.SetupExchangeManager(),
shutdown: make(chan struct{}),
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: testExchange,
Asset: asset.Spot,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
MakerFee: &decimal.Zero,
TakerFee: &decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
LiveData: &config.LiveData{
ExchangeCredentials: []config.Credentials{
{
Exchange: testExchange,
Keys: account.Credentials{
Key: "test",
Secret: "test",
ClientID: "test",
PEMKey: "test",
SubAccount: "test",
OneTimePassword: "test",
},
},
},
RealOrders: true,
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
exch, err := bt.exchangeManager.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
err = bt.SetupLiveDataHandler(0, 0, false, false)
if !errors.Is(err, nil) {
t.Fatalf("received: %v, expected: %v", err, nil)
}
err = bt.LiveDataHandler.Start()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
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 !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bt.Stop()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
func TestReset(t *testing.T) {
t.Parallel()
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, false, false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
bt := &BackTest{
shutdown: make(chan struct{}),
DataHolder: &data.HandlerHolder{},
Strategy: &dollarcostaverage.Strategy{},
Portfolio: &portfolio.Portfolio{},
Exchange: &exchange.Exchange{},
Statistic: &statistics.Statistic{},
EventQueue: &eventholder.Holder{},
Reports: &report.Data{},
Funding: f,
}
err = bt.Reset()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.Funding.IsUsingExchangeLevelFunding() {
t.Error("expected false")
}
bt = nil
err = bt.Reset()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestFullCycle(t *testing.T) {
t.Parallel()
ex := testExchange
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
tt := time.Now()
stats := &statistics.Statistic{}
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex][a] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex][a][cp.Base.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic)
port, err := portfolio.Setup(&size.Size{
BuySide: exchange.MinMax{},
SellSide: exchange.MinMax{},
}, &risk.Risk{}, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
fx := &binance.Binance{}
fx.Name = testExchange
err = port.SetCurrencySettingsMap(&exchange.Settings{Exchange: fx, Asset: a, Pair: cp})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true, false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
b, err := funding.CreateItem(ex, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
quote, err := funding.CreateItem(ex, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
pair, err := funding.CreatePair(b, quote)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = f.AddPair(pair)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
bt := BackTest{
DataHolder: &data.HandlerHolder{},
Strategy: &dollarcostaverage.Strategy{},
Portfolio: port,
Exchange: &exchange.Exchange{},
Statistic: stats,
EventQueue: &eventholder.Holder{},
Reports: &report.Data{},
hasProcessedDataAtOffset: make(map[int64]bool),
Funding: f,
shutdown: make(chan struct{}),
}
bt.DataHolder = data.NewHandlerHolder()
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 !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bt.DataHolder.SetDataForCurrency(ex, a, cp, k)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
bt.MetaData.DateLoaded = time.Now()
err = bt.Run()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
func TestStop(t *testing.T) {
t.Parallel()
bt := &BackTest{
shutdown: make(chan struct{}),
Statistic: &fakeStats{},
Reports: &fakeReport{},
}
err := bt.Stop()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
tt := bt.MetaData.DateEnded
err = bt.Stop()
if !errors.Is(err, errAlreadyRan) {
t.Errorf("received: %v, expected: %v", err, errAlreadyRan)
}
if !tt.Equal(bt.MetaData.DateEnded) {
t.Errorf("received '%v' expected '%v'", bt.MetaData.DateEnded, tt)
}
bt = nil
err = bt.Stop()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNilPointer)
}
}
func TestFullCycleMulti(t *testing.T) {
t.Parallel()
ex := testExchange
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
tt := time.Now()
stats := &statistics.Statistic{}
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex][a] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic)
stats.ExchangeAssetPairStatistics[ex][a][cp.Base.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic)
port, err := portfolio.Setup(&size.Size{
BuySide: exchange.MinMax{},
SellSide: exchange.MinMax{},
}, &risk.Risk{}, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = port.SetCurrencySettingsMap(&exchange.Settings{Exchange: &binance.Binance{}, Asset: a, Pair: cp})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true, false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
b, err := funding.CreateItem(ex, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
quote, err := funding.CreateItem(ex, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
pair, err := funding.CreatePair(b, quote)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = f.AddPair(pair)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
bt := BackTest{
DataHolder: &data.HandlerHolder{},
Portfolio: port,
Exchange: &exchange.Exchange{},
Statistic: stats,
EventQueue: &eventholder.Holder{},
Reports: &report.Data{},
Funding: f,
hasProcessedDataAtOffset: make(map[int64]bool),
shutdown: make(chan struct{}),
}
bt.Strategy, err = strategies.LoadStrategyByName(dollarcostaverage.Name, true)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
bt.DataHolder = data.NewHandlerHolder()
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 !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bt.DataHolder.SetDataForCurrency(ex, a, cp, k)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bt.Run()
if !errors.Is(err, errNotSetup) {
t.Errorf("received: %v, expected: %v", err, errNotSetup)
}
bt.MetaData.DateLoaded = time.Now()
err = bt.Run()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
type portfolioOverride struct {
Err error
portfolio.Portfolio
}
func (p portfolioOverride) CreateLiquidationOrdersForExchange(ev data.Event, _ funding.IFundingManager) ([]order.Event, error) {
if p.Err != nil {
return nil, p.Err
}
return []order.Event{
&order.Order{
Base: ev.GetBase(),
ID: "1",
Direction: gctorder.Short,
},
}, nil
}
func TestTriggerLiquidationsForExchange(t *testing.T) {
t.Parallel()
bt := BackTest{
shutdown: make(chan struct{}),
}
expectedError := common.ErrNilEvent
err := bt.triggerLiquidationsForExchange(nil, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.USDTMarginedFutures
expectedError = gctcommon.ErrNilPointer
ev := &evkline.Kline{
Base: &event.Base{Exchange: testExchange,
AssetType: a,
CurrencyPair: cp},
}
err = bt.triggerLiquidationsForExchange(ev, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
bt.Portfolio = &portfolioOverride{}
pnl := &portfolio.PNLSummary{}
bt.DataHolder = &data.HandlerHolder{}
d := &data.Base{}
err = d.SetStream([]data.Event{&evkline.Kline{
Base: &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.OneDay,
CurrencyPair: cp,
AssetType: a,
},
Open: leet,
Close: leet,
Low: leet,
High: leet,
Volume: leet,
}})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
da := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: testExchange,
Asset: a,
Pair: cp,
},
Base: d,
RangeHolder: &gctkline.IntervalRangeHolder{},
}
bt.Statistic = &statistics.Statistic{}
expectedError = nil
bt.EventQueue = &eventholder.Holder{}
bt.Funding = &funding.FundManager{}
err = bt.DataHolder.SetDataForCurrency(testExchange, a, cp, da)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bt.Statistic.SetEventForOffset(ev)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
pnl.Exchange = ev.Exchange
pnl.Asset = ev.AssetType
pnl.Pair = ev.CurrencyPair
err = bt.triggerLiquidationsForExchange(ev, pnl)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
ev2 := bt.EventQueue.NextEvent()
ev2o, ok := ev2.(order.Event)
if !ok {
t.Fatal("expected order event")
}
if ev2o.GetDirection() != gctorder.Short {
t.Error("expected liquidation order")
}
}
func TestUpdateStatsForDataEvent(t *testing.T) {
t.Parallel()
bt := &BackTest{
Statistic: &fakeStats{},
Funding: &funding.FundManager{},
Portfolio: &fakeFolio{},
shutdown: make(chan struct{}),
}
expectedError := common.ErrNilEvent
err := bt.updateStatsForDataEvent(nil, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Futures
ev := &evkline.Kline{
Base: &event.Base{
Exchange: testExchange,
AssetType: a,
CurrencyPair: cp,
},
}
expectedError = gctcommon.ErrNilPointer
err = bt.updateStatsForDataEvent(ev, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = nil
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
b, err := funding.CreateItem(testExchange, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
quote, err := funding.CreateItem(testExchange, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
pair, err := funding.CreateCollateral(b, quote)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
bt.Funding = f
exch := &binance.Binance{}
exch.Name = testExchange
ev.Time = time.Now()
fl := &fill.Fill{
Base: ev.Base,
Direction: gctorder.Short,
Amount: decimal.NewFromInt(1),
ClosePrice: decimal.NewFromInt(1),
VolumeAdjustedPrice: decimal.NewFromInt(1),
PurchasePrice: decimal.NewFromInt(1),
Total: decimal.NewFromInt(1),
Slippage: decimal.NewFromInt(1),
Order: &gctorder.Detail{
Exchange: testExchange,
AssetType: ev.AssetType,
Pair: cp,
Amount: 1,
Price: 1,
Side: gctorder.Short,
OrderID: "1",
Date: time.Now(),
},
}
_, err = bt.Portfolio.TrackFuturesOrder(fl, pair)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
err = bt.updateStatsForDataEvent(ev, pair)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
}
func TestProcessSignalEvent(t *testing.T) {
t.Parallel()
var expectedError error
bt := &BackTest{
Statistic: &fakeStats{},
Funding: &funding.FundManager{},
Portfolio: &fakeFolio{},
Exchange: &exchange.Exchange{},
EventQueue: &eventholder.Holder{},
shutdown: make(chan struct{}),
}
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.USDTMarginedFutures
de := &evkline.Kline{
Base: &event.Base{Exchange: testExchange,
AssetType: a,
CurrencyPair: cp},
}
err := bt.Statistic.SetEventForOffset(de)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
ev := &signal.Signal{
Base: de.Base,
}
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
b, err := funding.CreateItem(testExchange, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
quote, err := funding.CreateItem(testExchange, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
pair, err := funding.CreateCollateral(b, quote)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
bt.Funding = f
exch := &binance.Binance{}
exch.Name = testExchange
bt.Exchange.SetExchangeAssetCurrencySettings(a, cp, &exchange.Settings{
Exchange: exch,
Pair: cp,
Asset: a,
})
ev.Direction = gctorder.Short
err = bt.Statistic.SetEventForOffset(ev)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
err = bt.processSignalEvent(ev, pair)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
}
func TestProcessOrderEvent(t *testing.T) {
t.Parallel()
var expectedError error
pt, err := portfolio.Setup(&size.Size{}, &risk.Risk{}, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
bt := &BackTest{
Statistic: &statistics.Statistic{},
Funding: &funding.FundManager{},
Portfolio: pt,
Exchange: &exchange.Exchange{},
EventQueue: &eventholder.Holder{},
DataHolder: &data.HandlerHolder{},
shutdown: make(chan struct{}),
}
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.USDTMarginedFutures
de := &evkline.Kline{
Base: &event.Base{Exchange: testExchange,
AssetType: a,
CurrencyPair: cp},
}
err = bt.Statistic.SetEventForOffset(de)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
ev := &order.Order{
Base: de.Base,
}
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
b, err := funding.CreateItem(testExchange, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
quote, err := funding.CreateItem(testExchange, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
pair, err := funding.CreateCollateral(b, quote)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
bt.Funding = f
exch := &binance.Binance{}
exch.Name = testExchange
err = pt.SetCurrencySettingsMap(&exchange.Settings{
Exchange: exch,
Pair: cp,
Asset: a,
})
if !errors.Is(err, gctcommon.ErrNotYetImplemented) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNotYetImplemented)
}
bt.Exchange.SetExchangeAssetCurrencySettings(a, cp, &exchange.Settings{
Exchange: exch,
Pair: cp,
Asset: a,
})
ev.Direction = gctorder.Short
err = bt.Statistic.SetEventForOffset(ev)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
tt := time.Now()
bt.DataHolder = data.NewHandlerHolder()
k := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: testExchange,
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 !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bt.DataHolder.SetDataForCurrency(testExchange, a, cp, k)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bt.processOrderEvent(ev, pair)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
ev2 := bt.EventQueue.NextEvent()
if _, ok := ev2.(fill.Event); !ok {
t.Fatal("expected fill event")
}
}
func TestProcessFillEvent(t *testing.T) {
t.Parallel()
bt := &BackTest{
Statistic: &fakeStats{},
Funding: &fakeFunding{},
Portfolio: &fakeFolio{},
Exchange: &exchange.Exchange{},
EventQueue: &eventholder.Holder{},
DataHolder: &data.HandlerHolder{},
shutdown: make(chan struct{}),
}
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Futures
tt := time.Now()
de := &evkline.Kline{
Base: &event.Base{
Exchange: testExchange,
AssetType: a,
CurrencyPair: cp,
Time: tt,
},
}
err := bt.Statistic.SetEventForOffset(de)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
ev := &fill.Fill{
Base: de.Base,
}
em := engine.SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
em.Add(exch)
b, err := funding.CreateItem(testExchange, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
quote, err := funding.CreateItem(testExchange, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
pair, err := funding.CreateCollateral(b, quote)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.Exchange.SetExchangeAssetCurrencySettings(a, cp, &exchange.Settings{
Exchange: exch,
Pair: cp,
Asset: a,
})
ev.Direction = gctorder.Short
err = bt.Statistic.SetEventForOffset(ev)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.DataHolder = data.NewHandlerHolder()
k := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: testExchange,
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 !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bt.DataHolder.SetDataForCurrency(testExchange, a, cp, k)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bt.processFillEvent(ev, pair)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}
func TestProcessFuturesFillEvent(t *testing.T) {
t.Parallel()
var expectedError error
bt := &BackTest{
Statistic: &fakeStats{},
Funding: &funding.FundManager{},
Portfolio: &fakeFolio{},
Exchange: &exchange.Exchange{},
EventQueue: &eventholder.Holder{},
DataHolder: &data.HandlerHolder{},
shutdown: make(chan struct{}),
}
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Futures
de := &evkline.Kline{
Base: &event.Base{
Exchange: testExchange,
AssetType: a,
CurrencyPair: cp},
}
err := bt.Statistic.SetEventForOffset(de)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
ev := &fill.Fill{
Base: de.Base,
}
em := engine.SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
em.Add(exch)
b, err := funding.CreateItem(testExchange, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
quote, err := funding.CreateItem(testExchange, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
pair, err := funding.CreateCollateral(b, quote)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
bt.exchangeManager = em
bt.Exchange.SetExchangeAssetCurrencySettings(a, cp, &exchange.Settings{
Exchange: exch,
Pair: cp,
Asset: a,
})
ev.Direction = gctorder.Short
err = bt.Statistic.SetEventForOffset(ev)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
tt := time.Now()
bt.DataHolder = data.NewHandlerHolder()
k := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: testExchange,
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 !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
ev.Order = &gctorder.Detail{
Exchange: testExchange,
AssetType: ev.AssetType,
Pair: cp,
Amount: 1,
Price: 1,
Side: gctorder.Short,
OrderID: "1",
Date: time.Now(),
}
err = bt.DataHolder.SetDataForCurrency(testExchange, a, cp, k)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bt.processFuturesFillEvent(ev, pair)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
}
func TestCloseAllPositions(t *testing.T) {
t.Parallel()
bt, err := NewBacktester()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
pt := &portfolio.Portfolio{}
bt.Portfolio = pt
bt.Strategy = &dollarcostaverage.Strategy{}
err = bt.CloseAllPositions()
if !errors.Is(err, errLiveOnly) {
t.Errorf("received '%v' expected '%v'", err, errLiveOnly)
}
bt.shutdown = make(chan struct{})
dc := &dataChecker{
realOrders: true,
shutdown: make(chan bool),
}
bt.LiveDataHandler = dc
err = bt.CloseAllPositions()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
bt.shutdown = make(chan struct{})
bt.Strategy = &binancecashandcarry.Strategy{}
err = bt.CloseAllPositions()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
bt.shutdown = make(chan struct{})
bt.Portfolio = &fakeFolio{}
bt.Strategy = &fakeStrat{}
bt.Exchange = &exchange.Exchange{}
bt.Statistic = &fakeStats{}
bt.Reports = &fakeReport{}
bt.Funding = &fakeFunding{}
bt.DataHolder = &fakeDataHolder{}
dc.dataHolder = bt.DataHolder
dc.report = &report.Data{}
dc.funding = bt.Funding
cp := currency.NewPair(currency.BTC, currency.USD)
dc.sourcesToCheck = append(dc.sourcesToCheck, &liveDataSourceDataHandler{
exchange: &binance.Binance{},
exchangeName: testExchange,
asset: asset.Spot,
pair: cp,
underlyingPair: cp,
dataType: common.DataCandle,
dataRequestRetryTolerance: 1,
pairCandles: &kline.DataFromKline{
Base: &data.Base{},
Item: gctkline.Item{
Exchange: testExchange,
Pair: cp,
UnderlyingPair: cp,
Asset: asset.Spot,
Interval: gctkline.OneMin,
Candles: []gctkline.Candle{
{
Time: time.Now(),
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
},
},
},
},
})
err = bt.CloseAllPositions()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}
func TestRunLive(t *testing.T) {
t.Parallel()
bt, err := NewBacktester()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bt.RunLive()
if !errors.Is(err, errLiveOnly) {
t.Errorf("received '%v' expected '%v'", err, errLiveOnly)
}
bt.Funding = &funding.FundManager{}
bt.Reports = &report.Data{}
dc := &dataChecker{
exchangeManager: bt.exchangeManager,
eventTimeout: defaultEventTimeout,
dataCheckInterval: defaultDataCheckInterval,
dataHolder: bt.DataHolder,
report: bt.Reports,
funding: bt.Funding,
shutdown: make(chan bool),
}
bt.LiveDataHandler = dc
err = bt.RunLive()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
close(bt.shutdown)
bt.wg.Wait()
bt.shutdown = make(chan struct{})
dc = &dataChecker{
exchangeManager: bt.exchangeManager,
eventTimeout: defaultEventTimeout,
dataCheckInterval: defaultDataCheckInterval,
dataHolder: bt.DataHolder,
report: bt.Reports,
shutdown: make(chan bool),
dataUpdated: make(chan bool),
shutdownErr: make(chan bool),
funding: bt.Funding,
}
bt.LiveDataHandler = dc
cp := currency.NewPair(currency.BTC, currency.USD)
i := &gctkline.Item{
Exchange: testExchange,
Pair: cp,
UnderlyingPair: cp,
Asset: asset.Spot,
Interval: gctkline.FifteenSecond,
Candles: []gctkline.Candle{
{
Time: time.Now(),
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
},
},
}
// AppendDataSource(exchange gctexchange.IBotExchange, interval gctkline.Interval, asset asset.Asset, pair, underlyingPair currency.Pair, dataType int64) error
setup := &liveDataSourceSetup{
exchange: &binance.Binance{},
interval: i.Interval,
asset: i.Asset,
pair: i.Pair,
underlyingPair: i.UnderlyingPair,
dataType: common.DataCandle,
}
err = dc.AppendDataSource(setup)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.Reports = &report.Data{}
bt.Funding = &fakeFunding{}
bt.Statistic = &fakeStats{}
dc.started = 0
err = bt.RunLive()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}
func TestLiveLoop(t *testing.T) {
t.Parallel()
bt, err := NewBacktester()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.Reports = &fakeReport{}
bt.Funding = &fakeFunding{}
bt.Statistic = &fakeStats{}
dc := &dataChecker{
dataUpdated: make(chan bool),
shutdownErr: make(chan bool),
shutdown: make(chan bool),
}
bt.LiveDataHandler = dc
// dataUpdated case
var wg sync.WaitGroup
wg.Add(1)
go func() {
err = bt.liveCheck()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
wg.Done()
}()
dc.dataUpdated <- true
dc.shutdown <- true
wg.Wait()
// shutdown from error case
wg.Add(1)
dc.started = 0
go func() {
defer wg.Done()
err = bt.liveCheck()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}()
dc.shutdownErr <- true
wg.Wait()
// shutdown case
dc.started = 1
bt.shutdown = make(chan struct{})
wg.Add(1)
go func() {
defer wg.Done()
err = bt.liveCheck()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}()
dc.shutdown <- true
wg.Wait()
// backtester has shutdown
wg.Add(1)
bt.shutdown = make(chan struct{})
go func() {
defer wg.Done()
err = bt.liveCheck()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}()
close(bt.shutdown)
wg.Wait()
}
func TestSetExchangeCredentials(t *testing.T) {
t.Parallel()
err := setExchangeCredentials(nil, nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
cfg := &config.Config{}
f := &binance.Binance{}
f.SetDefaults()
b := f.GetBase()
err = setExchangeCredentials(cfg, b)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
ld := &config.LiveData{}
cfg.DataSettings = config.DataSettings{
LiveData: ld,
}
err = setExchangeCredentials(cfg, b)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
ld.RealOrders = true
err = setExchangeCredentials(cfg, b)
if !errors.Is(err, errIntervalUnset) {
t.Errorf("received '%v' expected '%v'", err, errIntervalUnset)
}
cfg.DataSettings.Interval = gctkline.OneMin
err = setExchangeCredentials(cfg, b)
if !errors.Is(err, errNoCredsNoLive) {
t.Errorf("received '%v' expected '%v'", err, errNoCredsNoLive)
}
cfg.DataSettings.LiveData.ExchangeCredentials = []config.Credentials{{}}
err = setExchangeCredentials(cfg, b)
if !errors.Is(err, gctexchange.ErrCredentialsAreEmpty) {
t.Errorf("received '%v' expected '%v'", err, gctexchange.ErrCredentialsAreEmpty)
}
// requires valid credentials here to get complete coverage
// enter them here
cfg.DataSettings.LiveData.ExchangeCredentials = []config.Credentials{{
Exchange: testExchange,
Keys: account.Credentials{
Key: "test",
Secret: "test",
},
}}
err = setExchangeCredentials(cfg, b)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}
func TestGetFees(t *testing.T) {
t.Parallel()
_, _, err := getFees(context.Background(), nil, currency.EMPTYPAIR)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
f := &binance.Binance{}
f.SetDefaults()
_, _, err = getFees(context.Background(), f, currency.EMPTYPAIR)
if !errors.Is(err, currency.ErrCurrencyPairEmpty) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrCurrencyPairEmpty)
}
maker, taker, err := getFees(context.Background(), f, currency.NewPair(currency.BTC, currency.USDT))
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrCurrencyPairEmpty)
}
if maker.IsZero() || taker.IsZero() {
t.Error("expected maker and taker fees")
}
}
func TestGenerateSummary(t *testing.T) {
t.Parallel()
bt := &BackTest{
shutdown: make(chan struct{}),
}
sum, err := bt.GenerateSummary()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !sum.MetaData.ID.IsNil() {
t.Errorf("received '%v' expected '%v'", sum.MetaData.ID, "")
}
id, err := uuid.NewV4()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.MetaData.ID = id
sum, err = bt.GenerateSummary()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if sum.MetaData.ID != id {
t.Errorf("received '%v' expected '%v'", sum.MetaData.ID, id)
}
bt = nil
_, err = bt.GenerateSummary()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestSetupMetaData(t *testing.T) {
t.Parallel()
bt := &BackTest{
shutdown: make(chan struct{}),
}
err := bt.SetupMetaData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.MetaData.ID.IsNil() {
t.Errorf("received '%v' expected '%v'", bt.MetaData.ID, "an ID")
}
firstID := bt.MetaData.ID
err = bt.SetupMetaData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.MetaData.ID != firstID {
t.Errorf("received '%v' expected '%v'", bt.MetaData.ID, firstID)
}
bt = nil
err = bt.SetupMetaData()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestIsRunning(t *testing.T) {
t.Parallel()
bt := &BackTest{
shutdown: make(chan struct{}),
}
if bt.IsRunning() {
t.Errorf("received '%v' expected '%v'", true, false)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Now()
bt.m.Unlock()
if !bt.IsRunning() {
t.Errorf("received '%v' expected '%v'", false, true)
}
bt.m.Lock()
bt.MetaData.Closed = true
bt.m.Unlock()
if bt.IsRunning() {
t.Errorf("received '%v' expected '%v'", true, false)
}
bt = nil
if bt.IsRunning() {
t.Errorf("received '%v' expected '%v'", true, false)
}
}
func TestHasRan(t *testing.T) {
t.Parallel()
bt := &BackTest{
shutdown: make(chan struct{}),
}
if bt.HasRan() {
t.Errorf("received '%v' expected '%v'", true, false)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Now()
bt.m.Unlock()
if bt.HasRan() {
t.Errorf("received '%v' expected '%v'", false, true)
}
bt.m.Lock()
bt.MetaData.Closed = true
bt.m.Unlock()
if !bt.HasRan() {
t.Errorf("received '%v' expected '%v'", true, false)
}
bt = nil
if bt.HasRan() {
t.Errorf("received '%v' expected '%v'", true, false)
}
}
func TestEqual(t *testing.T) {
t.Parallel()
bt := &BackTest{}
bt2 := &BackTest{}
bt3 := &BackTest{}
if !bt.Equal(bt2) {
t.Errorf("received '%v' expected '%v'", false, true)
}
err := bt.SetupMetaData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt2.MetaData = bt.MetaData
if !bt.Equal(bt2) {
t.Errorf("received '%v' expected '%v'", false, true)
}
if bt.Equal(nil) {
t.Errorf("received '%v' expected '%v'", true, false)
}
err = bt3.SetupMetaData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.Equal(bt3) {
t.Errorf("received '%v' expected '%v'", true, false)
}
bt = nil
if bt.Equal(bt2) {
t.Errorf("received '%v' expected '%v'", true, false)
}
}
func TestMatchesID(t *testing.T) {
t.Parallel()
bt := &BackTest{}
if bt.MatchesID(uuid.Nil) {
t.Errorf("received '%v' expected '%v'", true, false)
}
err := bt.SetupMetaData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.MatchesID(uuid.Nil) {
t.Errorf("received '%v' expected '%v'", true, false)
}
if !bt.MatchesID(bt.MetaData.ID) {
t.Errorf("received '%v' expected '%v'", false, true)
}
id := bt.MetaData.ID
bt.MetaData.ID = uuid.Nil
if bt.MatchesID(id) {
t.Errorf("received '%v' expected '%v'", true, false)
}
bt = nil
if bt.MatchesID(id) {
t.Errorf("received '%v' expected '%v'", true, false)
}
}
func TestExecuteStrategy(t *testing.T) {
t.Parallel()
bt := &BackTest{
DataHolder: &fakeDataHolder{},
Strategy: &fakeStrat{},
Portfolio: &fakeFolio{},
Statistic: &fakeStats{},
Reports: &fakeReport{},
Funding: &fakeFunding{},
EventQueue: &eventholder.Holder{},
shutdown: make(chan struct{}),
}
err := bt.ExecuteStrategy(false)
if !errors.Is(err, errNotSetup) {
t.Errorf("received '%v' expected '%v'", err, errNotSetup)
}
id, err := uuid.NewV4()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.m.Lock()
bt.MetaData.ID = id
bt.MetaData.DateLoaded = time.Now()
bt.MetaData.DateStarted = time.Now()
bt.m.Unlock()
err = bt.ExecuteStrategy(false)
if !errors.Is(err, errTaskIsRunning) {
t.Errorf("received '%v' expected '%v'", err, errTaskIsRunning)
}
err = bt.Stop()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bt.ExecuteStrategy(true)
if !errors.Is(err, errAlreadyRan) {
t.Errorf("received '%v' expected '%v'", err, errAlreadyRan)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Time{}
bt.MetaData.DateEnded = time.Time{}
bt.MetaData.Closed = false
bt.shutdown = make(chan struct{})
bt.m.Unlock()
err = bt.ExecuteStrategy(true)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Time{}
bt.MetaData.DateEnded = time.Time{}
bt.MetaData.Closed = false
bt.shutdown = make(chan struct{})
bt.m.Unlock()
err = bt.ExecuteStrategy(false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.m.Lock()
bt.MetaData.LiveTesting = true
bt.MetaData.DateStarted = time.Time{}
bt.MetaData.DateEnded = time.Time{}
bt.MetaData.Closed = false
bt.shutdown = make(chan struct{})
bt.m.Unlock()
err = bt.ExecuteStrategy(true)
if !errors.Is(err, errCannotHandleRequest) {
t.Errorf("received '%v' expected '%v'", err, errCannotHandleRequest)
}
err = bt.ExecuteStrategy(false)
if !errors.Is(err, errLiveOnly) {
t.Errorf("received '%v' expected '%v'", err, errLiveOnly)
}
bt = nil
err = bt.ExecuteStrategy(false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestNewBacktesterFromConfigs(t *testing.T) {
t.Parallel()
_, err := NewBacktesterFromConfigs(nil, nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
strat1 := filepath.Join("..", "config", "strategyexamples", "dca-api-candles.strat")
cfg, err := config.ReadStrategyConfigFromFile(strat1)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
dc, err := config.GenerateDefaultConfig()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = NewBacktesterFromConfigs(cfg, nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
_, err = NewBacktesterFromConfigs(nil, dc)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
bt, err := NewBacktesterFromConfigs(cfg, dc)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.MetaData.DateLoaded.IsZero() {
t.Errorf("received '%v' expected '%v'", bt.MetaData.DateLoaded, "a date")
}
}
func TestProcessSingleDataEvent(t *testing.T) {
t.Parallel()
bt := &BackTest{
Strategy: &fakeStrat{},
Portfolio: &fakeFolio{},
Statistic: &fakeStats{},
Reports: &fakeReport{},
Funding: &fakeFunding{},
DataHolder: &data.HandlerHolder{},
EventQueue: &eventholder.Holder{},
}
err := bt.processSingleDataEvent(nil, nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received '%v' expected '%v'", err, common.ErrNilEvent)
}
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
ev := &evkline.Kline{
Base: &event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.FifteenMin,
CurrencyPair: cp,
AssetType: a,
},
}
err = bt.processSingleDataEvent(ev, nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true, false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
b, err := funding.CreateItem(testExchange, a, cp.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
quote, err := funding.CreateItem(testExchange, a, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
collateral, err := funding.CreateCollateral(b, quote)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.Funding = f
tt := time.Now()
bt.DataHolder = data.NewHandlerHolder()
k := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: testExchange,
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 !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bt.DataHolder.SetDataForCurrency(testExchange, a, cp, k)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bt.processSingleDataEvent(ev, collateral)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}