Files
gocryptotrader/backtester/engine/setup.go
Scott f929b4d51e 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
2022-06-30 15:43:41 +10:00

868 lines
29 KiB
Go

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
}