diff --git a/.appveyor.yml b/.appveyor.yml
index f9b5938d67f..122e2005c1c 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -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') {
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 8307ca05fa0..e95c7b3a206 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -12,4 +12,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
- version: v1.45.2
+ version: v1.46.2
diff --git a/Makefile b/Makefile
index c17e1b3c0e7..2265d82f3c9 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
index 8b4b909da75..32453ead57e 100644
--- a/README.md
+++ b/README.md
@@ -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 |
diff --git a/backtester/README.md b/backtester/README.md
index a428001a5e9..b31f9bb9de6 100644
--- a/backtester/README.md
+++ b/backtester/README.md
@@ -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 |
diff --git a/backtester/backtest/backtest.go b/backtester/backtest/backtest.go
deleted file mode 100644
index a5650a9ae45..00000000000
--- a/backtester/backtest/backtest.go
+++ /dev/null
@@ -1,1198 +0,0 @@
-package backtest
-
-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"
- "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/data/kline/live"
- "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
- "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/compliance"
- "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
- "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/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/backtester/funding/trackingcurrencies"
- "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"
- 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"
-)
-
-// 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
-}
-
-// NewFromConfig takes a strategy config and configures a backtester variable to run
-func NewFromConfig(cfg *config.Config, templatePath, output string) (*BackTest, error) {
- log.Infoln(log.BackTester, "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)
- 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 := funding.SetupFundingManager(
- cfg.StrategySettings.UseExchangeLevelFunding,
- cfg.StrategySettings.DisableUSDTracking,
- )
- if cfg.StrategySettings.UseExchangeLevelFunding {
- for i := range cfg.StrategySettings.ExchangeLevelFunding {
- var a asset.Item
- a, err = asset.New(cfg.StrategySettings.ExchangeLevelFunding[i].Asset)
- if err != nil {
- return nil, err
- }
- cq := currency.NewCode(cfg.StrategySettings.ExchangeLevelFunding[i].Currency)
- var item *funding.Item
- item, err = funding.CreateItem(cfg.StrategySettings.ExchangeLevelFunding[i].ExchangeName,
- a,
- cq,
- cfg.StrategySettings.ExchangeLevelFunding[i].InitialFunds,
- cfg.StrategySettings.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
- }
- _, err = exch.GetDefaultConfig()
- 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)
- }
- var a asset.Item
- a, err = asset.New(cfg.CurrencySettings[i].Asset)
- if err != nil {
- return nil, fmt.Errorf(
- "%w for %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 = currency.NewCode(cfg.CurrencySettings[i].Base)
- q = currency.NewCode(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 format currency %v, %w", curr, err)
- }
- curr = curr.Format(requestFormat.Delimiter, requestFormat.Uppercase)
- err = exchBase.CurrencyPairs.EnablePair(a, curr)
- if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) {
- return nil, fmt.Errorf(
- "could not enable currency %v %v %v. Err %w",
- cfg.CurrencySettings[i].ExchangeName,
- cfg.CurrencySettings[i].Asset,
- cfg.CurrencySettings[i].Base+cfg.CurrencySettings[i].Quote,
- err)
- }
- portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][curr] = &risk.CurrencySettings{
- MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].Leverage.MaximumOrdersWithLeverageRatio,
- MaxLeverageRate: cfg.CurrencySettings[i].Leverage.MaximumLeverageRate,
- MaximumHoldingRatio: cfg.CurrencySettings[i].MaximumHoldingsRatio,
- }
- if cfg.CurrencySettings[i].MakerFee.GreaterThan(cfg.CurrencySettings[i].TakerFee) {
- log.Warnf(log.BackTester, "maker fee '%v' should not exceed taker fee '%v'. Please review config",
- cfg.CurrencySettings[i].MakerFee,
- cfg.CurrencySettings[i].TakerFee)
- }
-
- var baseItem, quoteItem *funding.Item
- if cfg.StrategySettings.UseExchangeLevelFunding {
- // 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
- }
- } else {
- var bFunds, qFunds decimal.Decimal
- if cfg.CurrencySettings[i].InitialBaseFunds != nil {
- bFunds = *cfg.CurrencySettings[i].InitialBaseFunds
- }
- if cfg.CurrencySettings[i].InitialQuoteFunds != nil {
- qFunds = *cfg.CurrencySettings[i].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.Pair
- 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: gctkline.Interval(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 == trackingPairs[i].Base &&
- cfg.CurrencySettings[j].Quote == 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 {
- var lookup *portfolio.Settings
-
- lookup, err = p.SetupCurrencySettingsMap(&e.CurrencySettings[i])
- if err != nil {
- return nil, err
- }
- lookup.Fee = e.CurrencySettings[i].TakerFee
- lookup.Leverage = e.CurrencySettings[i].Leverage
- lookup.BuySideSizing = e.CurrencySettings[i].BuySide
- lookup.SellSideSizing = e.CurrencySettings[i].SellSide
- lookup.ComplianceManager = compliance.Manager{
- Snapshots: []compliance.Snapshot{},
- }
- }
- bt.Portfolio = p
-
- cfg.PrintSetting()
-
- return bt, nil
-}
-
-func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange, error) {
- log.Infoln(log.BackTester, "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 {
- bt.Datas.SetDataForCurrency(exchangeName, a, pair, klineData)
- var makerFee, takerFee decimal.Decimal
- if cfg.CurrencySettings[i].MakerFee.GreaterThan(decimal.Zero) {
- makerFee = cfg.CurrencySettings[i].MakerFee
- }
- if cfg.CurrencySettings[i].TakerFee.GreaterThan(decimal.Zero) {
- takerFee = cfg.CurrencySettings[i].TakerFee
- }
- if makerFee.IsZero() || takerFee.IsZero() {
- var apiMakerFee, apiTakerFee decimal.Decimal
- apiMakerFee, apiTakerFee = getFees(context.TODO(), exch, pair)
- if makerFee.IsZero() {
- makerFee = apiMakerFee
- }
- if takerFee.IsZero() {
- takerFee = apiTakerFee
- }
- }
-
- if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(decimal.Zero) {
- log.Warnf(log.BackTester, "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(log.BackTester, "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(log.BackTester, "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
- }
- }
-
- resp.CurrencySettings = append(resp.CurrencySettings, exchange.Settings{
- Exchange: cfg.CurrencySettings[i].ExchangeName,
- MinimumSlippageRate: cfg.CurrencySettings[i].MinimumSlippagePercent,
- MaximumSlippageRate: cfg.CurrencySettings[i].MaximumSlippagePercent,
- Pair: pair,
- Asset: a,
- ExchangeFee: takerFee,
- MakerFee: takerFee,
- TakerFee: makerFee,
- UseRealOrders: realOrders,
- BuySide: buyRule,
- SellSide: sellRule,
- Leverage: exchange.Leverage{
- CanUseLeverage: cfg.CurrencySettings[i].Leverage.CanUseLeverage,
- MaximumLeverageRate: cfg.CurrencySettings[i].Leverage.MaximumLeverageRate,
- MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].Leverage.MaximumOrdersWithLeverageRatio,
- },
- Limits: limits,
- SkipCandleVolumeFitting: cfg.CurrencySettings[i].SkipCandleVolumeFitting,
- CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits,
- })
- }
- }
-
- return resp, nil
-}
-
-func (bt *BackTest) loadExchangePairAssetBase(exch, base, quote, ass string) (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, err = currency.NewPairFromStrings(base, quote)
- if err != nil {
- return nil, currency.EMPTYPAIR, asset.Empty, err
- }
-
- var a asset.Item
- a, err = asset.New(ass)
- if err != nil {
- return nil, currency.EMPTYPAIR, asset.Empty, err
- }
-
- exchangeBase := e.GetBase()
- if exchangeBase.ValidateAPICredentials(exchangeBase.GetDefaultCredentials()) != nil {
- log.Warnf(log.BackTester, "no credentials set for %v, this is theoretical only", exchangeBase.Name)
- }
-
- fPair, err = exchangeBase.FormatExchangeCurrency(cp, a)
- if err != nil {
- return nil, currency.EMPTYPAIR, asset.Empty, err
- }
- return e, fPair, a, 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(log.BackTester, "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(log.BackTester, "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(log.BackTester, "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,
- 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),
- gctkline.Interval(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(log.BackTester, "%v", summary)
- }
- case cfg.DataSettings.DatabaseData != nil:
- if cfg.DataSettings.DatabaseData.InclusiveEndDate {
- cfg.DataSettings.DatabaseData.EndDate = cfg.DataSettings.DatabaseData.EndDate.Add(cfg.DataSettings.Interval)
- }
- 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(log.BackTester, 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,
- gctkline.Interval(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(log.BackTester, "%v", summary)
- }
- case cfg.DataSettings.APIData != nil:
- if cfg.DataSettings.APIData.InclusiveEndDate {
- cfg.DataSettings.APIData.EndDate = cfg.DataSettings.APIData.EndDate.Add(cfg.DataSettings.Interval)
- }
- 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")
- }
-
- 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,
- 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,
- gctkline.Interval(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,
- 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(log.BackTester, "%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(log.BackTester, "invalid API credentials set, real orders set to false")
- cfg.DataSettings.LiveData.RealOrders = false
- }
- return nil
-}
-
-// Run will iterate over loaded data events
-// save them and then handle the event based on its type
-func (bt *BackTest) Run() error {
- log.Info(log.BackTester, "running backtester against pre-defined data")
-dataLoadingIssue:
- for ev := bt.EventQueue.NextEvent(); ; ev = bt.EventQueue.NextEvent() {
- if ev == nil {
- dataHandlerMap := bt.Datas.GetAllData()
- for exchangeName, exchangeMap := range dataHandlerMap {
- for assetItem, assetMap := range exchangeMap {
- var hasProcessedData bool
- for currencyPair, dataHandler := range assetMap {
- d := dataHandler.Next()
- if d == nil {
- if !bt.hasHandledEvent {
- log.Errorf(log.BackTester, "Unable to perform `Next` for %v %v %v", exchangeName, assetItem, currencyPair)
- }
- break dataLoadingIssue
- }
- if bt.Strategy.UsingSimultaneousProcessing() && hasProcessedData {
- continue
- }
- bt.EventQueue.AppendEvent(d)
- hasProcessedData = true
- }
- }
- }
- }
- if ev != nil {
- err := bt.handleEvent(ev)
- if err != nil {
- return err
- }
- }
- if !bt.hasHandledEvent {
- bt.hasHandledEvent = true
- }
- }
-
- return nil
-}
-
-// 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 {
- funds, err := bt.Funding.GetFundingForEvent(ev)
- if err != nil {
- return err
- }
- switch eType := ev.(type) {
- case common.DataEventHandler:
- if bt.Strategy.UsingSimultaneousProcessing() {
- err = bt.processSimultaneousDataEvents()
- if err != nil {
- return err
- }
- bt.Funding.CreateSnapshot(ev.GetTime())
- return nil
- }
- err = bt.processSingleDataEvent(eType, funds)
- if err != nil {
- return err
- }
- bt.Funding.CreateSnapshot(ev.GetTime())
- return nil
- case signal.Event:
- bt.processSignalEvent(eType, funds)
- case order.Event:
- bt.processOrderEvent(eType, funds)
- case fill.Event:
- bt.processFillEvent(eType, funds)
- default:
- return fmt.Errorf("%w %v received, could not process",
- errUnhandledDatatype,
- ev)
- }
-
- return nil
-}
-
-func (bt *BackTest) processSingleDataEvent(ev common.DataEventHandler, funds funding.IPairReader) error {
- err := bt.updateStatsForDataEvent(ev, funds)
- if err != nil {
- return err
- }
- d := bt.Datas.GetDataForCurrency(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
- 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.Error(log.BackTester, err)
- return nil
- }
- err = bt.Statistic.SetEventForOffset(s)
- if err != nil {
- log.Error(log.BackTester, err)
- }
- bt.EventQueue.AppendEvent(s)
-
- return nil
-}
-
-// processSimultaneousDataEvents determines what signal events are generated and appended
-// to the event queue based on whether it is running a multi-currency consideration strategy order not
-//
-// for multi-currency-consideration it will pass all currency datas to the strategy for it to determine what
-// currencies to act upon
-//
-// for non-multi-currency-consideration strategies, it will simply process every currency individually
-// against the strategy and generate signals
-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.GetFundingForEAP(latestData.GetExchange(), latestData.GetAssetType(), latestData.Pair())
- if err != nil {
- return err
- }
- err = bt.updateStatsForDataEvent(latestData, funds)
- if err != nil && err == statistics.ErrAlreadyProcessed {
- continue
- }
- 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.Error(log.BackTester, err)
- return nil
- }
- for i := range signals {
- err = bt.Statistic.SetEventForOffset(signals[i])
- if err != nil {
- log.Error(log.BackTester, 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.IPairReader) error {
- // update statistics with the latest price
- err := bt.Statistic.SetupEventForTime(ev)
- if err != nil {
- if err == statistics.ErrAlreadyProcessed {
- return err
- }
- log.Error(log.BackTester, err)
- }
- // update portfolio manager with the latest price
- err = bt.Portfolio.UpdateHoldings(ev, funds)
- if err != nil {
- log.Error(log.BackTester, err)
- }
- return nil
-}
-
-// processSignalEvent receives an event from the strategy for processing under the portfolio
-func (bt *BackTest) processSignalEvent(ev signal.Event, funds funding.IPairReserver) {
- cs, err := bt.Exchange.GetCurrencySettings(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
- if err != nil {
- log.Error(log.BackTester, err)
- return
- }
- var o *order.Order
- o, err = bt.Portfolio.OnSignal(ev, &cs, funds)
- if err != nil {
- log.Error(log.BackTester, err)
- return
- }
- err = bt.Statistic.SetEventForOffset(o)
- if err != nil {
- log.Error(log.BackTester, err)
- }
-
- bt.EventQueue.AppendEvent(o)
-}
-
-func (bt *BackTest) processOrderEvent(ev order.Event, funds funding.IPairReleaser) {
- d := bt.Datas.GetDataForCurrency(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
- f, err := bt.Exchange.ExecuteOrder(ev, d, bt.orderManager, funds)
- if err != nil {
- if f == nil {
- log.Errorf(log.BackTester, "fill event should always be returned, please fix, %v", err)
- return
- }
- log.Errorf(log.BackTester, "%v %v %v %v", f.GetExchange(), f.GetAssetType(), f.Pair(), err)
- }
- err = bt.Statistic.SetEventForOffset(f)
- if err != nil {
- log.Error(log.BackTester, err)
- }
- bt.EventQueue.AppendEvent(f)
-}
-
-func (bt *BackTest) processFillEvent(ev fill.Event, funds funding.IPairReader) {
- t, err := bt.Portfolio.OnFill(ev, funds)
- if err != nil {
- log.Error(log.BackTester, err)
- return
- }
-
- err = bt.Statistic.SetEventForOffset(t)
- if err != nil {
- log.Error(log.BackTester, err)
- }
-
- var holding *holdings.Holding
- holding, err = bt.Portfolio.ViewHoldingAtTimePeriod(ev)
- if err != nil {
- log.Error(log.BackTester, err)
- }
-
- err = bt.Statistic.AddHoldingsForTime(holding)
- if err != nil {
- log.Error(log.BackTester, err)
- }
-
- var cp *compliance.Manager
- cp, err = bt.Portfolio.GetComplianceManager(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
- if err != nil {
- log.Error(log.BackTester, err)
- }
-
- snap := cp.GetLatestSnapshot()
- err = bt.Statistic.AddComplianceSnapshotForTime(snap, ev)
- if err != nil {
- log.Error(log.BackTester, err)
- }
-}
-
-// 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(log.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 * 2)
- dates, err := gctkline.CalculateCandleDateRanges(
- startDate,
- startDate.AddDate(1, 0, 0),
- gctkline.Interval(cfg.DataSettings.Interval),
- 0)
- if err != nil {
- log.Errorf(log.BackTester, "%v. Please check your GoCryptoTrader configuration", err)
- return
- }
- candles, err := live.LoadData(context.TODO(),
- exch,
- dataType,
- cfg.DataSettings.Interval,
- fPair,
- a)
- if err != nil {
- log.Errorf(log.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(log.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(log.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,
- fPair,
- a)
- if err != nil {
- return err
- }
- if len(candles.Candles) == 0 {
- return nil
- }
- resp.AppendResults(candles)
- bt.Reports.UpdateItem(&resp.Item)
- log.Info(log.BackTester, "sleeping for 30 seconds before checking for new candle data")
- return nil
-}
-
-// Stop shuts down the live data loop
-func (bt *BackTest) Stop() {
- close(bt.shutdown)
-}
diff --git a/backtester/backtest/backtest_test.go b/backtester/backtest/backtest_test.go
deleted file mode 100644
index b18b3039f0e..00000000000
--- a/backtester/backtest/backtest_test.go
+++ /dev/null
@@ -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] = ¤cy.PairStore{
- Available: currency.Pairs{cp},
- Enabled: currency.Pairs{cp},
- AssetEnabled: convert.BoolPtr(true),
- ConfigFormat: ¤cy.PairFormat{Uppercase: true},
- RequestFormat: ¤cy.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] = ¤cy.PairStore{
- Available: currency.Pairs{cp},
- Enabled: currency.Pairs{cp},
- AssetEnabled: convert.BoolPtr(true),
- ConfigFormat: ¤cy.PairFormat{Uppercase: true},
- RequestFormat: ¤cy.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] = ¤cy.PairStore{
- Available: currency.Pairs{cp},
- Enabled: currency.Pairs{cp},
- AssetEnabled: convert.BoolPtr(true),
- ConfigFormat: ¤cy.PairFormat{Uppercase: true},
- RequestFormat: ¤cy.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] = ¤cy.PairStore{
- Available: currency.Pairs{cp},
- Enabled: currency.Pairs{cp},
- AssetEnabled: convert.BoolPtr(true),
- ConfigFormat: ¤cy.PairFormat{Uppercase: true},
- RequestFormat: ¤cy.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)
- }
-}
diff --git a/backtester/common/common.go b/backtester/common/common.go
index ccb5c4d2bad..5fde4ffb3f1 100644
--- a/backtester/common/common.go
+++ b/backtester/common/common.go
@@ -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()
+}
diff --git a/backtester/common/common_test.go b/backtester/common/common_test.go
index 25992ab80ba..aee94e9fb9d 100644
--- a/backtester/common/common_test.go
+++ b/backtester/common/common_test.go
@@ -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")
+ }
+}
diff --git a/backtester/common/common_types.go b/backtester/common/common_types.go
index 658afae6100..83596106484 100644
--- a/backtester/common/common_types.go
+++ b/backtester/common/common_types.go
@@ -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 = `
/ __ / __ / ___/ //_/ __/ _ \/ ___/ __/ _ \/ ___/
/ /_/ / /_/ / /__/ ,< / /_/ __(__ ) /_/ __/ /
/_____/\__,_/\___/_/|_|\__/\___/____/\__/\___/_/
+
`
diff --git a/backtester/config/README.md b/backtester/config/README.md
index 4e8f75b1f29..7458a2f3770 100644
--- a/backtester/config/README.md
+++ b/backtester/config/README.md
@@ -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 } ` |
+| 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
+
+| 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 | `[]` |
-| 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 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
diff --git a/backtester/config/config.go b/backtester/config/config.go
index c42b5118a9a..1092d5d6fc2 100644
--- a/backtester/config/config.go
+++ b/backtester/config/config.go
@@ -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))
+ }
+}
diff --git a/backtester/config/config_test.go b/backtester/config/config_test.go
index c1f36a801b0..a16f2abe6cc 100644
--- a/backtester/config/config_test.go
+++ b/backtester/config/config_test.go
@@ -22,7 +22,7 @@ import (
)
const (
- testExchange = "binance"
+ testExchange = "ftx"
dca = "dollarcostaverage"
// change this if you modify a config and want it to save to the example folder
saveConfig = false
@@ -32,57 +32,330 @@ var (
startDate = time.Date(time.Now().Year()-1, 8, 1, 0, 0, 0, 0, time.Local)
endDate = time.Date(time.Now().Year()-1, 12, 1, 0, 0, 0, 0, time.Local)
tradeEndDate = startDate.Add(time.Hour * 72)
- makerFee = decimal.NewFromFloat(0.001)
- takerFee = decimal.NewFromFloat(0.002)
+ makerFee = decimal.NewFromFloat(0.0002)
+ takerFee = decimal.NewFromFloat(0.0007)
minMax = MinMax{
MinimumSize: decimal.NewFromFloat(0.005),
MaximumSize: decimal.NewFromInt(2),
MaximumTotal: decimal.NewFromInt(40000),
}
- initialQuoteFunds1 *decimal.Decimal
- initialQuoteFunds2 *decimal.Decimal
- initialBaseFunds *decimal.Decimal
+ initialFunds1000000 *decimal.Decimal
+ initialFunds100000 *decimal.Decimal
+ initialFunds10 *decimal.Decimal
)
func TestMain(m *testing.M) {
iF1 := decimal.NewFromInt(1000000)
iF2 := decimal.NewFromInt(100000)
iBF := decimal.NewFromInt(10)
- initialQuoteFunds1 = &iF1
- initialQuoteFunds2 = &iF2
- initialBaseFunds = &iBF
+ initialFunds1000000 = &iF1
+ initialFunds100000 = &iF2
+ initialFunds10 = &iBF
os.Exit(m.Run())
}
func TestLoadConfig(t *testing.T) {
+ t.Parallel()
_, err := LoadConfig([]byte(`{}`))
if err != nil {
t.Error(err)
}
}
-func TestReadConfigFromFile(t *testing.T) {
- tempDir := t.TempDir()
- var passFile *os.File
- passFile, err := ioutil.TempFile(tempDir, "*.start")
+func TestValidateDate(t *testing.T) {
+ t.Parallel()
+ c := Config{}
+ err := c.validateDate()
if err != nil {
- t.Fatalf("Problem creating temp file at %v: %s\n", passFile, err)
+ t.Error(err)
}
- _, err = passFile.WriteString("{}")
+ c.DataSettings = DataSettings{
+ DatabaseData: &DatabaseData{},
+ }
+ err = c.validateDate()
+ if !errors.Is(err, errStartEndUnset) {
+ t.Errorf("received: %v, expected: %v", err, errStartEndUnset)
+ }
+ c.DataSettings.DatabaseData.StartDate = time.Now()
+ c.DataSettings.DatabaseData.EndDate = c.DataSettings.DatabaseData.StartDate
+ err = c.validateDate()
+ if !errors.Is(err, errBadDate) {
+ t.Errorf("received: %v, expected: %v", err, errBadDate)
+ }
+ c.DataSettings.DatabaseData.EndDate = c.DataSettings.DatabaseData.StartDate.Add(time.Minute)
+ err = c.validateDate()
if err != nil {
t.Error(err)
}
- err = passFile.Close()
+ c.DataSettings.APIData = &APIData{}
+ err = c.validateDate()
+ if !errors.Is(err, errStartEndUnset) {
+ t.Errorf("received: %v, expected: %v", err, errStartEndUnset)
+ }
+ c.DataSettings.APIData.StartDate = time.Now()
+ c.DataSettings.APIData.EndDate = c.DataSettings.APIData.StartDate
+ err = c.validateDate()
+ if !errors.Is(err, errBadDate) {
+ t.Errorf("received: %v, expected: %v", err, errBadDate)
+ }
+ c.DataSettings.APIData.EndDate = c.DataSettings.APIData.StartDate.Add(time.Minute)
+ err = c.validateDate()
if err != nil {
t.Error(err)
}
- _, err = ReadConfigFromFile(passFile.Name())
+}
+
+func TestValidateCurrencySettings(t *testing.T) {
+ t.Parallel()
+ c := Config{}
+ err := c.validateCurrencySettings()
+ if !errors.Is(err, errNoCurrencySettings) {
+ t.Errorf("received: %v, expected: %v", err, errNoCurrencySettings)
+ }
+ c.CurrencySettings = append(c.CurrencySettings, CurrencySettings{})
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errUnsetCurrency) {
+ t.Errorf("received: %v, expected: %v", err, errUnsetCurrency)
+ }
+ leet := decimal.NewFromInt(1337)
+ c.CurrencySettings[0].SpotDetails = &SpotDetails{InitialQuoteFunds: &leet}
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errUnsetCurrency) {
+ t.Errorf("received: %v, expected: %v", err, errUnsetCurrency)
+ }
+ c.CurrencySettings[0].Base = currency.NewCode("lol")
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, asset.ErrNotSupported) {
+ t.Errorf("received: %v, expected: %v", err, asset.ErrNotSupported)
+ }
+ c.CurrencySettings[0].Asset = asset.Spot
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errUnsetExchange) {
+ t.Errorf("received: %v, expected: %v", err, errUnsetExchange)
+ }
+ c.CurrencySettings[0].ExchangeName = "lol"
+ err = c.validateCurrencySettings()
if err != nil {
t.Error(err)
}
+
+ c.CurrencySettings[0].Asset = asset.PerpetualSwap
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errPerpetualsUnsupported) {
+ t.Errorf("received: %v, expected: %v", err, errPerpetualsUnsupported)
+ }
+
+ c.CurrencySettings[0].Asset = asset.Futures
+ c.CurrencySettings[0].Quote = currency.NewCode("PERP")
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errPerpetualsUnsupported) {
+ t.Errorf("received: %v, expected: %v", err, errPerpetualsUnsupported)
+ }
+
+ c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
+ c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(3)
+ c.CurrencySettings[0].Quote = currency.NewCode("USD")
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errFeatureIncompatible) {
+ t.Errorf("received: %v, expected: %v", err, errFeatureIncompatible)
+ }
+
+ c.CurrencySettings[0].Asset = asset.Spot
+ c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(-1)
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errBadSlippageRates) {
+ t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
+ }
+ c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
+ c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(-1)
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errBadSlippageRates) {
+ t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
+ }
+ c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
+ c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(1)
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errBadSlippageRates) {
+ t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
+ }
+
+ c.CurrencySettings[0].SpotDetails = &SpotDetails{}
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errBadInitialFunds) {
+ t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
+ }
+
+ z := decimal.Zero
+ c.CurrencySettings[0].SpotDetails.InitialQuoteFunds = &z
+ c.CurrencySettings[0].SpotDetails.InitialBaseFunds = &z
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errBadInitialFunds) {
+ t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
+ }
+
+ c.CurrencySettings[0].SpotDetails.InitialQuoteFunds = &leet
+ c.FundingSettings.UseExchangeLevelFunding = true
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errBadInitialFunds) {
+ t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
+ }
+
+ c.CurrencySettings[0].SpotDetails.InitialQuoteFunds = &z
+ c.CurrencySettings[0].SpotDetails.InitialBaseFunds = &leet
+ c.FundingSettings.UseExchangeLevelFunding = true
+ err = c.validateCurrencySettings()
+ if !errors.Is(err, errBadInitialFunds) {
+ t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
+ }
+}
+
+func TestValidateMinMaxes(t *testing.T) {
+ t.Parallel()
+ c := &Config{}
+ err := c.validateMinMaxes()
+ if err != nil {
+ t.Error(err)
+ }
+
+ c.CurrencySettings = []CurrencySettings{
+ {
+ SellSide: MinMax{
+ MinimumSize: decimal.NewFromInt(-1),
+ },
+ },
+ }
+ err = c.validateMinMaxes()
+ if !errors.Is(err, errSizeLessThanZero) {
+ t.Errorf("received %v expected %v", err, errSizeLessThanZero)
+ }
+ c.CurrencySettings = []CurrencySettings{
+ {
+ SellSide: MinMax{
+ MaximumTotal: decimal.NewFromInt(-1),
+ },
+ },
+ }
+ err = c.validateMinMaxes()
+ if !errors.Is(err, errSizeLessThanZero) {
+ t.Errorf("received %v expected %v", err, errSizeLessThanZero)
+ }
+ c.CurrencySettings = []CurrencySettings{
+ {
+ SellSide: MinMax{
+ MaximumSize: decimal.NewFromInt(-1),
+ },
+ },
+ }
+ err = c.validateMinMaxes()
+ if !errors.Is(err, errSizeLessThanZero) {
+ t.Errorf("received %v expected %v", err, errSizeLessThanZero)
+ }
+
+ c.CurrencySettings = []CurrencySettings{
+ {
+ BuySide: MinMax{
+ MinimumSize: decimal.NewFromInt(2),
+ MaximumTotal: decimal.NewFromInt(10),
+ MaximumSize: decimal.NewFromInt(1),
+ },
+ },
+ }
+ err = c.validateMinMaxes()
+ if !errors.Is(err, errMaxSizeMinSizeMismatch) {
+ t.Errorf("received %v expected %v", err, errMaxSizeMinSizeMismatch)
+ }
+
+ c.CurrencySettings = []CurrencySettings{
+ {
+ BuySide: MinMax{
+ MinimumSize: decimal.NewFromInt(2),
+ MaximumSize: decimal.NewFromInt(2),
+ },
+ },
+ }
+ err = c.validateMinMaxes()
+ if !errors.Is(err, errMinMaxEqual) {
+ t.Errorf("received %v expected %v", err, errMinMaxEqual)
+ }
+
+ c.CurrencySettings = []CurrencySettings{
+ {
+ BuySide: MinMax{
+ MinimumSize: decimal.NewFromInt(1),
+ MaximumTotal: decimal.NewFromInt(10),
+ MaximumSize: decimal.NewFromInt(2),
+ },
+ },
+ }
+ c.PortfolioSettings = PortfolioSettings{
+ BuySide: MinMax{
+ MinimumSize: decimal.NewFromInt(-1),
+ },
+ }
+ err = c.validateMinMaxes()
+ if !errors.Is(err, errSizeLessThanZero) {
+ t.Errorf("received %v expected %v", err, errSizeLessThanZero)
+ }
+ c.PortfolioSettings = PortfolioSettings{
+ SellSide: MinMax{
+ MinimumSize: decimal.NewFromInt(-1),
+ },
+ }
+ err = c.validateMinMaxes()
+ if !errors.Is(err, errSizeLessThanZero) {
+ t.Errorf("received %v expected %v", err, errSizeLessThanZero)
+ }
+}
+
+func TestValidateStrategySettings(t *testing.T) {
+ t.Parallel()
+ c := &Config{}
+ err := c.validateStrategySettings()
+ if !errors.Is(err, base.ErrStrategyNotFound) {
+ t.Errorf("received %v expected %v", err, base.ErrStrategyNotFound)
+ }
+ c.StrategySettings = StrategySettings{Name: dca}
+ err = c.validateStrategySettings()
+ if !errors.Is(err, nil) {
+ t.Errorf("received %v expected %v", err, nil)
+ }
+
+ c.StrategySettings.SimultaneousSignalProcessing = true
+ err = c.validateStrategySettings()
+ if !errors.Is(err, nil) {
+ t.Errorf("received %v expected %v", err, nil)
+ }
+ c.FundingSettings = FundingSettings{}
+ c.FundingSettings.UseExchangeLevelFunding = true
+ err = c.validateStrategySettings()
+ if !errors.Is(err, errExchangeLevelFundingDataRequired) {
+ t.Errorf("received %v expected %v", err, errExchangeLevelFundingDataRequired)
+ }
+ c.FundingSettings.ExchangeLevelFunding = []ExchangeLevelFunding{
+ {
+ InitialFunds: decimal.NewFromInt(-1),
+ },
+ }
+ err = c.validateStrategySettings()
+ if !errors.Is(err, errBadInitialFunds) {
+ t.Errorf("received %v expected %v", err, errBadInitialFunds)
+ }
+
+ c.StrategySettings.SimultaneousSignalProcessing = false
+ err = c.validateStrategySettings()
+ if !errors.Is(err, errSimultaneousProcessingRequired) {
+ t.Errorf("received %v expected %v", err, errSimultaneousProcessingRequired)
+ }
+
+ c.FundingSettings.UseExchangeLevelFunding = false
+ err = c.validateStrategySettings()
+ if !errors.Is(err, errExchangeLevelFundingRequired) {
+ t.Errorf("received %v expected %v", err, errExchangeLevelFundingRequired)
+ }
}
func TestPrintSettings(t *testing.T) {
+ t.Parallel()
cfg := Config{
Nickname: "super fun run",
Goal: "To demonstrate rendering of settings",
@@ -96,22 +369,23 @@ func TestPrintSettings(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds1,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds1000000,
+ InitialBaseFunds: initialFunds1000000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
+ FuturesDetails: &FuturesDetails{},
},
},
DataSettings: DataSettings{
- Interval: kline.OneMin.Duration(),
+ Interval: kline.OneMin,
DataType: common.CandleStr,
APIData: &APIData{
StartDate: startDate,
@@ -148,9 +422,64 @@ func TestPrintSettings(t *testing.T) {
},
}
cfg.PrintSetting()
+ cfg.FundingSettings = FundingSettings{
+ UseExchangeLevelFunding: true,
+ ExchangeLevelFunding: []ExchangeLevelFunding{{}},
+ }
+ cfg.PrintSetting()
+}
+
+func TestValidate(t *testing.T) {
+ t.Parallel()
+ c := &Config{
+ StrategySettings: StrategySettings{Name: dca},
+ CurrencySettings: []CurrencySettings{
+ {
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialBaseFunds: initialFunds10,
+ InitialQuoteFunds: initialFunds100000,
+ },
+ BuySide: MinMax{
+ MinimumSize: decimal.NewFromInt(1),
+ MaximumSize: decimal.NewFromInt(10),
+ MaximumTotal: decimal.NewFromInt(10),
+ },
+ },
+ },
+ }
+ if err := c.Validate(); !errors.Is(err, nil) {
+ t.Errorf("received %v expected %v", err, nil)
+ }
+}
+
+func TestReadConfigFromFile(t *testing.T) {
+ tempDir := t.TempDir()
+ passFile, err := ioutil.TempFile(tempDir, "*.start")
+ if err != nil {
+ t.Fatalf("Problem creating temp file at %v: %s\n", passFile, err)
+ }
+ _, err = passFile.WriteString("{}")
+ if err != nil {
+ t.Error(err)
+ }
+ err = passFile.Close()
+ if err != nil {
+ t.Error(err)
+ }
+ _, err = ReadConfigFromFile(passFile.Name())
+ if err != nil {
+ t.Error(err)
+ }
}
func TestGenerateConfigForDCAAPICandles(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyDCAAPICandles",
Goal: "To demonstrate DCA strategy using API candles",
@@ -159,22 +488,21 @@ func TestGenerateConfigForDCAAPICandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
APIData: &APIData{
StartDate: startDate,
@@ -210,19 +538,24 @@ func TestGenerateConfigForDCAAPICandles(t *testing.T) {
}
func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyDCAAPICandlesExchangeLevelFunding",
Goal: "To demonstrate DCA strategy using API candles using a shared pool of funds",
StrategySettings: StrategySettings{
Name: dca,
SimultaneousSignalProcessing: true,
- UseExchangeLevelFunding: true,
DisableUSDTracking: true,
+ },
+ FundingSettings: FundingSettings{
+ UseExchangeLevelFunding: true,
ExchangeLevelFunding: []ExchangeLevelFunding{
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Currency: currency.USDT.String(),
+ Asset: asset.Spot,
+ Currency: currency.USDT,
InitialFunds: decimal.NewFromInt(100000),
},
},
@@ -230,29 +563,27 @@ func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) {
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.ETH.String(),
- Quote: currency.USDT.String(),
+ Asset: asset.Spot,
+ Base: currency.ETH,
+ Quote: currency.USDT,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
APIData: &APIData{
StartDate: startDate,
@@ -288,6 +619,9 @@ func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) {
}
func TestGenerateConfigForDCAAPITrades(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyDCAAPITrades",
Goal: "To demonstrate running the DCA strategy using API trade data",
@@ -296,23 +630,22 @@ func TestGenerateConfigForDCAAPITrades(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: "ftx",
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: "ftx",
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
SkipCandleVolumeFitting: true,
},
},
DataSettings: DataSettings{
- Interval: kline.OneHour.Duration(),
+ Interval: kline.OneHour,
DataType: common.TradeStr,
APIData: &APIData{
StartDate: startDate,
@@ -356,6 +689,9 @@ func TestGenerateConfigForDCAAPITrades(t *testing.T) {
}
func TestGenerateConfigForDCAAPICandlesMultipleCurrencies(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyDCAAPICandlesMultipleCurrencies",
Goal: "To demonstrate running the DCA strategy using the API against multiple currencies candle data",
@@ -364,36 +700,34 @@ func TestGenerateConfigForDCAAPICandlesMultipleCurrencies(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.ETH.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.ETH,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
APIData: &APIData{
StartDate: startDate,
@@ -429,6 +763,9 @@ func TestGenerateConfigForDCAAPICandlesMultipleCurrencies(t *testing.T) {
}
func TestGenerateConfigForDCAAPICandlesSimultaneousProcessing(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyDCAAPICandlesSimultaneousProcessing",
Goal: "To demonstrate how simultaneous processing can work",
@@ -438,36 +775,34 @@ func TestGenerateConfigForDCAAPICandlesSimultaneousProcessing(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds1,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds1000000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.ETH.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.ETH,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
APIData: &APIData{
StartDate: startDate,
@@ -503,6 +838,9 @@ func TestGenerateConfigForDCAAPICandlesSimultaneousProcessing(t *testing.T) {
}
func TestGenerateConfigForDCALiveCandles(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyDCALiveCandles",
Goal: "To demonstrate live trading proof of concept against candle data",
@@ -512,22 +850,21 @@ func TestGenerateConfigForDCALiveCandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneMin.Duration(),
+ Interval: kline.OneMin,
DataType: common.CandleStr,
LiveData: &LiveData{
APIKeyOverride: "",
@@ -566,6 +903,9 @@ func TestGenerateConfigForDCALiveCandles(t *testing.T) {
}
func TestGenerateConfigForRSIAPICustomSettings(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "TestGenerateRSICandleAPICustomSettingsStrat",
Goal: "To demonstrate the RSI strategy using API candle data and custom settings",
@@ -579,37 +919,35 @@ func TestGenerateConfigForRSIAPICustomSettings(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.ETH.String(),
- Quote: currency.USDT.String(),
- InitialBaseFunds: initialBaseFunds,
- InitialQuoteFunds: initialQuoteFunds1,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.ETH,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialBaseFunds: initialFunds10,
+ InitialQuoteFunds: initialFunds1000000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
APIData: &APIData{
StartDate: startDate,
@@ -645,6 +983,9 @@ func TestGenerateConfigForRSIAPICustomSettings(t *testing.T) {
}
func TestGenerateConfigForDCACSVCandles(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
fp := filepath.Join("..", "testdata", "binance_BTCUSDT_24h_2019_01_01_2020_01_01.csv")
cfg := Config{
Nickname: "ExampleStrategyDCACSVCandles",
@@ -655,22 +996,21 @@ func TestGenerateConfigForDCACSVCandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
CSVData: &CSVData{
FullPath: fp,
@@ -704,6 +1044,9 @@ func TestGenerateConfigForDCACSVCandles(t *testing.T) {
}
func TestGenerateConfigForDCACSVTrades(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
fp := filepath.Join("..", "testdata", "binance_BTCUSDT_24h-trades_2020_11_16.csv")
cfg := Config{
Nickname: "ExampleStrategyDCACSVTrades",
@@ -714,20 +1057,19 @@ func TestGenerateConfigForDCACSVTrades(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneMin.Duration(),
+ Interval: kline.OneMin,
DataType: common.TradeStr,
CSVData: &CSVData{
FullPath: fp,
@@ -759,6 +1101,9 @@ func TestGenerateConfigForDCACSVTrades(t *testing.T) {
}
func TestGenerateConfigForDCADatabaseCandles(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyDCADatabaseCandles",
Goal: "To demonstrate the DCA strategy using database candle data",
@@ -767,22 +1112,21 @@ func TestGenerateConfigForDCADatabaseCandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: minMax,
- SellSide: minMax,
- Leverage: Leverage{
- CanUseLeverage: false,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
+ SpotDetails: &SpotDetails{
+ InitialQuoteFunds: initialFunds100000,
},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ BuySide: minMax,
+ SellSide: minMax,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
DatabaseData: &DatabaseData{
StartDate: startDate,
@@ -827,103 +1171,103 @@ func TestGenerateConfigForDCADatabaseCandles(t *testing.T) {
}
func TestGenerateConfigForTop2Bottom2(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
+ }
cfg := Config{
Nickname: "ExampleStrategyTop2Bottom2",
Goal: "To demonstrate a complex strategy using exchange level funding and simultaneous processing of data signals",
StrategySettings: StrategySettings{
Name: top2bottom2.Name,
- UseExchangeLevelFunding: true,
SimultaneousSignalProcessing: true,
+
+ CustomSettings: map[string]interface{}{
+ "mfi-low": 32,
+ "mfi-high": 68,
+ "mfi-period": 14,
+ },
+ },
+ FundingSettings: FundingSettings{
+ UseExchangeLevelFunding: true,
ExchangeLevelFunding: []ExchangeLevelFunding{
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Currency: currency.BTC.String(),
+ Asset: asset.Spot,
+ Currency: currency.BTC,
InitialFunds: decimal.NewFromFloat(3),
},
{
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Currency: currency.USDT.String(),
- InitialFunds: decimal.NewFromInt(10000),
- },
- },
- CustomSettings: map[string]interface{}{
- "mfi-low": 32,
- "mfi-high": 68,
- "mfi-period": 14,
+ ExchangeName: testExchange,
+ Asset: asset.Spot,
+ Currency: currency.USDT,
+ InitialFunds: decimal.NewFromInt(10000),
+ },
},
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USDT,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.DOGE.String(),
- Quote: currency.USDT.String(),
+ Asset: asset.Spot,
+ Base: currency.DOGE,
+ Quote: currency.USDT,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.ETH.String(),
- Quote: currency.BTC.String(),
+ Asset: asset.Spot,
+ Base: currency.ETH,
+ Quote: currency.BTC,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.LTC.String(),
- Quote: currency.BTC.String(),
+ Asset: asset.Spot,
+ Base: currency.LTC,
+ Quote: currency.BTC,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.XRP.String(),
- Quote: currency.USDT.String(),
+ Asset: asset.Spot,
+ Base: currency.XRP,
+ Quote: currency.USDT,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BNB.String(),
- Quote: currency.BTC.String(),
+ Asset: asset.Spot,
+ Base: currency.BNB,
+ Quote: currency.BTC,
BuySide: minMax,
SellSide: minMax,
- Leverage: Leverage{},
- MakerFee: makerFee,
- TakerFee: takerFee,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
DataSettings: DataSettings{
- Interval: kline.OneDay.Duration(),
+ Interval: kline.OneDay,
DataType: common.CandleStr,
APIData: &APIData{
StartDate: startDate,
@@ -955,256 +1299,76 @@ func TestGenerateConfigForTop2Bottom2(t *testing.T) {
}
}
-func TestValidateDate(t *testing.T) {
- c := Config{}
- err := c.validateDate()
- if err != nil {
- t.Error(err)
- }
- c.DataSettings = DataSettings{
- DatabaseData: &DatabaseData{},
- }
- err = c.validateDate()
- if !errors.Is(err, errStartEndUnset) {
- t.Errorf("received: %v, expected: %v", err, errStartEndUnset)
- }
- c.DataSettings.DatabaseData.StartDate = time.Now()
- c.DataSettings.DatabaseData.EndDate = c.DataSettings.DatabaseData.StartDate
- err = c.validateDate()
- if !errors.Is(err, errBadDate) {
- t.Errorf("received: %v, expected: %v", err, errBadDate)
- }
- c.DataSettings.DatabaseData.EndDate = c.DataSettings.DatabaseData.StartDate.Add(time.Minute)
- err = c.validateDate()
- if err != nil {
- t.Error(err)
- }
- c.DataSettings.APIData = &APIData{}
- err = c.validateDate()
- if !errors.Is(err, errStartEndUnset) {
- t.Errorf("received: %v, expected: %v", err, errStartEndUnset)
- }
- c.DataSettings.APIData.StartDate = time.Now()
- c.DataSettings.APIData.EndDate = c.DataSettings.APIData.StartDate
- err = c.validateDate()
- if !errors.Is(err, errBadDate) {
- t.Errorf("received: %v, expected: %v", err, errBadDate)
- }
- c.DataSettings.APIData.EndDate = c.DataSettings.APIData.StartDate.Add(time.Minute)
- err = c.validateDate()
- if err != nil {
- t.Error(err)
- }
-}
-
-func TestValidateCurrencySettings(t *testing.T) {
- c := Config{}
- err := c.validateCurrencySettings()
- if !errors.Is(err, errNoCurrencySettings) {
- t.Errorf("received: %v, expected: %v", err, errNoCurrencySettings)
- }
- c.CurrencySettings = append(c.CurrencySettings, CurrencySettings{})
- err = c.validateCurrencySettings()
- if !errors.Is(err, errBadInitialFunds) {
- t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
- }
- leet := decimal.NewFromInt(1337)
- c.CurrencySettings[0].InitialQuoteFunds = &leet
- err = c.validateCurrencySettings()
- if !errors.Is(err, errUnsetCurrency) {
- t.Errorf("received: %v, expected: %v", err, errUnsetCurrency)
- }
- c.CurrencySettings[0].Base = "lol"
- err = c.validateCurrencySettings()
- if !errors.Is(err, errUnsetAsset) {
- t.Errorf("received: %v, expected: %v", err, errUnsetAsset)
- }
- c.CurrencySettings[0].Asset = "lol"
- err = c.validateCurrencySettings()
- if !errors.Is(err, errUnsetExchange) {
- t.Errorf("received: %v, expected: %v", err, errUnsetExchange)
- }
- c.CurrencySettings[0].ExchangeName = "lol"
- err = c.validateCurrencySettings()
- if err != nil {
- t.Error(err)
- }
- c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(-1)
- err = c.validateCurrencySettings()
- if !errors.Is(err, errBadSlippageRates) {
- t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
- }
- c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
- c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(-1)
- err = c.validateCurrencySettings()
- if !errors.Is(err, errBadSlippageRates) {
- t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
- }
- c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
- c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(1)
- err = c.validateCurrencySettings()
- if !errors.Is(err, errBadSlippageRates) {
- t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
- }
-}
-
-func TestValidateMinMaxes(t *testing.T) {
- t.Parallel()
- c := &Config{}
- err := c.validateMinMaxes()
- if err != nil {
- t.Error(err)
+func TestGenerateFTXCashAndCarryStrategy(t *testing.T) {
+ if !saveConfig {
+ t.Skip()
}
-
- c.CurrencySettings = []CurrencySettings{
- {
- SellSide: MinMax{
- MinimumSize: decimal.NewFromInt(-1),
- },
+ cfg := Config{
+ Nickname: "ExampleCashAndCarry",
+ Goal: "To demonstrate a cash and carry strategy",
+ StrategySettings: StrategySettings{
+ Name: "ftx-cash-carry",
+ SimultaneousSignalProcessing: true,
},
- }
- err = c.validateMinMaxes()
- if !errors.Is(err, errSizeLessThanZero) {
- t.Errorf("received %v expected %v", err, errSizeLessThanZero)
- }
- c.CurrencySettings = []CurrencySettings{
- {
- SellSide: MinMax{
- MaximumTotal: decimal.NewFromInt(-1),
+ FundingSettings: FundingSettings{
+ UseExchangeLevelFunding: true,
+ ExchangeLevelFunding: []ExchangeLevelFunding{
+ {
+ ExchangeName: "ftx",
+ Asset: asset.Spot,
+ Currency: currency.USD,
+ InitialFunds: *initialFunds100000,
+ },
},
},
- }
- err = c.validateMinMaxes()
- if !errors.Is(err, errSizeLessThanZero) {
- t.Errorf("received %v expected %v", err, errSizeLessThanZero)
- }
- c.CurrencySettings = []CurrencySettings{
- {
- SellSide: MinMax{
- MaximumSize: decimal.NewFromInt(-1),
+ CurrencySettings: []CurrencySettings{
+ {
+ ExchangeName: "ftx",
+ Asset: asset.Futures,
+ Base: currency.BTC,
+ Quote: currency.NewCode("20210924"),
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
- },
- }
- err = c.validateMinMaxes()
- if !errors.Is(err, errSizeLessThanZero) {
- t.Errorf("received %v expected %v", err, errSizeLessThanZero)
- }
-
- c.CurrencySettings = []CurrencySettings{
- {
- BuySide: MinMax{
- MinimumSize: decimal.NewFromInt(2),
- MaximumTotal: decimal.NewFromInt(10),
- MaximumSize: decimal.NewFromInt(1),
+ {
+ ExchangeName: "ftx",
+ Asset: asset.Spot,
+ Base: currency.BTC,
+ Quote: currency.USD,
+ MakerFee: &makerFee,
+ TakerFee: &takerFee,
},
},
- }
- err = c.validateMinMaxes()
- if !errors.Is(err, errMaxSizeMinSizeMismatch) {
- t.Errorf("received %v expected %v", err, errMaxSizeMinSizeMismatch)
- }
-
- c.CurrencySettings = []CurrencySettings{
- {
- BuySide: MinMax{
- MinimumSize: decimal.NewFromInt(2),
- MaximumSize: decimal.NewFromInt(2),
+ DataSettings: DataSettings{
+ Interval: kline.OneDay,
+ DataType: common.CandleStr,
+ APIData: &APIData{
+ StartDate: time.Date(2021, 1, 14, 0, 0, 0, 0, time.UTC),
+ EndDate: time.Date(2021, 9, 24, 0, 0, 0, 0, time.UTC),
+ InclusiveEndDate: false,
},
},
- }
- err = c.validateMinMaxes()
- if !errors.Is(err, errMinMaxEqual) {
- t.Errorf("received %v expected %v", err, errMinMaxEqual)
- }
-
- c.CurrencySettings = []CurrencySettings{
- {
- BuySide: MinMax{
- MinimumSize: decimal.NewFromInt(1),
- MaximumTotal: decimal.NewFromInt(10),
- MaximumSize: decimal.NewFromInt(2),
+ PortfolioSettings: PortfolioSettings{
+ Leverage: Leverage{
+ CanUseLeverage: true,
},
},
- }
- c.PortfolioSettings = PortfolioSettings{
- BuySide: MinMax{
- MinimumSize: decimal.NewFromInt(-1),
- },
- }
- err = c.validateMinMaxes()
- if !errors.Is(err, errSizeLessThanZero) {
- t.Errorf("received %v expected %v", err, errSizeLessThanZero)
- }
- c.PortfolioSettings = PortfolioSettings{
- SellSide: MinMax{
- MinimumSize: decimal.NewFromInt(-1),
- },
- }
- err = c.validateMinMaxes()
- if !errors.Is(err, errSizeLessThanZero) {
- t.Errorf("received %v expected %v", err, errSizeLessThanZero)
- }
-}
-
-func TestValidateStrategySettings(t *testing.T) {
- t.Parallel()
- c := &Config{}
- err := c.validateStrategySettings()
- if !errors.Is(err, base.ErrStrategyNotFound) {
- t.Errorf("received %v expected %v", err, base.ErrStrategyNotFound)
- }
- c.StrategySettings = StrategySettings{Name: dca}
- err = c.validateStrategySettings()
- if !errors.Is(err, nil) {
- t.Errorf("received %v expected %v", err, nil)
- }
- c.StrategySettings.UseExchangeLevelFunding = true
- err = c.validateStrategySettings()
- if !errors.Is(err, errSimultaneousProcessingRequired) {
- t.Errorf("received %v expected %v", err, errSimultaneousProcessingRequired)
- }
- c.StrategySettings.SimultaneousSignalProcessing = true
- err = c.validateStrategySettings()
- if !errors.Is(err, errExchangeLevelFundingDataRequired) {
- t.Errorf("received %v expected %v", err, errExchangeLevelFundingDataRequired)
- }
- c.StrategySettings.ExchangeLevelFunding = []ExchangeLevelFunding{
- {
- InitialFunds: decimal.NewFromInt(-1),
- },
- }
- err = c.validateStrategySettings()
- if !errors.Is(err, errBadInitialFunds) {
- t.Errorf("received %v expected %v", err, errBadInitialFunds)
- }
- c.StrategySettings.UseExchangeLevelFunding = false
- err = c.validateStrategySettings()
- if !errors.Is(err, errExchangeLevelFundingRequired) {
- t.Errorf("received %v expected %v", err, errExchangeLevelFundingRequired)
- }
-}
-
-func TestValidate(t *testing.T) {
- t.Parallel()
- c := &Config{
- StrategySettings: StrategySettings{Name: dca},
- CurrencySettings: []CurrencySettings{
- {
- ExchangeName: testExchange,
- Asset: asset.Spot.String(),
- Base: currency.BTC.String(),
- Quote: currency.USDT.String(),
- InitialBaseFunds: initialBaseFunds,
- InitialQuoteFunds: initialQuoteFunds2,
- BuySide: MinMax{
- MinimumSize: decimal.NewFromInt(1),
- MaximumSize: decimal.NewFromInt(10),
- MaximumTotal: decimal.NewFromInt(10),
- },
- },
+ StatisticSettings: StatisticSettings{
+ RiskFreeRate: decimal.NewFromFloat(0.03),
},
}
- if err := c.Validate(); !errors.Is(err, nil) {
- t.Errorf("received %v expected %v", err, nil)
+ if saveConfig {
+ result, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ p, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.WriteFile(filepath.Join(p, "examples", "ftx-cash-carry.strat"), result, file.DefaultPermissionOctal)
+ if err != nil {
+ t.Error(err)
+ }
}
}
diff --git a/backtester/config/config_types.go b/backtester/config/config_types.go
index a68401481e0..60d0403a072 100644
--- a/backtester/config/config_types.go
+++ b/backtester/config/config_types.go
@@ -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
diff --git a/backtester/config/configbuilder/main.go b/backtester/config/configbuilder/main.go
index 226e751c588..456ef391a89 100644
--- a/backtester/config/configbuilder/main.go
+++ b/backtester/config/configbuilder/main.go
@@ -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
diff --git a/backtester/config/examples/README.md b/backtester/config/examples/README.md
index 0e973e476a2..9d868e9fc69 100644
--- a/backtester/config/examples/README.md
+++ b/backtester/config/examples/README.md
@@ -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
diff --git a/backtester/config/examples/dca-api-candles-exchange-level-funding.strat b/backtester/config/examples/dca-api-candles-exchange-level-funding.strat
index 7cb4692293d..dca2e09819f 100644
--- a/backtester/config/examples/dca-api-candles-exchange-level-funding.strat
+++ b/backtester/config/examples/dca-api-candles-exchange-level-funding.strat
@@ -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",
diff --git a/backtester/config/examples/dca-api-candles-multiple-currencies.strat b/backtester/config/examples/dca-api-candles-multiple-currencies.strat
index 26a52309d28..08de70e93aa 100644
--- a/backtester/config/examples/dca-api-candles-multiple-currencies.strat
+++ b/backtester/config/examples/dca-api-candles-multiple-currencies.strat
@@ -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",
diff --git a/backtester/config/examples/dca-api-candles-simultaneous-processing.strat b/backtester/config/examples/dca-api-candles-simultaneous-processing.strat
index 8e92da7bf1f..662f4a5ec46 100644
--- a/backtester/config/examples/dca-api-candles-simultaneous-processing.strat
+++ b/backtester/config/examples/dca-api-candles-simultaneous-processing.strat
@@ -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",
diff --git a/backtester/config/examples/dca-api-candles.strat b/backtester/config/examples/dca-api-candles.strat
index bd55e24ee2e..6e93d23c865 100644
--- a/backtester/config/examples/dca-api-candles.strat
+++ b/backtester/config/examples/dca-api-candles.strat
@@ -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",
diff --git a/backtester/config/examples/dca-api-trades.strat b/backtester/config/examples/dca-api-trades.strat
index 285595ba7ea..a253a2a7bca 100644
--- a/backtester/config/examples/dca-api-trades.strat
+++ b/backtester/config/examples/dca-api-trades.strat
@@ -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",
diff --git a/backtester/config/examples/dca-candles-live.strat b/backtester/config/examples/dca-candles-live.strat
index 5ebbd139b55..7fe0bedefb0 100644
--- a/backtester/config/examples/dca-candles-live.strat
+++ b/backtester/config/examples/dca-candles-live.strat
@@ -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",
diff --git a/backtester/config/examples/dca-csv-candles.strat b/backtester/config/examples/dca-csv-candles.strat
index a5556b0c5ba..b95c3639d56 100644
--- a/backtester/config/examples/dca-csv-candles.strat
+++ b/backtester/config/examples/dca-csv-candles.strat
@@ -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",
diff --git a/backtester/config/examples/dca-csv-trades.strat b/backtester/config/examples/dca-csv-trades.strat
index c8b16f661fd..8bdd43dd3d7 100644
--- a/backtester/config/examples/dca-csv-trades.strat
+++ b/backtester/config/examples/dca-csv-trades.strat
@@ -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",
diff --git a/backtester/config/examples/dca-database-candles.strat b/backtester/config/examples/dca-database-candles.strat
index 2b5182c6447..08e2b18e282 100644
--- a/backtester/config/examples/dca-database-candles.strat
+++ b/backtester/config/examples/dca-database-candles.strat
@@ -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",
diff --git a/backtester/config/examples/ftx-cash-carry.strat b/backtester/config/examples/ftx-cash-carry.strat
new file mode 100644
index 00000000000..d820027a8a6
--- /dev/null
+++ b/backtester/config/examples/ftx-cash-carry.strat
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/backtester/config/examples/rsi-api-candles.strat b/backtester/config/examples/rsi-api-candles.strat
index d8ff88b1453..2a97532e601 100644
--- a/backtester/config/examples/rsi-api-candles.strat
+++ b/backtester/config/examples/rsi-api-candles.strat
@@ -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",
diff --git a/backtester/config/examples/t2b2-api-candles-exchange-funding.strat b/backtester/config/examples/t2b2-api-candles-exchange-funding.strat
index 1f4b3b9256c..4ba22996368 100644
--- a/backtester/config/examples/t2b2-api-candles-exchange-funding.strat
+++ b/backtester/config/examples/t2b2-api-candles-exchange-funding.strat
@@ -4,41 +4,38 @@
"strategy-settings": {
"name": "top2bottom2",
"use-simultaneous-signal-processing": true,
+ "disable-usd-tracking": false,
+ "custom-settings": {
+ "mfi-high": 68,
+ "mfi-low": 32,
+ "mfi-period": 14
+ }
+ },
+ "funding-settings": {
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
- "exchange-name": "binance",
+ "exchange-name": "ftx",
"asset": "spot",
"currency": "BTC",
"initial-funds": "3",
"transfer-fee": "0"
},
{
- "exchange-name": "binance",
+ "exchange-name": "ftx",
"asset": "spot",
"currency": "USDT",
"initial-funds": "10000",
"transfer-fee": "0"
}
- ],
- "disable-usd-tracking": false,
- "custom-settings": {
- "mfi-high": 68,
- "mfi-low": 32,
- "mfi-period": 14
- }
+ ]
},
"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",
diff --git a/backtester/data/data.go b/backtester/data/data.go
index c0e5fa14b8b..43ef41ffd10 100644
--- a/backtester/data/data.go
+++ b/backtester/data/data.go
@@ -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 {
diff --git a/backtester/data/data_test.go b/backtester/data/data_test.go
index c9f028bce5a..0fc5fe45e61 100644
--- a/backtester/data/data_test.go
+++ b/backtester/data/data_test.go
@@ -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 TestBaseDataFunctions(t *testing.T) {
+func TestLatest(t *testing.T) {
t.Parallel()
var d Base
- if latest := d.Latest(); latest != nil {
- t.Error("expected nil")
+ 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
+
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)
+ if len(d.stream) != 0 {
+ t.Error("expected 0")
+ }
+ 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))
+ }
+ d.Next()
+ d.Next()
+ if !d.IsLastEvent() {
+ t.Error("expected last event")
+ }
o = d.Offset()
- if o != 0 {
- t.Error("expected 0")
+ if o != 4 {
+ t.Error("expected 4")
}
- if list := d.List(); list != nil {
- t.Error("expected nil")
+ if list := d.List(); len(list) != 0 {
+ t.Error("expected 0")
}
- if history := d.History(); history != nil {
- t.Error("expected nil")
+ 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
+}
diff --git a/backtester/data/data_types.go b/backtester/data/data_types.go
index 9ff5a139b7d..2edc681303e 100644
--- a/backtester/data/data_types.go
+++ b/backtester/data/data_types.go
@@ -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
diff --git a/backtester/data/kline/csv/csv.go b/backtester/data/kline/csv/csv.go
index 39c2d6a75e0..38043a54070 100644
--- a/backtester/data/kline/csv/csv.go
+++ b/backtester/data/kline/csv/csv.go
@@ -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)
}
}()
diff --git a/backtester/data/kline/database/database.go b/backtester/data/kline/database/database.go
index f9c4571c082..e8ae596120f 100644
--- a/backtester/data/kline/database/database.go
+++ b/backtester/data/kline/database/database.go
@@ -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:
diff --git a/backtester/data/kline/kline.go b/backtester/data/kline/kline.go
index e4e3234a1e1..f4e0b8c59a6 100644
--- a/backtester/data/kline/kline.go
+++ b/backtester/data/kline/kline.go
@@ -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
diff --git a/backtester/data/kline/kline_test.go b/backtester/data/kline/kline_test.go
index 4aae0887988..e6fe0b32213 100644
--- a/backtester/data/kline/kline_test.go
+++ b/backtester/data/kline/kline_test.go
@@ -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,
diff --git a/backtester/data/kline/kline_types.go b/backtester/data/kline/kline_types.go
index d44d395f51c..0c466ace3a0 100644
--- a/backtester/data/kline/kline_types.go
+++ b/backtester/data/kline/kline_types.go
@@ -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
}
diff --git a/backtester/backtest/README.md b/backtester/engine/README.md
similarity index 90%
rename from backtester/backtest/README.md
rename to backtester/engine/README.md
index 754c403603a..682917a9b67 100644
--- a/backtester/backtest/README.md
+++ b/backtester/engine/README.md
@@ -1,16 +1,16 @@
-# GoCryptoTrader Backtester: Backtest package
+# GoCryptoTrader Backtester: Engine package
[![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
diff --git a/backtester/engine/backtest.go b/backtester/engine/backtest.go
new file mode 100644
index 00000000000..92b910d1560
--- /dev/null
+++ b/backtester/engine/backtest.go
@@ -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)
+}
diff --git a/backtester/engine/backtest_test.go b/backtester/engine/backtest_test.go
new file mode 100644
index 00000000000..5c35faffdb3
--- /dev/null
+++ b/backtester/engine/backtest_test.go
@@ -0,0 +1,1377 @@
+package engine
+
+import (
+ "errors"
+ "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/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/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 = "ftx"
+
+var leet = decimal.NewFromInt(1337)
+
+type portfolioOverride struct {
+ Err error
+ portfolio.Portfolio
+}
+
+func (p portfolioOverride) CreateLiquidationOrdersForExchange(ev common.DataEventHandler, _ 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 TestNewFromConfig(t *testing.T) {
+ t.Parallel()
+ _, err := NewFromConfig(nil, "", "", false)
+ if !errors.Is(err, errNilConfig) {
+ t.Errorf("received %v, expected %v", err, errNilConfig)
+ }
+
+ cfg := &config.Config{}
+ _, err = NewFromConfig(cfg, "", "", false)
+ if !errors.Is(err, base.ErrStrategyNotFound) {
+ t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
+ }
+
+ cfg.CurrencySettings = []config.CurrencySettings{
+ {
+ ExchangeName: "test",
+ Base: currency.NewCode("test"),
+ Quote: currency.NewCode("test"),
+ },
+ {
+ ExchangeName: testExchange,
+ Base: currency.BTC,
+ Quote: currency.NewCode("0624"),
+ Asset: asset.Futures,
+ },
+ }
+ _, err = NewFromConfig(cfg, "", "", false)
+ if !errors.Is(err, engine.ErrExchangeNotFound) {
+ t.Errorf("received: %v, expected: %v", err, engine.ErrExchangeNotFound)
+ }
+ cfg.CurrencySettings[0].ExchangeName = testExchange
+ _, err = NewFromConfig(cfg, "", "", false)
+ if !errors.Is(err, asset.ErrNotSupported) {
+ t.Errorf("received: %v, expected: %v", err, asset.ErrNotSupported)
+ }
+ cfg.CurrencySettings[0].Asset = asset.Spot
+ _, err = NewFromConfig(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.USD
+ cfg.DataSettings.APIData = &config.APIData{
+ StartDate: time.Time{},
+ EndDate: time.Time{},
+ }
+
+ _, err = NewFromConfig(cfg, "", "", false)
+ if err != nil && !strings.Contains(err.Error(), "unrecognised dataType") {
+ t.Error(err)
+ }
+ cfg.DataSettings.DataType = common.CandleStr
+ _, err = NewFromConfig(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 = NewFromConfig(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 = NewFromConfig(cfg, "", "", false)
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+
+ 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.Futures,
+ Currency: currency.BTC,
+ InitialFunds: leet,
+ TransferFee: leet,
+ },
+ }
+ _, err = NewFromConfig(cfg, "", "", false)
+ 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,
+ Base: cp.Base,
+ Quote: cp.Quote,
+ SpotDetails: &config.SpotDetails{
+ InitialQuoteFunds: &leet,
+ },
+ BuySide: config.MinMax{},
+ SellSide: config.MinMax{},
+ MakerFee: &decimal.Zero,
+ TakerFee: &decimal.Zero,
+ },
+ },
+ DataSettings: config.DataSettings{
+ DataType: common.CandleStr,
+ Interval: gctkline.OneMin,
+ 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] = ¤cy.PairStore{
+ Available: currency.Pairs{cp},
+ Enabled: currency.Pairs{cp},
+ AssetEnabled: convert.BoolPtr(true),
+ ConfigFormat: ¤cy.PairFormat{Uppercase: true},
+ RequestFormat: ¤cy.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,
+ Base: cp.Base,
+ Quote: cp.Quote,
+ SpotDetails: &config.SpotDetails{
+ InitialQuoteFunds: &leet,
+ },
+ BuySide: config.MinMax{},
+ SellSide: config.MinMax{},
+ 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("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] = ¤cy.PairStore{
+ Available: currency.Pairs{cp},
+ Enabled: currency.Pairs{cp},
+ AssetEnabled: convert.BoolPtr(true),
+ ConfigFormat: ¤cy.PairFormat{Uppercase: true},
+ RequestFormat: ¤cy.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,
+ Base: cp.Base,
+ Quote: cp.Quote,
+ SpotDetails: &config.SpotDetails{
+ InitialQuoteFunds: &leet,
+ },
+ BuySide: config.MinMax{},
+ SellSide: config.MinMax{},
+ 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] = ¤cy.PairStore{
+ Available: currency.Pairs{cp},
+ Enabled: currency.Pairs{cp},
+ AssetEnabled: convert.BoolPtr(true),
+ ConfigFormat: ¤cy.PairFormat{Uppercase: true},
+ RequestFormat: ¤cy.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,
+ Base: cp.Base,
+ Quote: cp.Quote,
+ SpotDetails: &config.SpotDetails{
+ InitialQuoteFunds: &leet,
+ },
+ BuySide: config.MinMax{},
+ SellSide: config.MinMax{},
+ MakerFee: &decimal.Zero,
+ TakerFee: &decimal.Zero,
+ },
+ },
+ DataSettings: config.DataSettings{
+ DataType: common.CandleStr,
+ Interval: gctkline.OneMin,
+ 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] = ¤cy.PairStore{
+ Available: currency.Pairs{cp},
+ Enabled: currency.Pairs{cp},
+ AssetEnabled: convert.BoolPtr(true),
+ ConfigFormat: ¤cy.PairFormat{Uppercase: true},
+ RequestFormat: ¤cy.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
+ 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, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, false)
+ if err != nil {
+ t.Error(err)
+ }
+ 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)
+ }
+ fx := &ftx.FTX{}
+ fx.Name = testExchange
+ err = port.SetupCurrencySettingsMap(&exchange.Settings{Exchange: fx, Asset: a, Pair: cp})
+ if err != nil {
+ t.Error(err)
+ }
+ f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true)
+ if err != nil {
+ t.Error(err)
+ }
+ 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)
+
+ bt.Run()
+}
+
+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: &ftx.FTX{}, Asset: a, Pair: cp})
+ if err != nil {
+ t.Error(err)
+ }
+ f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, true)
+ if err != nil {
+ t.Error(err)
+ }
+ 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)
+
+ bt.Run()
+}
+
+func TestTriggerLiquidationsForExchange(t *testing.T) {
+ t.Parallel()
+ bt := BackTest{}
+ 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.Futures
+ expectedError = common.ErrNilArguments
+ 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.Datas = &data.HandlerPerCurrency{}
+ d := data.Base{}
+ d.SetStream([]common.DataEventHandler{&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,
+ }})
+ d.Next()
+ 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{}
+ bt.Datas.SetDataForCurrency(testExchange, a, cp, da)
+ err = bt.Statistic.SetupEventForTime(ev)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ pnl.Exchange = ev.Exchange
+ pnl.Item = 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()
+ pt := &portfolio.Portfolio{}
+ bt := &BackTest{
+ Statistic: &statistics.Statistic{},
+ Funding: &funding.FundManager{},
+ Portfolio: pt,
+ }
+ 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 = common.ErrNilArguments
+ 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)
+ 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 := &ftx.FTX{}
+ exch.Name = testExchange
+ err = pt.SetupCurrencySettingsMap(&exchange.Settings{
+ Exchange: exch,
+ Pair: cp,
+ Asset: a,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ 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 = pt.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
+ 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{},
+ }
+ 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.SetupEventForTime(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)
+ 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 := &ftx.FTX{}
+ exch.Name = testExchange
+ err = pt.SetupCurrencySettingsMap(&exchange.Settings{
+ Exchange: exch,
+ Pair: cp,
+ Asset: a,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ 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{},
+ Datas: &data.HandlerPerCurrency{},
+ }
+ 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.SetupEventForTime(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)
+ 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 := &ftx.FTX{}
+ exch.Name = testExchange
+ err = pt.SetupCurrencySettingsMap(&exchange.Settings{
+ Exchange: exch,
+ Pair: cp,
+ Asset: a,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ 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.Datas.Setup()
+ 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 err != nil {
+ t.Error(err)
+ }
+
+ bt.Datas.SetDataForCurrency(testExchange, a, cp, &k)
+ 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()
+ 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{},
+ Datas: &data.HandlerPerCurrency{},
+ }
+ cp := currency.NewPair(currency.BTC, currency.USD)
+ a := asset.Futures
+ de := &evkline.Kline{
+ Base: &event.Base{Exchange: testExchange,
+ AssetType: a,
+ CurrencyPair: cp},
+ }
+ err = bt.Statistic.SetupEventForTime(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()
+ cfg, err := exch.GetDefaultConfig()
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = exch.Setup(cfg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ em.Add(exch)
+ f, err := funding.SetupFundingManager(em, false, true)
+ 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)
+ }
+
+ err = f.AddItem(b)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ err = f.AddItem(quote)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ spotBase, err := funding.CreateItem(testExchange, asset.Spot, cp.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ spotQuote, err := funding.CreateItem(testExchange, asset.Spot, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ spotPair, err := funding.CreatePair(spotBase, spotQuote)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ err = f.AddPair(spotPair)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ bt.Funding = f
+ err = pt.SetupCurrencySettingsMap(&exchange.Settings{
+ Exchange: exch,
+ Pair: cp,
+ Asset: a,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ 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.Datas.Setup()
+ 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 err != nil {
+ t.Error(err)
+ }
+
+ bt.Datas.SetDataForCurrency(testExchange, a, cp, &k)
+ err = bt.processFillEvent(ev, pair)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestProcessFuturesFillEvent(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{},
+ Datas: &data.HandlerPerCurrency{},
+ }
+ cp := currency.NewPair(currency.BTC, currency.USD)
+ a := asset.Futures
+ de := &evkline.Kline{
+ Base: &event.Base{Exchange: testExchange,
+ AssetType: a,
+ CurrencyPair: cp},
+ }
+ err = bt.Statistic.SetupEventForTime(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()
+ cfg, err := exch.GetDefaultConfig()
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = exch.Setup(cfg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ em.Add(exch)
+ f, err := funding.SetupFundingManager(em, false, true)
+ 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)
+ }
+
+ err = f.AddItem(b)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ err = f.AddItem(quote)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ spotBase, err := funding.CreateItem(testExchange, asset.Spot, cp.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ spotQuote, err := funding.CreateItem(testExchange, asset.Spot, cp.Quote, decimal.NewFromInt(1337), decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ spotPair, err := funding.CreatePair(spotBase, spotQuote)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ err = f.AddPair(spotPair)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+ bt.exchangeManager = em
+ bt.Funding = f
+ err = pt.SetupCurrencySettingsMap(&exchange.Settings{
+ Exchange: exch,
+ Pair: cp,
+ Asset: a,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ 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.Datas.Setup()
+ 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 err != nil {
+ t.Error(err)
+ }
+ ev.Order = &gctorder.Detail{
+ Exchange: testExchange,
+ AssetType: ev.AssetType,
+ Pair: cp,
+ Amount: 1,
+ Price: 1,
+ Side: gctorder.Short,
+ OrderID: "1",
+ Date: time.Now(),
+ }
+ bt.Datas.SetDataForCurrency(testExchange, a, cp, &k)
+ err = bt.processFuturesFillEvent(ev, pair)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v'", err, expectedError)
+ }
+}
diff --git a/backtester/backtest/backtest_types.go b/backtester/engine/backtest_types.go
similarity index 99%
rename from backtester/backtest/backtest_types.go
rename to backtester/engine/backtest_types.go
index c6d9266ccf4..248bc70513c 100644
--- a/backtester/backtest/backtest_types.go
+++ b/backtester/engine/backtest_types.go
@@ -1,4 +1,4 @@
-package backtest
+package engine
import (
"errors"
diff --git a/backtester/engine/live.go b/backtester/engine/live.go
new file mode 100644
index 00000000000..e5257febe9d
--- /dev/null
+++ b/backtester/engine/live.go
@@ -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
+}
diff --git a/backtester/engine/setup.go b/backtester/engine/setup.go
new file mode 100644
index 00000000000..74b9c233630
--- /dev/null
+++ b/backtester/engine/setup.go
@@ -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
+}
diff --git a/backtester/eventhandlers/exchange/exchange.go b/backtester/eventhandlers/exchange/exchange.go
index 678ddb86ed8..a4db12b8db2 100644
--- a/backtester/eventhandlers/exchange/exchange.go
+++ b/backtester/eventhandlers/exchange/exchange.go
@@ -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(),
- }
- eventFunds := o.GetAllocatedFunds()
+ Base: o.GetBase(),
+ Direction: o.GetDirection(),
+ Amount: o.GetAmount(),
+ ClosePrice: o.GetClosePrice(),
+ FillDependentEvent: o.GetFillDependentEvent(),
+ Liquidated: o.IsLiquidating(),
+ }
+ 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
}
diff --git a/backtester/eventhandlers/exchange/exchange_test.go b/backtester/eventhandlers/exchange/exchange_test.go
index 173f9abe765..41d9fe4df0d 100644
--- a/backtester/eventhandlers/exchange/exchange_test.go
+++ b/backtester/eventhandlers/exchange/exchange_test.go
@@ -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,
- }
- e.SetExchangeAssetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
+ Exchange: f,
+ UseRealOrders: true,
+ Pair: currency.NewPair(currency.BTC, currency.USDT),
+ Asset: asset.Spot,
+ }
+ 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),
- }
-
- 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,
- },
+ ClosePrice: decimal.NewFromInt(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)
+ }
+}
diff --git a/backtester/eventhandlers/exchange/exchange_types.go b/backtester/eventhandlers/exchange/exchange_types.go
index c265082dca7..f1d1be84b21 100644
--- a/backtester/eventhandlers/exchange/exchange_types.go
+++ b/backtester/eventhandlers/exchange/exchange_types.go
@@ -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.
diff --git a/backtester/eventhandlers/exchange/slippage/slippage.go b/backtester/eventhandlers/exchange/slippage/slippage.go
index efed0e02d88..364e7edf163 100644
--- a/backtester/eventhandlers/exchange/slippage/slippage.go
+++ b/backtester/eventhandlers/exchange/slippage/slippage.go
@@ -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()))
diff --git a/backtester/eventhandlers/portfolio/compliance/compliance.go b/backtester/eventhandlers/portfolio/compliance/compliance.go
index 08e7fec05ba..aadfd92fdff 100644
--- a/backtester/eventhandlers/portfolio/compliance/compliance.go
+++ b/backtester/eventhandlers/portfolio/compliance/compliance.go
@@ -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
}
diff --git a/backtester/eventhandlers/portfolio/compliance/compliance_test.go b/backtester/eventhandlers/portfolio/compliance/compliance_test.go
index 6508d5b7ddf..8385642e5a8 100644
--- a/backtester/eventhandlers/portfolio/compliance/compliance_test.go
+++ b/backtester/eventhandlers/portfolio/compliance/compliance_test.go
@@ -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) {
diff --git a/backtester/eventhandlers/portfolio/compliance/compliance_types.go b/backtester/eventhandlers/portfolio/compliance/compliance_types.go
index 51584ff7d6e..afa7168d986 100644
--- a/backtester/eventhandlers/portfolio/compliance/compliance_types.go
+++ b/backtester/eventhandlers/portfolio/compliance/compliance_types.go
@@ -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"`
}
diff --git a/backtester/eventhandlers/portfolio/holdings/holdings.go b/backtester/eventhandlers/portfolio/holdings/holdings.go
index e3eecf10ead..95468d78ba7 100644
--- a/backtester/eventhandlers/portfolio/holdings/holdings.go
+++ b/backtester/eventhandlers/portfolio/holdings/holdings.go
@@ -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
+ }
+ 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.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())
+ 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)
diff --git a/backtester/eventhandlers/portfolio/holdings/holdings_test.go b/backtester/eventhandlers/portfolio/holdings/holdings_test.go
index 1ce7c32c1d1..998bb2d5426 100644
--- a/backtester/eventhandlers/portfolio/holdings/holdings_test.go
+++ b/backtester/eventhandlers/portfolio/holdings/holdings_test.go
@@ -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)
}
diff --git a/backtester/eventhandlers/portfolio/holdings/holdings_types.go b/backtester/eventhandlers/portfolio/holdings/holdings_types.go
index f7aa55aa473..ad7fa7d71ec 100644
--- a/backtester/eventhandlers/portfolio/holdings/holdings_types.go
+++ b/backtester/eventhandlers/portfolio/holdings/holdings_types.go
@@ -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"`
diff --git a/backtester/eventhandlers/portfolio/portfolio.go b/backtester/eventhandlers/portfolio/portfolio.go
index ee25b2f5b15..6c0d3c03085 100644
--- a/backtester/eventhandlers/portfolio/portfolio.go
+++ b/backtester/eventhandlers/portfolio/portfolio.go
@@ -3,6 +3,7 @@ package portfolio
import (
"errors"
"fmt"
+ "strings"
"time"
"github.com/shopspring/decimal"
@@ -10,72 +11,23 @@ import (
"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/holdings"
- "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk"
"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/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
+ "github.com/thrasher-corp/gocryptotrader/config"
"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"
)
-// 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
-}
-
-// GetLatestOrderSnapshotForEvent gets orders related to the event
-func (p *Portfolio) GetLatestOrderSnapshotForEvent(e common.EventHandler) (compliance.Snapshot, error) {
- eapSettings, ok := p.exchangeAssetPairSettings[e.GetExchange()][e.GetAssetType()][e.Pair()]
- if !ok {
- return compliance.Snapshot{}, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, e.GetExchange(), e.GetAssetType(), e.Pair())
- }
- return eapSettings.ComplianceManager.GetLatestSnapshot(), nil
-}
-
-// GetLatestOrderSnapshots returns the latest snapshots from all stored pair data
-func (p *Portfolio) GetLatestOrderSnapshots() ([]compliance.Snapshot, error) {
- var resp []compliance.Snapshot
- for _, exchangeMap := range p.exchangeAssetPairSettings {
- for _, assetMap := range exchangeMap {
- for _, pairMap := range assetMap {
- resp = append(resp, pairMap.ComplianceManager.GetLatestSnapshot())
- }
- }
- }
- if len(resp) == 0 {
- return nil, errNoPortfolioSettings
- }
- return resp, nil
-}
-
// OnSignal receives the event from the strategy on whether it has signalled to buy, do nothing or sell
// on buy/sell, the portfolio manager will size the order and assess the risk of the order
// if successful, it will pass on an order.Order to be used by the exchange event handler to place an order based on
// the portfolio manager's recommendations
-func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds funding.IPairReserver) (*order.Order, error) {
+func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds funding.IFundReserver) (*order.Order, error) {
if ev == nil || cs == nil {
return nil, common.ErrNilArguments
}
@@ -90,16 +42,11 @@ func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds fundi
}
o := &order.Order{
- Base: event.Base{
- Offset: ev.GetOffset(),
- Exchange: ev.GetExchange(),
- Time: ev.GetTime(),
- CurrencyPair: ev.Pair(),
- AssetType: ev.GetAssetType(),
- Interval: ev.GetInterval(),
- Reason: ev.GetReason(),
- },
- Direction: ev.GetDirection(),
+ Base: ev.GetBase(),
+ Direction: ev.GetDirection(),
+ FillDependentEvent: ev.GetFillDependentEvent(),
+ Amount: ev.GetAmount(),
+ ClosePrice: ev.GetClosePrice(),
}
if ev.GetDirection() == gctorder.UnknownSide {
return o, errInvalidDirection
@@ -119,50 +66,113 @@ func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds fundi
ev.GetDirection() == gctorder.TransferredFunds {
return o, nil
}
-
if !funds.CanPlaceOrder(ev.GetDirection()) {
- if ev.GetDirection() == gctorder.Sell {
- o.AppendReason("no holdings to sell")
- o.SetDirection(gctorder.CouldNotSell)
- } else if ev.GetDirection() == gctorder.Buy {
- o.AppendReason("not enough funds to buy")
- o.SetDirection(gctorder.CouldNotBuy)
- }
- ev.SetDirection(o.Direction)
- return o, nil
+ return cannotPurchase(ev, o)
}
- o.Price = ev.GetPrice()
o.OrderType = gctorder.Market
o.BuyLimit = ev.GetBuyLimit()
o.SellLimit = ev.GetSellLimit()
var sizingFunds decimal.Decimal
- if ev.GetDirection() == gctorder.Sell {
- sizingFunds = funds.BaseAvailable()
- } else {
- sizingFunds = funds.QuoteAvailable()
+ var side = ev.GetDirection()
+ if ev.GetAssetType() == asset.Spot {
+ if side == gctorder.ClosePosition {
+ side = gctorder.Sell
+ }
+ pReader, err := funds.GetPairReader()
+ if err != nil {
+ return nil, err
+ }
+ switch side {
+ case gctorder.Buy, gctorder.Bid:
+ sizingFunds = pReader.QuoteAvailable()
+ case gctorder.Sell, gctorder.Ask:
+ sizingFunds = pReader.BaseAvailable()
+ }
+ } else if ev.GetAssetType().IsFutures() {
+ if ev.GetDirection() == gctorder.ClosePosition {
+ // lookup position
+ positions := lookup.FuturesTracker.GetPositions()
+ if len(positions) == 0 {
+ // cannot close a non existent position
+ return nil, errNoHoldings
+ }
+ sizingFunds = positions[len(positions)-1].Exposure
+ d := positions[len(positions)-1].OpeningDirection
+ switch d {
+ case gctorder.Short:
+ side = gctorder.Long
+ case gctorder.Long:
+ side = gctorder.Short
+ }
+ } else {
+ collateralFunds, err := funds.GetCollateralReader()
+ if err != nil {
+ return nil, err
+ }
+ sizingFunds = collateralFunds.AvailableFunds()
+ }
+ }
+ if sizingFunds.LessThanOrEqual(decimal.Zero) {
+ return cannotPurchase(ev, o)
+ }
+ sizedOrder, err := p.sizeOrder(ev, cs, o, sizingFunds, funds)
+ if err != nil {
+ return sizedOrder, err
+ }
+ if common.CanTransact(sizedOrder.Direction) {
+ sizedOrder.SetDirection(side)
+ }
+ if ev.GetDirection() == gctorder.ClosePosition {
+ sizedOrder.ClosingPosition = true
}
- sizedOrder := p.sizeOrder(ev, cs, o, sizingFunds, funds)
-
return p.evaluateOrder(ev, o, sizedOrder)
}
-func (p *Portfolio) evaluateOrder(d common.Directioner, originalOrderSignal, sizedOrder *order.Order) (*order.Order, error) {
+func cannotPurchase(ev signal.Event, o *order.Order) (*order.Order, error) {
+ if ev == nil {
+ return nil, common.ErrNilEvent
+ }
+ if o == nil {
+ return nil, fmt.Errorf("%w received nil order for %v %v %v", common.ErrNilArguments, ev.GetExchange(), ev.GetAssetType(), ev.Pair())
+ }
+ o.AppendReason(notEnoughFundsTo + " " + ev.GetDirection().Lower())
+ switch ev.GetDirection() {
+ case gctorder.Buy, gctorder.Bid:
+ o.SetDirection(gctorder.CouldNotBuy)
+ case gctorder.Sell, gctorder.Ask:
+ o.SetDirection(gctorder.CouldNotSell)
+ case gctorder.Short:
+ o.SetDirection(gctorder.CouldNotShort)
+ case gctorder.Long:
+ o.SetDirection(gctorder.CouldNotLong)
+ default:
+ // ensure that unknown scenarios don't affect anything
+ o.SetDirection(gctorder.DoNothing)
+ }
+ ev.SetDirection(o.Direction)
+ return o, nil
+}
+
+func (p *Portfolio) evaluateOrder(d common.Directioner, originalOrderSignal, ev *order.Order) (*order.Order, error) {
var evaluatedOrder *order.Order
cm, err := p.GetComplianceManager(originalOrderSignal.GetExchange(), originalOrderSignal.GetAssetType(), originalOrderSignal.Pair())
if err != nil {
return nil, err
}
- evaluatedOrder, err = p.riskManager.EvaluateOrder(sizedOrder, p.GetLatestHoldingsForAllCurrencies(), cm.GetLatestSnapshot())
+ evaluatedOrder, err = p.riskManager.EvaluateOrder(ev, p.GetLatestHoldingsForAllCurrencies(), cm.GetLatestSnapshot())
if err != nil {
originalOrderSignal.AppendReason(err.Error())
switch d.GetDirection() {
- case gctorder.Buy:
+ case gctorder.Buy, gctorder.CouldNotBuy:
originalOrderSignal.Direction = gctorder.CouldNotBuy
- case gctorder.Sell:
+ case gctorder.Sell, gctorder.CouldNotSell:
originalOrderSignal.Direction = gctorder.CouldNotSell
- case gctorder.CouldNotBuy, gctorder.CouldNotSell:
+ case gctorder.Short:
+ originalOrderSignal.Direction = gctorder.CouldNotShort
+ case gctorder.Long:
+ originalOrderSignal.Direction = gctorder.CouldNotLong
default:
originalOrderSignal.Direction = gctorder.DoNothing
}
@@ -173,50 +183,51 @@ func (p *Portfolio) evaluateOrder(d common.Directioner, originalOrderSignal, siz
return evaluatedOrder, nil
}
-func (p *Portfolio) sizeOrder(d common.Directioner, cs *exchange.Settings, originalOrderSignal *order.Order, sizingFunds decimal.Decimal, funds funding.IPairReserver) *order.Order {
- sizedOrder, err := p.sizeManager.SizeOrder(originalOrderSignal, sizingFunds, cs)
- if err != nil {
- originalOrderSignal.AppendReason(err.Error())
+func (p *Portfolio) sizeOrder(d common.Directioner, cs *exchange.Settings, originalOrderSignal *order.Order, sizingFunds decimal.Decimal, funds funding.IFundReserver) (*order.Order, error) {
+ sizedOrder, estFee, err := p.sizeManager.SizeOrder(originalOrderSignal, sizingFunds, cs)
+ if err != nil || sizedOrder.Amount.IsZero() {
switch originalOrderSignal.Direction {
- case gctorder.Buy:
+ case gctorder.Buy, gctorder.Bid:
originalOrderSignal.Direction = gctorder.CouldNotBuy
- case gctorder.Sell:
+ case gctorder.Sell, gctorder.Ask:
originalOrderSignal.Direction = gctorder.CouldNotSell
+ case gctorder.Long:
+ originalOrderSignal.Direction = gctorder.CouldNotLong
+ case gctorder.Short:
+ originalOrderSignal.Direction = gctorder.CouldNotShort
default:
originalOrderSignal.Direction = gctorder.DoNothing
}
d.SetDirection(originalOrderSignal.Direction)
- return originalOrderSignal
- }
-
- if sizedOrder.Amount.IsZero() {
- switch originalOrderSignal.Direction {
- case gctorder.Buy:
- originalOrderSignal.Direction = gctorder.CouldNotBuy
- case gctorder.Sell:
- originalOrderSignal.Direction = gctorder.CouldNotSell
- default:
- originalOrderSignal.Direction = gctorder.DoNothing
+ if err != nil {
+ originalOrderSignal.AppendReason(err.Error())
+ return originalOrderSignal, nil
}
- d.SetDirection(originalOrderSignal.Direction)
originalOrderSignal.AppendReason("sized order to 0")
}
- if d.GetDirection() == gctorder.Sell {
- err = funds.Reserve(sizedOrder.Amount, gctorder.Sell)
+ switch d.GetDirection() {
+ case gctorder.Buy,
+ gctorder.Bid,
+ gctorder.Sell,
+ gctorder.Ask,
+ gctorder.Short,
+ gctorder.Long:
+ sizedOrder.AllocatedFunds = sizedOrder.Amount.Mul(sizedOrder.ClosePrice).Add(estFee)
+ case gctorder.ClosePosition:
sizedOrder.AllocatedFunds = sizedOrder.Amount
- } else {
- err = funds.Reserve(sizedOrder.Amount.Mul(sizedOrder.Price), gctorder.Buy)
- sizedOrder.AllocatedFunds = sizedOrder.Amount.Mul(sizedOrder.Price)
+ default:
+ return nil, errInvalidDirection
}
+ err = funds.Reserve(sizedOrder.AllocatedFunds, d.GetDirection())
if err != nil {
sizedOrder.Direction = gctorder.DoNothing
- sizedOrder.AppendReason(err.Error())
+ return sizedOrder, err
}
- return sizedOrder
+ return sizedOrder, nil
}
// OnFill processes the event after an order has been placed by the exchange. Its purpose is to track holdings for future portfolio decisions.
-func (p *Portfolio) OnFill(ev fill.Event, funding funding.IPairReader) (*fill.Fill, error) {
+func (p *Portfolio) OnFill(ev fill.Event, funds funding.IFundReleaser) (fill.Event, error) {
if ev == nil {
return nil, common.ErrNilEvent
}
@@ -229,16 +240,22 @@ func (p *Portfolio) OnFill(ev fill.Event, funding funding.IPairReader) (*fill.Fi
// Get the holding from the previous iteration, create it if it doesn't yet have a timestamp
h := lookup.GetHoldingsForTime(ev.GetTime().Add(-ev.GetInterval().Duration()))
if !h.Timestamp.IsZero() {
- h.Update(ev, funding)
+ err = h.Update(ev, funds)
+ if err != nil {
+ return nil, err
+ }
} else {
h = lookup.GetLatestHoldings()
if h.Timestamp.IsZero() {
- h, err = holdings.Create(ev, funding)
+ h, err = holdings.Create(ev, funds)
if err != nil {
return nil, err
}
} else {
- h.Update(ev, funding)
+ err = h.Update(ev, funds)
+ if err != nil {
+ return nil, err
+ }
}
}
err = p.setHoldingsForOffset(&h, true)
@@ -246,33 +263,15 @@ func (p *Portfolio) OnFill(ev fill.Event, funding funding.IPairReader) (*fill.Fi
err = p.setHoldingsForOffset(&h, false)
}
if err != nil {
- log.Error(log.BackTester, err)
+ log.Error(common.Portfolio, err)
}
err = p.addComplianceSnapshot(ev)
if err != nil {
- log.Error(log.BackTester, err)
- }
-
- direction := ev.GetDirection()
- if direction == gctorder.DoNothing ||
- direction == gctorder.CouldNotBuy ||
- direction == gctorder.CouldNotSell ||
- direction == gctorder.MissingData ||
- direction == gctorder.UnknownSide {
- fe, ok := ev.(*fill.Fill)
- if !ok {
- return nil, fmt.Errorf("%w expected fill event", common.ErrInvalidDataType)
- }
- fe.ExchangeFee = decimal.Zero
- return fe, nil
+ log.Error(common.Portfolio, err)
}
- fe, ok := ev.(*fill.Fill)
- if !ok {
- return nil, fmt.Errorf("%w expected fill event", common.ErrInvalidDataType)
- }
- return fe, nil
+ return ev, nil
}
// addComplianceSnapshot gets the previous snapshot of compliance events, updates with the latest fillevent
@@ -294,67 +293,105 @@ func (p *Portfolio) addComplianceSnapshot(fillEvent fill.Event) error {
ClosePrice: fillEvent.GetClosePrice(),
VolumeAdjustedPrice: fillEvent.GetVolumeAdjustedPrice(),
SlippageRate: fillEvent.GetSlippageRate(),
- Detail: fo,
CostBasis: price.Mul(amount).Add(fee),
}
+ snapOrder.Order = fo
prevSnap.Orders = append(prevSnap.Orders, snapOrder)
}
- return complianceManager.AddSnapshot(prevSnap.Orders, fillEvent.GetTime(), fillEvent.GetOffset(), false)
+ snap := &compliance.Snapshot{
+ Offset: fillEvent.GetOffset(),
+ Timestamp: fillEvent.GetTime(),
+ Orders: prevSnap.Orders,
+ }
+ return complianceManager.AddSnapshot(snap, false)
}
-// GetComplianceManager returns the order snapshots for a given exchange, asset, pair
-func (p *Portfolio) GetComplianceManager(exchangeName string, a asset.Item, cp currency.Pair) (*compliance.Manager, error) {
- lookup := p.exchangeAssetPairSettings[exchangeName][a][cp]
- if lookup == nil {
- return nil, fmt.Errorf("%w for %v %v %v could not retrieve compliance manager", errNoPortfolioSettings, exchangeName, a, cp)
+func (p *Portfolio) setHoldingsForOffset(h *holdings.Holding, overwriteExisting bool) error {
+ if h.Timestamp.IsZero() {
+ return errHoldingsNoTimestamp
}
- return &lookup.ComplianceManager, nil
+ lookup, ok := p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair]
+ if !ok {
+ return fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, h.Exchange, h.Asset, h.Pair)
+ }
+
+ if overwriteExisting && len(lookup.HoldingsSnapshots) == 0 {
+ return errNoHoldings
+ }
+ for i := len(lookup.HoldingsSnapshots) - 1; i >= 0; i-- {
+ if lookup.HoldingsSnapshots[i].Offset == h.Offset {
+ if overwriteExisting {
+ lookup.HoldingsSnapshots[i] = *h
+ p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair] = lookup
+ return nil
+ }
+ return errHoldingsAlreadySet
+ }
+ }
+ if overwriteExisting {
+ return fmt.Errorf("%w at %v", errNoHoldings, h.Timestamp)
+ }
+
+ lookup.HoldingsSnapshots = append(lookup.HoldingsSnapshots, *h)
+ p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair] = lookup
+ return nil
}
-// SetFee sets the fee rate
-func (p *Portfolio) SetFee(exch string, a asset.Item, cp currency.Pair, fee decimal.Decimal) {
- lookup := p.exchangeAssetPairSettings[exch][a][cp]
- lookup.Fee = fee
+// GetLatestOrderSnapshotForEvent gets orders related to the event
+func (p *Portfolio) GetLatestOrderSnapshotForEvent(e common.EventHandler) (compliance.Snapshot, error) {
+ eapSettings, ok := p.exchangeAssetPairSettings[e.GetExchange()][e.GetAssetType()][e.Pair()]
+ if !ok {
+ return compliance.Snapshot{}, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, e.GetExchange(), e.GetAssetType(), e.Pair())
+ }
+ return eapSettings.ComplianceManager.GetLatestSnapshot(), nil
}
-// GetFee can panic for bad requests, but why are you getting things that don't exist?
-func (p *Portfolio) GetFee(exchangeName string, a asset.Item, cp currency.Pair) decimal.Decimal {
- if p.exchangeAssetPairSettings == nil {
- return decimal.Zero
+// GetLatestOrderSnapshots returns the latest snapshots from all stored pair data
+func (p *Portfolio) GetLatestOrderSnapshots() ([]compliance.Snapshot, error) {
+ var resp []compliance.Snapshot
+ for _, exchangeMap := range p.exchangeAssetPairSettings {
+ for _, assetMap := range exchangeMap {
+ for _, pairMap := range assetMap {
+ resp = append(resp, pairMap.ComplianceManager.GetLatestSnapshot())
+ }
+ }
}
+ if len(resp) == 0 {
+ return nil, errNoPortfolioSettings
+ }
+ return resp, nil
+}
+
+// GetComplianceManager returns the order snapshots for a given exchange, asset, pair
+func (p *Portfolio) GetComplianceManager(exchangeName string, a asset.Item, cp currency.Pair) (*compliance.Manager, error) {
lookup := p.exchangeAssetPairSettings[exchangeName][a][cp]
if lookup == nil {
- return decimal.Zero
+ return nil, fmt.Errorf("%w for %v %v %v could not retrieve compliance manager", errNoPortfolioSettings, exchangeName, a, cp)
}
- return lookup.Fee
+ return &lookup.ComplianceManager, nil
}
// UpdateHoldings updates the portfolio holdings for the data event
-func (p *Portfolio) UpdateHoldings(ev common.DataEventHandler, funds funding.IPairReader) error {
- if ev == nil {
+func (p *Portfolio) UpdateHoldings(e common.DataEventHandler, funds funding.IFundReleaser) error {
+ if e == nil {
return common.ErrNilEvent
}
if funds == nil {
return funding.ErrFundsNotFound
}
- lookup, ok := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
- if !ok {
- return fmt.Errorf("%w for %v %v %v",
- errNoPortfolioSettings,
- ev.GetExchange(),
- ev.GetAssetType(),
- ev.Pair())
+ settings, err := p.getSettings(e.GetExchange(), e.GetAssetType(), e.Pair())
+ if err != nil {
+ return fmt.Errorf("%v %v %v %w", e.GetExchange(), e.GetAssetType(), e.Pair(), err)
}
- h := lookup.GetLatestHoldings()
+ h := settings.GetLatestHoldings()
if h.Timestamp.IsZero() {
- var err error
- h, err = holdings.Create(ev, funds)
+ h, err = holdings.Create(e, funds)
if err != nil {
return err
}
}
- h.UpdateValue(ev)
- err := p.setHoldingsForOffset(&h, true)
+ h.UpdateValue(e)
+ err = p.setHoldingsForOffset(&h, true)
if errors.Is(err, errNoHoldings) {
err = p.setHoldingsForOffset(&h, false)
}
@@ -378,101 +415,382 @@ func (p *Portfolio) GetLatestHoldingsForAllCurrencies() []holdings.Holding {
return resp
}
-func (p *Portfolio) setHoldingsForOffset(h *holdings.Holding, overwriteExisting bool) error {
- if h.Timestamp.IsZero() {
- return errHoldingsNoTimestamp
+// ViewHoldingAtTimePeriod retrieves a snapshot of holdings at a specific time period,
+// returning empty when not found
+func (p *Portfolio) ViewHoldingAtTimePeriod(ev common.EventHandler) (*holdings.Holding, error) {
+ exchangeAssetPairSettings := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
+ if exchangeAssetPairSettings == nil {
+ return nil, fmt.Errorf("%w for %v %v %v", errNoHoldings, ev.GetExchange(), ev.GetAssetType(), ev.Pair())
}
- lookup, ok := p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair]
- if !ok {
- return fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, h.Exchange, h.Asset, h.Pair)
+
+ for i := len(exchangeAssetPairSettings.HoldingsSnapshots) - 1; i >= 0; i-- {
+ if ev.GetTime().Equal(exchangeAssetPairSettings.HoldingsSnapshots[i].Timestamp) {
+ return &exchangeAssetPairSettings.HoldingsSnapshots[i], nil
+ }
}
- if overwriteExisting && len(lookup.HoldingsSnapshots) == 0 {
- return errNoHoldings
+ return nil, fmt.Errorf("%w for %v %v %v at %v", errNoHoldings, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetTime())
+}
+
+// GetLatestHoldings returns the latest holdings after being sorted by time
+func (s *Settings) GetLatestHoldings() holdings.Holding {
+ if len(s.HoldingsSnapshots) == 0 {
+ return holdings.Holding{}
}
- for i := len(lookup.HoldingsSnapshots) - 1; i >= 0; i-- {
- if lookup.HoldingsSnapshots[i].Offset == h.Offset {
- if overwriteExisting {
- lookup.HoldingsSnapshots[i] = *h
- return nil
- }
- return errHoldingsAlreadySet
+
+ return s.HoldingsSnapshots[len(s.HoldingsSnapshots)-1]
+}
+
+// GetHoldingsForTime returns the holdings for a time period, or an empty holding if not found
+func (s *Settings) GetHoldingsForTime(t time.Time) holdings.Holding {
+ for i := len(s.HoldingsSnapshots) - 1; i >= 0; i-- {
+ if s.HoldingsSnapshots[i].Timestamp.Equal(t) {
+ return s.HoldingsSnapshots[i]
}
}
- if overwriteExisting {
- return fmt.Errorf("%w at %v", errNoHoldings, h.Timestamp)
+ return holdings.Holding{}
+}
+
+// GetPositions returns all futures positions for an event's exchange, asset, pair
+func (p *Portfolio) GetPositions(e common.EventHandler) ([]gctorder.PositionStats, error) {
+ settings, err := p.getFuturesSettingsFromEvent(e)
+ if err != nil {
+ return nil, err
+ }
+ return settings.FuturesTracker.GetPositions(), nil
+}
+
+// GetLatestPosition returns all futures positions for an event's exchange, asset, pair
+func (p *Portfolio) GetLatestPosition(e common.EventHandler) (*gctorder.PositionStats, error) {
+ settings, err := p.getFuturesSettingsFromEvent(e)
+ if err != nil {
+ return nil, err
+ }
+ positions := settings.FuturesTracker.GetPositions()
+ if len(positions) == 0 {
+ return nil, fmt.Errorf("%w %v %v %v", gctorder.ErrPositionsNotLoadedForPair, e.GetExchange(), e.GetAssetType(), e.Pair())
+ }
+ return &positions[len(positions)-1], nil
+}
+
+// UpdatePNL will analyse any futures orders that have been placed over the backtesting run
+// that are not closed and calculate their PNL
+func (p *Portfolio) UpdatePNL(e common.EventHandler, closePrice decimal.Decimal) error {
+ settings, err := p.getFuturesSettingsFromEvent(e)
+ if err != nil {
+ return err
+ }
+ _, err = settings.FuturesTracker.UpdateOpenPositionUnrealisedPNL(closePrice.InexactFloat64(), e.GetTime())
+ if err != nil && !errors.Is(err, gctorder.ErrPositionClosed) {
+ return err
}
- lookup.HoldingsSnapshots = append(lookup.HoldingsSnapshots, *h)
return nil
}
-// ViewHoldingAtTimePeriod retrieves a snapshot of holdings at a specific time period,
-// returning empty when not found
-func (p *Portfolio) ViewHoldingAtTimePeriod(ev common.EventHandler) (*holdings.Holding, error) {
- exchangeAssetPairSettings := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
- if exchangeAssetPairSettings == nil {
- return nil, fmt.Errorf("%w for %v %v %v", errNoHoldings, ev.GetExchange(), ev.GetAssetType(), ev.Pair())
+// TrackFuturesOrder updates the futures tracker with a new order
+// from a fill event
+func (p *Portfolio) TrackFuturesOrder(ev fill.Event, fund funding.IFundReleaser) (*PNLSummary, error) {
+ if ev == nil {
+ return nil, common.ErrNilEvent
+ }
+ if fund == nil {
+ return nil, fmt.Errorf("%w missing funding", common.ErrNilArguments)
+ }
+ detail := ev.GetOrder()
+ if detail == nil {
+ return nil, gctorder.ErrSubmissionIsNil
+ }
+ if !detail.AssetType.IsFutures() {
+ return nil, fmt.Errorf("order '%v' %w", detail.OrderID, gctorder.ErrNotFuturesAsset)
}
- for i := len(exchangeAssetPairSettings.HoldingsSnapshots) - 1; i >= 0; i-- {
- if ev.GetTime().Equal(exchangeAssetPairSettings.HoldingsSnapshots[i].Timestamp) {
- return &exchangeAssetPairSettings.HoldingsSnapshots[i], nil
+ collateralReleaser, err := fund.CollateralReleaser()
+ if err != nil {
+ return nil, fmt.Errorf("%v %v %v %w", detail.Exchange, detail.AssetType, detail.Pair, err)
+ }
+ settings, err := p.getSettings(detail.Exchange, detail.AssetType, detail.Pair)
+ if err != nil {
+ return nil, fmt.Errorf("%v %v %v %w", detail.Exchange, detail.AssetType, detail.Pair, err)
+ }
+
+ err = settings.FuturesTracker.TrackNewOrder(detail)
+ if err != nil {
+ return nil, err
+ }
+
+ pos := settings.FuturesTracker.GetPositions()
+ if len(pos) == 0 {
+ return nil, fmt.Errorf("%w should not happen", errNoHoldings)
+ }
+ amount := decimal.NewFromFloat(detail.Amount)
+ switch {
+ case ev.IsLiquidated():
+ collateralReleaser.Liquidate()
+ err = settings.FuturesTracker.Liquidate(ev.GetClosePrice(), ev.GetTime())
+ if err != nil {
+ return nil, err
+ }
+ case pos[len(pos)-1].OpeningDirection != detail.Side:
+ err = collateralReleaser.TakeProfit(amount, pos[len(pos)-1].RealisedPNL)
+ if err != nil {
+ return nil, err
+ }
+ err = p.UpdatePNL(ev, ev.GetClosePrice())
+ if err != nil {
+ return nil, fmt.Errorf("%v %v %v %w", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
+ }
+ default:
+ err = collateralReleaser.UpdateContracts(detail.Side, amount)
+ if err != nil {
+ return nil, err
}
}
- return nil, fmt.Errorf("%w for %v %v %v at %v", errNoHoldings, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetTime())
+ return p.GetLatestPNLForEvent(ev)
}
-// SetupCurrencySettingsMap ensures a map is created and no panics happen
-func (p *Portfolio) SetupCurrencySettingsMap(settings *exchange.Settings) (*Settings, error) {
- if settings == nil {
- return nil, errNoPortfolioSettings
+// GetLatestPNLForEvent takes in an event and returns the latest PNL data
+// if it exists
+func (p *Portfolio) GetLatestPNLForEvent(e common.EventHandler) (*PNLSummary, error) {
+ if e == nil {
+ return nil, common.ErrNilEvent
}
- if settings.Exchange == "" {
- return nil, errExchangeUnset
+ response := &PNLSummary{
+ Exchange: e.GetExchange(),
+ Item: e.GetAssetType(),
+ Pair: e.Pair(),
+ Offset: e.GetOffset(),
}
- if settings.Asset == asset.Empty {
- return nil, errAssetUnset
+ position, err := p.GetLatestPosition(e)
+ if err != nil {
+ return nil, err
}
- if settings.Pair.IsEmpty() {
- return nil, errCurrencyPairUnset
+ pnlHistory := position.PNLHistory
+ if len(pnlHistory) == 0 {
+ return response, nil
}
- if p.exchangeAssetPairSettings == nil {
- p.exchangeAssetPairSettings = make(map[string]map[asset.Item]map[currency.Pair]*Settings)
+ response.Result = pnlHistory[len(pnlHistory)-1]
+ response.CollateralCurrency = position.CollateralCurrency
+ return response, nil
+}
+
+// CheckLiquidationStatus checks funding against position
+// and liquidates and removes funding if position unable to continue
+func (p *Portfolio) CheckLiquidationStatus(ev common.DataEventHandler, collateralReader funding.ICollateralReader, pnl *PNLSummary) error {
+ if ev == nil {
+ return common.ErrNilEvent
+ }
+ if collateralReader == nil {
+ return fmt.Errorf("%w collateral reader missing", common.ErrNilArguments)
}
- if p.exchangeAssetPairSettings[settings.Exchange] == nil {
- p.exchangeAssetPairSettings[settings.Exchange] = make(map[asset.Item]map[currency.Pair]*Settings)
+ if pnl == nil {
+ return fmt.Errorf("%w pnl summary missing", common.ErrNilArguments)
}
- if p.exchangeAssetPairSettings[settings.Exchange][settings.Asset] == nil {
- p.exchangeAssetPairSettings[settings.Exchange][settings.Asset] = make(map[currency.Pair]*Settings)
+ availableFunds := collateralReader.AvailableFunds()
+ position, err := p.GetLatestPosition(ev)
+ if err != nil {
+ return err
}
- if _, ok := p.exchangeAssetPairSettings[settings.Exchange][settings.Asset][settings.Pair]; !ok {
- p.exchangeAssetPairSettings[settings.Exchange][settings.Asset][settings.Pair] = &Settings{}
+ if !position.Status.IsInactive() &&
+ pnl.Result.UnrealisedPNL.IsNegative() &&
+ pnl.Result.UnrealisedPNL.Abs().GreaterThan(availableFunds) {
+ return gctorder.ErrPositionLiquidated
}
- return p.exchangeAssetPairSettings[settings.Exchange][settings.Asset][settings.Pair], nil
+ return nil
}
-// GetLatestHoldings returns the latest holdings after being sorted by time
-func (e *Settings) GetLatestHoldings() holdings.Holding {
- if len(e.HoldingsSnapshots) == 0 {
- return holdings.Holding{}
+// CreateLiquidationOrdersForExchange creates liquidation orders, for any that exist on the same exchange where a liquidation is occurring
+func (p *Portfolio) CreateLiquidationOrdersForExchange(ev common.DataEventHandler, funds funding.IFundingManager) ([]order.Event, error) {
+ if ev == nil {
+ return nil, common.ErrNilEvent
+ }
+ if funds == nil {
+ return nil, fmt.Errorf("%w, requires funding manager", common.ErrNilArguments)
+ }
+ var closingOrders []order.Event
+ assetPairSettings, ok := p.exchangeAssetPairSettings[ev.GetExchange()]
+ if !ok {
+ return nil, config.ErrExchangeNotFound
+ }
+ for item, pairMap := range assetPairSettings {
+ for pair, settings := range pairMap {
+ switch {
+ case item.IsFutures():
+ positions := settings.FuturesTracker.GetPositions()
+ if len(positions) == 0 {
+ continue
+ }
+ pos := positions[len(positions)-1]
+ if !pos.Exposure.IsPositive() {
+ continue
+ }
+ direction := gctorder.Short
+ if pos.LatestDirection == gctorder.Short {
+ direction = gctorder.Long
+ }
+ closingOrders = append(closingOrders, &order.Order{
+ Base: &event.Base{
+ Offset: ev.GetOffset(),
+ Exchange: pos.Exchange,
+ Time: ev.GetTime(),
+ Interval: ev.GetInterval(),
+ CurrencyPair: pos.Pair,
+ UnderlyingPair: ev.GetUnderlyingPair(),
+ AssetType: pos.Asset,
+ Reasons: []string{"LIQUIDATED"},
+ },
+ Direction: direction,
+ Status: gctorder.Liquidated,
+ ClosePrice: ev.GetClosePrice(),
+ Amount: pos.Exposure,
+ AllocatedFunds: pos.Exposure,
+ OrderType: gctorder.Market,
+ LiquidatingPosition: true,
+ })
+ case item == asset.Spot:
+ allFunds := funds.GetAllFunding()
+ for i := range allFunds {
+ if allFunds[i].Asset.IsFutures() {
+ continue
+ }
+ if allFunds[i].Currency.IsFiatCurrency() || allFunds[i].Currency.IsStableCurrency() {
+ // close orders for assets
+ // funding manager will zero for fiat/stable
+ continue
+ }
+ closingOrders = append(closingOrders, &order.Order{
+ Base: &event.Base{
+ Offset: ev.GetOffset(),
+ Exchange: ev.GetExchange(),
+ Time: ev.GetTime(),
+ Interval: ev.GetInterval(),
+ CurrencyPair: pair,
+ AssetType: item,
+ Reasons: []string{"LIQUIDATED"},
+ },
+ Direction: gctorder.Sell,
+ Status: gctorder.Liquidated,
+ Amount: allFunds[i].Available,
+ OrderType: gctorder.Market,
+ AllocatedFunds: allFunds[i].Available,
+ LiquidatingPosition: true,
+ })
+ }
+ }
+ }
}
- return e.HoldingsSnapshots[len(e.HoldingsSnapshots)-1]
+ return closingOrders, nil
}
-// GetHoldingsForTime returns the holdings for a time period, or an empty holding if not found
-func (e *Settings) GetHoldingsForTime(t time.Time) holdings.Holding {
- if e.HoldingsSnapshots == nil {
- // no holdings yet
- return holdings.Holding{}
+func (p *Portfolio) getFuturesSettingsFromEvent(e common.EventHandler) (*Settings, error) {
+ if e == nil {
+ return nil, common.ErrNilEvent
}
- for i := len(e.HoldingsSnapshots) - 1; i >= 0; i-- {
- if e.HoldingsSnapshots[i].Timestamp.Equal(t) {
- return e.HoldingsSnapshots[i]
+ if !e.GetAssetType().IsFutures() {
+ return nil, gctorder.ErrNotFuturesAsset
+ }
+ settings, err := p.getSettings(e.GetExchange(), e.GetAssetType(), e.Pair())
+ if err != nil {
+ return nil, fmt.Errorf("%v %v %v %w", e.GetExchange(), e.GetAssetType(), e.Pair(), err)
+ }
+
+ if settings.FuturesTracker == nil {
+ return nil, fmt.Errorf("%w for %v %v %v", errUnsetFuturesTracker, e.GetExchange(), e.GetAssetType(), e.Pair())
+ }
+
+ return settings, nil
+}
+
+func (p *Portfolio) getSettings(exch string, item asset.Item, pair currency.Pair) (*Settings, error) {
+ exchMap, ok := p.exchangeAssetPairSettings[strings.ToLower(exch)]
+ if !ok {
+ return nil, errExchangeUnset
+ }
+ itemMap, ok := exchMap[item]
+ if !ok {
+ return nil, errAssetUnset
+ }
+ pairSettings, ok := itemMap[pair]
+ if !ok {
+ return nil, errCurrencyPairUnset
+ }
+
+ return pairSettings, nil
+}
+
+// GetLatestPNLs returns all PNL details in one array
+func (p *Portfolio) GetLatestPNLs() []PNLSummary {
+ var result []PNLSummary
+ for exch, assetPairSettings := range p.exchangeAssetPairSettings {
+ for ai, pairSettings := range assetPairSettings {
+ if !ai.IsFutures() {
+ continue
+ }
+ for cp, settings := range pairSettings {
+ if settings == nil {
+ continue
+ }
+ if settings.FuturesTracker == nil {
+ continue
+ }
+ summary := PNLSummary{
+ Exchange: exch,
+ Item: ai,
+ Pair: cp,
+ }
+ positions := settings.FuturesTracker.GetPositions()
+ if len(positions) > 0 {
+ pnlHistory := positions[len(positions)-1].PNLHistory
+ if len(pnlHistory) > 0 {
+ summary.Result = pnlHistory[len(pnlHistory)-1]
+ summary.CollateralCurrency = positions[0].CollateralCurrency
+ }
+ }
+
+ result = append(result, summary)
+ }
}
}
- return holdings.Holding{}
+ return result
+}
+
+// GetUnrealisedPNL returns a basic struct containing unrealised PNL
+func (p *PNLSummary) GetUnrealisedPNL() BasicPNLResult {
+ return BasicPNLResult{
+ Time: p.Result.Time,
+ PNL: p.Result.UnrealisedPNL,
+ Currency: p.CollateralCurrency,
+ }
+}
+
+// GetRealisedPNL returns a basic struct containing realised PNL
+func (p *PNLSummary) GetRealisedPNL() BasicPNLResult {
+ return BasicPNLResult{
+ Time: p.Result.Time,
+ PNL: p.Result.RealisedPNL,
+ Currency: p.CollateralCurrency,
+ }
+}
+
+// GetExposure returns the position exposure
+func (p *PNLSummary) GetExposure() decimal.Decimal {
+ return p.Result.Exposure
+}
+
+// GetCollateralCurrency returns the collateral currency
+func (p *PNLSummary) GetCollateralCurrency() currency.Code {
+ return p.CollateralCurrency
+}
+
+// GetDirection returns the direction
+func (p *PNLSummary) GetDirection() gctorder.Side {
+ return p.Result.Direction
+}
+
+// GetPositionStatus returns the position status
+func (p *PNLSummary) GetPositionStatus() gctorder.Status {
+ return p.Result.Status
}
diff --git a/backtester/eventhandlers/portfolio/portfolio_test.go b/backtester/eventhandlers/portfolio/portfolio_test.go
index 59fde85bdf5..7c8c3b8dc0b 100644
--- a/backtester/eventhandlers/portfolio/portfolio_test.go
+++ b/backtester/eventhandlers/portfolio/portfolio_test.go
@@ -2,6 +2,7 @@ package portfolio
import (
"errors"
+ "strings"
"testing"
"time"
@@ -18,13 +19,15 @@ import (
"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/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"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"
func TestReset(t *testing.T) {
t.Parallel()
@@ -66,27 +69,29 @@ func TestSetup(t *testing.T) {
func TestSetupCurrencySettingsMap(t *testing.T) {
t.Parallel()
p := &Portfolio{}
- _, err := p.SetupCurrencySettingsMap(nil)
+ err := p.SetupCurrencySettingsMap(nil)
if !errors.Is(err, errNoPortfolioSettings) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{})
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{})
if !errors.Is(err, errExchangeUnset) {
t.Errorf("received: %v, expected: %v", err, errExchangeUnset)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi"})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff})
if !errors.Is(err, errAssetUnset) {
t.Errorf("received: %v, expected: %v", err, errAssetUnset)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot})
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot})
if !errors.Is(err, errCurrencyPairUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyPairUnset)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
@@ -107,7 +112,9 @@ func TestSetHoldings(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
@@ -147,7 +154,9 @@ func TestGetLatestHoldingsForAllCurrencies(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
@@ -197,7 +206,7 @@ func TestViewHoldingAtTimePeriod(t *testing.T) {
p := Portfolio{}
tt := time.Now()
s := &signal.Signal{
- Base: event.Base{
+ Base: &event.Base{
Time: tt,
Exchange: testExchange,
AssetType: asset.Spot,
@@ -209,7 +218,9 @@ func TestViewHoldingAtTimePeriod(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, errNoHoldings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
@@ -254,21 +265,25 @@ func TestUpdate(t *testing.T) {
if !errors.Is(err, funding.ErrFundsNotFound) {
t.Errorf("received '%v' expected '%v'", err, funding.ErrFundsNotFound)
}
- b, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(1), decimal.Zero)
+ bc, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(1), decimal.Zero)
if err != nil {
t.Fatal(err)
}
- q, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(100), decimal.Zero)
+ qc, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(100), decimal.Zero)
if err != nil {
t.Fatal(err)
}
- pair, err := funding.CreatePair(b, q)
- if err != nil {
- t.Fatal(err)
+ pair, err := funding.CreatePair(bc, qc)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
}
- err = p.UpdateHoldings(&kline.Kline{}, pair)
- if !errors.Is(err, errNoPortfolioSettings) {
- t.Errorf("received '%v' expected '%v'", err, errNoPortfolioSettings)
+
+ b := &event.Base{}
+ err = p.UpdateHoldings(&kline.Kline{
+ Base: b,
+ }, pair)
+ if !errors.Is(err, errExchangeUnset) {
+ t.Errorf("received '%v' expected '%v'", err, errExchangeUnset)
}
tt := time.Now()
@@ -277,49 +292,30 @@ func TestUpdate(t *testing.T) {
Exchange: testExchange,
Asset: asset.Spot,
Pair: currency.NewPair(currency.BTC, currency.USD),
- Timestamp: tt}, false)
+ Timestamp: tt,
+ }, false)
if !errors.Is(err, errNoPortfolioSettings) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
-
+ b.Time = tt
+ b.Exchange = testExchange
+ b.CurrencyPair = currency.NewPair(currency.BTC, currency.USD)
+ b.AssetType = asset.Spot
err = p.UpdateHoldings(&kline.Kline{
- Base: event.Base{
- Time: tt,
- Exchange: testExchange,
- CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
- AssetType: asset.Spot,
- },
+ Base: b,
}, pair)
if err != nil {
t.Error(err)
}
}
-func TestGetFee(t *testing.T) {
- t.Parallel()
- p := Portfolio{}
- f := p.GetFee("", asset.Empty, currency.EMPTYPAIR)
- if !f.IsZero() {
- t.Error("expected 0")
- }
-
- _, err := p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
- if err != nil {
- t.Error(err)
- }
-
- p.SetFee("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD), decimal.NewFromInt(1337))
- f = p.GetFee("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
- if !f.Equal(decimal.NewFromInt(1337)) {
- t.Errorf("expected %v received %v", 1337, f)
- }
-}
-
func TestGetComplianceManager(t *testing.T) {
t.Parallel()
p := Portfolio{}
@@ -328,12 +324,14 @@ func TestGetComplianceManager(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
var cm *compliance.Manager
- cm, err = p.GetComplianceManager("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
+ cm, err = p.GetComplianceManager(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USD))
if err != nil {
t.Error(err)
}
@@ -350,24 +348,28 @@ func TestAddComplianceSnapshot(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
- err = p.addComplianceSnapshot(&fill.Fill{})
+ err = p.addComplianceSnapshot(&fill.Fill{
+ Base: &event.Base{},
+ })
if !errors.Is(err, errNoPortfolioSettings) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
err = p.addComplianceSnapshot(&fill.Fill{
- Base: event.Base{
- Exchange: "hi",
+ Base: &event.Base{
+ Exchange: testExchange,
CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
AssetType: asset.Spot,
},
Order: &gctorder.Detail{
- Exchange: "hi",
+ Exchange: testExchange,
Pair: currency.NewPair(currency.BTC, currency.USD),
AssetType: asset.Spot,
},
@@ -386,13 +388,13 @@ func TestOnFill(t *testing.T) {
}
f := &fill.Fill{
- Base: event.Base{
- Exchange: "hi",
+ Base: &event.Base{
+ Exchange: testExchange,
CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
AssetType: asset.Spot,
},
Order: &gctorder.Detail{
- Exchange: "hi",
+ Exchange: testExchange,
Pair: currency.NewPair(currency.BTC, currency.USD),
AssetType: asset.Spot,
},
@@ -401,7 +403,9 @@ func TestOnFill(t *testing.T) {
if !errors.Is(err, errNoPortfolioSettings) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
@@ -437,8 +441,10 @@ func TestOnSignal(t *testing.T) {
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
-
- s := &signal.Signal{}
+ b := &event.Base{}
+ s := &signal.Signal{
+ Base: b,
+ }
_, err = p.OnSignal(s, &exchange.Settings{}, nil)
if !errors.Is(err, errSizeManagerUnset) {
t.Errorf("received: %v, expected: %v", err, errSizeManagerUnset)
@@ -456,15 +462,15 @@ func TestOnSignal(t *testing.T) {
if !errors.Is(err, funding.ErrFundsNotFound) {
t.Errorf("received: %v, expected: %v", err, funding.ErrFundsNotFound)
}
- b, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(1337), decimal.Zero)
+ bc, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(1337), decimal.Zero)
if err != nil {
t.Fatal(err)
}
- q, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(1337), decimal.Zero)
+ qc, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(1337), decimal.Zero)
if err != nil {
t.Fatal(err)
}
- pair, err := funding.CreatePair(b, q)
+ pair, err := funding.CreatePair(bc, qc)
if err != nil {
t.Fatal(err)
}
@@ -478,16 +484,17 @@ func TestOnSignal(t *testing.T) {
if !errors.Is(err, errNoPortfolioSettings) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
+ b.Exchange = testExchange
+ b.CurrencyPair = currency.NewPair(currency.BTC, currency.USD)
+ b.AssetType = asset.Spot
s = &signal.Signal{
- Base: event.Base{
- Exchange: "hi",
- CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
- AssetType: asset.Spot,
- },
+ Base: b,
Direction: gctorder.Buy,
}
var resp *order.Order
@@ -495,7 +502,7 @@ func TestOnSignal(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- if resp.Reason == "" {
+ if len(resp.Reasons) != 2 {
t.Error("expected issue")
}
@@ -504,7 +511,7 @@ func TestOnSignal(t *testing.T) {
if err != nil {
t.Error(err)
}
- if resp.Reason == "" {
+ if len(resp.Reasons) != 4 {
t.Error("expected issue")
}
@@ -516,7 +523,7 @@ func TestOnSignal(t *testing.T) {
s.Direction = gctorder.Buy
err = p.setHoldingsForOffset(&holdings.Holding{
- Exchange: testExchange,
+ Exchange: "lol",
Asset: asset.Spot,
Pair: currency.NewPair(currency.BTC, currency.USD),
Timestamp: time.Now(),
@@ -525,7 +532,7 @@ func TestOnSignal(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
- _, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
if err != nil {
t.Error(err)
}
@@ -539,6 +546,7 @@ func TestOnSignal(t *testing.T) {
s.ClosePrice = decimal.NewFromInt(10)
s.Direction = gctorder.Buy
+ s.Amount = decimal.NewFromInt(1)
resp, err = p.OnSignal(s, &exchange.Settings{}, pair)
if err != nil {
t.Error(err)
@@ -567,37 +575,47 @@ func TestGetLatestHoldings(t *testing.T) {
func TestGetSnapshotAtTime(t *testing.T) {
t.Parallel()
p := Portfolio{}
- _, err := p.GetLatestOrderSnapshotForEvent(&kline.Kline{})
+ b := &event.Base{}
+ _, err := p.GetLatestOrderSnapshotForEvent(&kline.Kline{
+ Base: b,
+ })
if !errors.Is(err, errNoPortfolioSettings) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
cp := currency.NewPair(currency.XRP, currency.DOGE)
- s, err := p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "exch", Asset: asset.Spot, Pair: currency.NewPair(currency.XRP, currency.DOGE)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: cp})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
tt := time.Now()
- err = s.ComplianceManager.AddSnapshot([]compliance.SnapshotOrder{
- {
- Detail: &gctorder.Detail{
- Exchange: "exch",
- AssetType: asset.Spot,
- Pair: cp,
- Amount: 1337,
+ s, ok := p.exchangeAssetPairSettings[testExchange][asset.Spot][cp]
+ if !ok {
+ t.Fatal("couldn't get settings")
+ }
+ err = s.ComplianceManager.AddSnapshot(&compliance.Snapshot{
+ Orders: []compliance.SnapshotOrder{
+ {
+ Order: &gctorder.Detail{
+ Exchange: testExchange,
+ AssetType: asset.Spot,
+ Pair: cp,
+ Amount: 1337,
+ },
},
},
- }, tt, 0, false)
+ }, false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
+ b.Exchange = testExchange
+ b.Time = tt
+ b.Interval = gctkline.OneDay
+ b.CurrencyPair = cp
+ b.AssetType = asset.Spot
e := &kline.Kline{
- Base: event.Base{
- Exchange: "exch",
- Time: tt,
- Interval: gctkline.OneDay,
- CurrencyPair: cp,
- AssetType: asset.Spot,
- },
+ Base: b,
}
ss, err := p.GetLatestOrderSnapshotForEvent(e)
@@ -607,7 +625,7 @@ func TestGetSnapshotAtTime(t *testing.T) {
if len(ss.Orders) != 1 {
t.Fatal("expected 1")
}
- if ss.Orders[0].Amount != 1337 {
+ if ss.Orders[0].Order.Amount != 1337 {
t.Error("expected 1")
}
}
@@ -620,52 +638,853 @@ func TestGetLatestSnapshot(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
}
cp := currency.NewPair(currency.XRP, currency.DOGE)
- s, err := p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "exch", Asset: asset.Spot, Pair: currency.NewPair(currency.XRP, currency.DOGE)})
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.XRP, currency.DOGE)})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
- tt := time.Now()
- err = s.ComplianceManager.AddSnapshot([]compliance.SnapshotOrder{
- {
- Detail: &gctorder.Detail{
- Exchange: "exch",
- AssetType: asset.Spot,
- Pair: cp,
- Amount: 1337,
+ s, ok := p.exchangeAssetPairSettings[testExchange][asset.Spot][cp]
+ if !ok {
+ t.Fatal("couldn't get settings")
+ }
+ err = s.ComplianceManager.AddSnapshot(&compliance.Snapshot{
+ Orders: []compliance.SnapshotOrder{
+ {
+ Order: &gctorder.Detail{
+ Exchange: testExchange,
+ AssetType: asset.Spot,
+ Pair: cp,
+ Amount: 1337,
+ },
},
},
- }, tt, 0, false)
+ }, false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
- ss, err := p.GetLatestOrderSnapshots()
+ _, err = p.GetLatestOrderSnapshots()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
- err = s.ComplianceManager.AddSnapshot([]compliance.SnapshotOrder{
- ss[0].Orders[0],
- {
- Detail: &gctorder.Detail{
- Exchange: "exch",
- AssetType: asset.Spot,
- Pair: cp,
- Amount: 1338,
+ err = s.ComplianceManager.AddSnapshot(&compliance.Snapshot{
+ Orders: []compliance.SnapshotOrder{
+ {
+ Order: &gctorder.Detail{
+ Exchange: testExchange,
+ AssetType: asset.Spot,
+ Pair: cp,
+ Amount: 1337,
+ },
},
},
- }, tt, 1, false)
+ }, false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
- ss, err = p.GetLatestOrderSnapshots()
+ ss, err := p.GetLatestOrderSnapshots()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(ss) != 1 {
t.Fatal("expected 1")
}
- if len(ss[0].Orders) != 2 {
- t.Error("expected 2")
+ if len(ss[0].Orders) != 1 {
+ t.Errorf("expected 1, received %v", len(ss[0].Orders))
+ }
+}
+
+func TestCalculatePNL(t *testing.T) {
+ p := &Portfolio{}
+ ev := &kline.Kline{
+ Base: &event.Base{},
+ }
+ err := p.UpdatePNL(ev, decimal.Zero)
+ if !errors.Is(err, gctorder.ErrNotFuturesAsset) {
+ t.Errorf("received: %v, expected: %v", err, gctorder.ErrNotFuturesAsset)
+ }
+
+ exch := &ftx.FTX{}
+ exch.Name = testExchange
+ a := asset.Futures
+ pair, err := currency.NewPairFromStrings("BTC", "1231")
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{
+ Exchange: exch,
+ UseRealOrders: false,
+ Pair: pair,
+ Asset: a,
+ })
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ tt := time.Now().Add(time.Hour)
+ tt0 := time.Now().Add(-time.Hour)
+ ev.Exchange = exch.Name
+ ev.AssetType = a
+ ev.CurrencyPair = pair
+ ev.Time = tt0
+
+ err = p.UpdatePNL(ev, decimal.Zero)
+ if !errors.Is(err, gctorder.ErrPositionsNotLoadedForPair) {
+ t.Errorf("received: %v, expected: %v", err, gctorder.ErrPositionsNotLoadedForPair)
+ }
+
+ od := &gctorder.Detail{
+ Price: 1336,
+ Amount: 20,
+ Exchange: exch.Name,
+ Side: gctorder.Short,
+ AssetType: a,
+ Date: tt0,
+ Pair: pair,
+ OrderID: "lol",
+ }
+
+ s, ok := p.exchangeAssetPairSettings[strings.ToLower(exch.Name)][a][pair]
+ if !ok {
+ t.Fatal("couldn't get settings")
+ }
+ ev.Close = decimal.NewFromInt(1337)
+ err = s.ComplianceManager.AddSnapshot(&compliance.Snapshot{
+ Offset: 0,
+ Timestamp: tt0,
+ Orders: []compliance.SnapshotOrder{
+ {
+ Order: od,
+ },
+ },
+ }, false)
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ odCp := od.Copy()
+ odCp.Price = od.Price - 1
+ odCp.Side = gctorder.Long
+ err = s.ComplianceManager.AddSnapshot(&compliance.Snapshot{
+ Offset: 1,
+ Timestamp: tt,
+ Orders: []compliance.SnapshotOrder{
+ {
+ Order: od,
+ },
+ {
+ Order: &odCp,
+ },
+ },
+ }, false)
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+
+ err = s.FuturesTracker.TrackNewOrder(od)
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ err = p.UpdatePNL(ev, decimal.NewFromInt(1))
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+
+ pos := s.FuturesTracker.GetPositions()
+ if len(pos) != 1 {
+ t.Fatalf("expected one position, received '%v'", len(pos))
+ }
+ if len(pos[0].PNLHistory) == 0 {
+ t.Fatal("expected a pnl entry ( ͡° ͜ʖ ͡°)")
+ }
+ if !pos[0].UnrealisedPNL.Equal(decimal.NewFromInt(26700)) {
+ // 20 orders * $1 difference * 1x leverage
+ t.Errorf("expected 26700, received '%v'", pos[0].UnrealisedPNL)
+ }
+}
+
+func TestTrackFuturesOrder(t *testing.T) {
+ t.Parallel()
+ p := &Portfolio{}
+ var expectedError = common.ErrNilEvent
+ _, err := p.TrackFuturesOrder(nil, nil)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+ expectedError = common.ErrNilArguments
+ _, err = p.TrackFuturesOrder(&fill.Fill{}, nil)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+ fundPair := &funding.SpotPair{}
+ expectedError = gctorder.ErrSubmissionIsNil
+ _, err = p.TrackFuturesOrder(&fill.Fill{}, fundPair)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+
+ expectedError = gctorder.ErrNotFuturesAsset
+ od := &gctorder.Detail{}
+ _, err = p.TrackFuturesOrder(&fill.Fill{
+ Order: od,
+ }, fundPair)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+
+ od.AssetType = asset.Futures
+ expectedError = funding.ErrNotCollateral
+ _, err = p.TrackFuturesOrder(&fill.Fill{
+ Order: od,
+ }, fundPair)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+
+ expectedError = nil
+ contract, err := funding.CreateItem(od.Exchange, od.AssetType, od.Pair.Base, decimal.NewFromInt(100), decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+ collateral, err := funding.CreateItem(od.Exchange, od.AssetType, od.Pair.Quote, decimal.NewFromInt(100), decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+ collat, err := funding.CreateCollateral(contract, collateral)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+ expectedError = errExchangeUnset
+ _, err = p.TrackFuturesOrder(&fill.Fill{
+ Order: od,
+ }, collat)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+
+ cp := currency.NewPair(currency.XRP, currency.DOGE)
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Futures, Pair: cp})
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ od.Pair = cp
+ od.Exchange = testExchange
+ od.Side = gctorder.Short
+ od.AssetType = asset.Futures
+ od.Amount = 1337
+ od.Price = 1337
+ od.OrderID = testExchange
+ od.Date = time.Now()
+ expectedError = nil
+
+ _, err = p.TrackFuturesOrder(&fill.Fill{
+ Order: od,
+ Base: &event.Base{
+ Exchange: testExchange,
+ AssetType: asset.Futures,
+ CurrencyPair: cp,
+ },
+ }, collat)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+}
+
+func TestGetHoldingsForTime(t *testing.T) {
+ t.Parallel()
+ s := &Settings{}
+ h := s.GetHoldingsForTime(time.Now())
+ if !h.Timestamp.IsZero() {
+ t.Error("expected unset holdings")
+ }
+ tt := time.Now()
+ s.HoldingsSnapshots = append(s.HoldingsSnapshots, holdings.Holding{
+ Timestamp: tt,
+ Offset: 1337,
+ })
+ h = s.GetHoldingsForTime(time.Unix(1337, 0))
+ if !h.Timestamp.IsZero() {
+ t.Error("expected unset holdings")
+ }
+
+ h = s.GetHoldingsForTime(tt)
+ if h.Timestamp.IsZero() && h.Offset != 1337 {
+ t.Error("expected set holdings")
+ }
+}
+
+func TestGetPositions(t *testing.T) {
+ t.Parallel()
+ p := &Portfolio{}
+ var expectedError = common.ErrNilEvent
+ _, err := p.GetPositions(nil)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ ev := &fill.Fill{
+ Base: &event.Base{
+ Exchange: testExchange,
+ CurrencyPair: currency.NewPair(currency.BTC, currency.USDT),
+ AssetType: asset.Futures,
+ },
+ }
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: ev.AssetType, Pair: ev.Pair()})
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ expectedError = nil
+ _, err = p.GetPositions(ev)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestGetLatestPNLForEvent(t *testing.T) {
+ t.Parallel()
+ p := &Portfolio{}
+ var expectedError = common.ErrNilEvent
+ _, err := p.GetLatestPNLForEvent(nil)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ ev := &fill.Fill{
+ Base: &event.Base{
+ Exchange: testExchange,
+ CurrencyPair: currency.NewPair(currency.BTC, currency.USDT),
+ AssetType: asset.Futures,
+ },
+ }
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: ev.AssetType, Pair: ev.Pair()})
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ expectedError = gctorder.ErrPositionsNotLoadedForPair
+ _, err = p.GetLatestPNLForEvent(ev)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ settings, ok := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
+ if !ok {
+ t.Fatalf("where did settings go?")
+ }
+ expectedError = nil
+ err = settings.FuturesTracker.TrackNewOrder(&gctorder.Detail{
+ Exchange: ev.GetExchange(),
+ AssetType: ev.AssetType,
+ Pair: ev.Pair(),
+ Amount: 1,
+ Price: 1,
+ OrderID: "one",
+ Date: time.Now(),
+ Side: gctorder.Buy,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ latest, err := p.GetLatestPNLForEvent(ev)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ if latest == nil {
+ t.Error("unexpected")
+ }
+}
+
+func TestGetFuturesSettingsFromEvent(t *testing.T) {
+ t.Parallel()
+ p := &Portfolio{}
+ var expectedError = common.ErrNilEvent
+ _, err := p.getFuturesSettingsFromEvent(nil)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ expectedError = gctorder.ErrNotFuturesAsset
+ b := &event.Base{}
+
+ _, err = p.getFuturesSettingsFromEvent(&fill.Fill{
+ Base: b,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ b.Exchange = testExchange
+ b.CurrencyPair = currency.NewPair(currency.BTC, currency.USDT)
+ b.AssetType = asset.Futures
+ ev := &fill.Fill{
+ Base: b,
+ }
+ expectedError = errExchangeUnset
+ _, err = p.getFuturesSettingsFromEvent(ev)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: ev.AssetType, Pair: ev.Pair()})
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ expectedError = nil
+ settings, err := p.getFuturesSettingsFromEvent(ev)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ expectedError = errUnsetFuturesTracker
+ settings.FuturesTracker = nil
+ _, err = p.getFuturesSettingsFromEvent(ev)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestGetLatestPNLs(t *testing.T) {
+ t.Parallel()
+ p := &Portfolio{}
+ latest := p.GetLatestPNLs()
+ if len(latest) != 0 {
+ t.Error("expected empty")
+ }
+ ev := &fill.Fill{
+ Base: &event.Base{
+ Exchange: testExchange,
+ CurrencyPair: currency.NewPair(currency.BTC, currency.USDT),
+ AssetType: asset.Futures,
+ },
+ }
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ err := p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: ev.AssetType, Pair: ev.Pair()})
+ if !errors.Is(err, nil) {
+ t.Errorf("received: %v, expected: %v", err, nil)
+ }
+ settings, ok := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
+ if !ok {
+ t.Fatalf("where did settings go?")
+ }
+ err = settings.FuturesTracker.TrackNewOrder(&gctorder.Detail{
+ Exchange: ev.GetExchange(),
+ AssetType: ev.AssetType,
+ Pair: ev.Pair(),
+ Amount: 1,
+ Price: 1,
+ OrderID: "one",
+ Date: time.Now(),
+ Side: gctorder.Buy,
+ })
+ if !errors.Is(err, nil) {
+ t.Fatalf("received '%v' expected '%v'", err, nil)
+ }
+ latest = p.GetLatestPNLs()
+ if len(latest) != 1 {
+ t.Error("expected 1")
+ }
+}
+
+func TestGetUnrealisedPNL(t *testing.T) {
+ t.Parallel()
+ p := PNLSummary{
+ Exchange: testExchange,
+ Item: asset.Futures,
+ Pair: currency.NewPair(currency.BTC, currency.USDT),
+ CollateralCurrency: currency.USD,
+ Offset: 1,
+ Result: gctorder.PNLResult{
+ Time: time.Now(),
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1338),
+ RealisedPNL: decimal.NewFromInt(1339),
+ Price: decimal.NewFromInt(1331),
+ Exposure: decimal.NewFromInt(1332),
+ Direction: gctorder.Short,
+ Fee: decimal.NewFromInt(1333),
+ IsLiquidated: true,
+ },
+ }
+ result := p.GetUnrealisedPNL()
+ if !result.PNL.Equal(p.Result.UnrealisedPNL) {
+ t.Errorf("received '%v' expected '%v'", result.PNL, p.Result.UnrealisedPNL)
+ }
+ if !result.Time.Equal(p.Result.Time) {
+ t.Errorf("received '%v' expected '%v'", result.Time, p.Result.Time)
+ }
+ if !result.Currency.Equal(p.CollateralCurrency) {
+ t.Errorf("received '%v' expected '%v'", result.Currency, p.CollateralCurrency)
+ }
+}
+
+func TestGetRealisedPNL(t *testing.T) {
+ t.Parallel()
+ p := PNLSummary{
+ Exchange: testExchange,
+ Item: asset.Futures,
+ Pair: currency.NewPair(currency.BTC, currency.USDT),
+ CollateralCurrency: currency.USD,
+ Offset: 1,
+ Result: gctorder.PNLResult{
+ Time: time.Now(),
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1338),
+ RealisedPNL: decimal.NewFromInt(1339),
+ Price: decimal.NewFromInt(1331),
+ Exposure: decimal.NewFromInt(1332),
+ Direction: gctorder.Short,
+ Fee: decimal.NewFromInt(1333),
+ IsLiquidated: true,
+ },
+ }
+ result := p.GetRealisedPNL()
+ if !result.PNL.Equal(p.Result.RealisedPNL) {
+ t.Errorf("received '%v' expected '%v'", result.PNL, p.Result.RealisedPNL)
+ }
+ if !result.Time.Equal(p.Result.Time) {
+ t.Errorf("received '%v' expected '%v'", result.Time, p.Result.Time)
+ }
+ if !result.Currency.Equal(p.CollateralCurrency) {
+ t.Errorf("received '%v' expected '%v'", result.Currency, p.CollateralCurrency)
+ }
+}
+
+func TestGetExposure(t *testing.T) {
+ t.Parallel()
+ p := PNLSummary{
+ Exchange: testExchange,
+ Item: asset.Futures,
+ Pair: currency.NewPair(currency.BTC, currency.USDT),
+ CollateralCurrency: currency.USD,
+ Offset: 1,
+ Result: gctorder.PNLResult{
+ Time: time.Now(),
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1338),
+ RealisedPNL: decimal.NewFromInt(1339),
+ Price: decimal.NewFromInt(1331),
+ Exposure: decimal.NewFromInt(1332),
+ Direction: gctorder.Short,
+ Fee: decimal.NewFromInt(1333),
+ IsLiquidated: true,
+ },
+ }
+ if !p.GetExposure().Equal(p.Result.Exposure) {
+ t.Errorf("received '%v' expected '%v'", p.GetExposure(), p.Result.Exposure)
+ }
+}
+
+func TestGetCollateralCurrency(t *testing.T) {
+ t.Parallel()
+ p := PNLSummary{
+ Exchange: testExchange,
+ Item: asset.Futures,
+ Pair: currency.NewPair(currency.BTC, currency.USDT),
+ CollateralCurrency: currency.USD,
+ Offset: 1,
+ Result: gctorder.PNLResult{
+ Time: time.Now(),
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1338),
+ RealisedPNL: decimal.NewFromInt(1339),
+ Price: decimal.NewFromInt(1331),
+ Exposure: decimal.NewFromInt(1332),
+ Direction: gctorder.Short,
+ Fee: decimal.NewFromInt(1333),
+ IsLiquidated: true,
+ },
+ }
+ result := p.GetCollateralCurrency()
+ if !result.Equal(p.CollateralCurrency) {
+ t.Errorf("received '%v' expected '%v'", result, p.CollateralCurrency)
+ }
+}
+
+func TestGetDirection(t *testing.T) {
+ t.Parallel()
+ p := PNLSummary{
+ Exchange: testExchange,
+ Item: asset.Futures,
+ Pair: currency.NewPair(currency.BTC, currency.USDT),
+ CollateralCurrency: currency.USD,
+ Offset: 1,
+ Result: gctorder.PNLResult{
+ Time: time.Now(),
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1338),
+ RealisedPNL: decimal.NewFromInt(1339),
+ Price: decimal.NewFromInt(1331),
+ Exposure: decimal.NewFromInt(1332),
+ Direction: gctorder.Short,
+ Fee: decimal.NewFromInt(1333),
+ IsLiquidated: true,
+ },
+ }
+ if p.GetDirection() != (p.Result.Direction) {
+ t.Errorf("received '%v' expected '%v'", p.GetDirection(), p.Result.Direction)
+ }
+}
+
+func TestCannotPurchase(t *testing.T) {
+ t.Parallel()
+ var expectedError = common.ErrNilEvent
+ _, err := cannotPurchase(nil, nil)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ s := &signal.Signal{
+ Base: &event.Base{},
+ }
+ expectedError = common.ErrNilArguments
+ _, err = cannotPurchase(s, nil)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ o := &order.Order{
+ Base: &event.Base{},
+ }
+ s.Direction = gctorder.Buy
+ expectedError = nil
+ result, err := cannotPurchase(s, o)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ if result.Direction != gctorder.CouldNotBuy {
+ t.Errorf("received '%v' expected '%v'", result.Direction, gctorder.CouldNotBuy)
+ }
+
+ s.Direction = gctorder.Sell
+ expectedError = nil
+ result, err = cannotPurchase(s, o)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ if result.Direction != gctorder.CouldNotSell {
+ t.Errorf("received '%v' expected '%v'", result.Direction, gctorder.CouldNotSell)
+ }
+
+ s.Direction = gctorder.Short
+ expectedError = nil
+ result, err = cannotPurchase(s, o)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ if result.Direction != gctorder.CouldNotShort {
+ t.Errorf("received '%v' expected '%v'", result.Direction, gctorder.CouldNotShort)
+ }
+
+ s.Direction = gctorder.Long
+ expectedError = nil
+ result, err = cannotPurchase(s, o)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ if result.Direction != gctorder.CouldNotLong {
+ t.Errorf("received '%v' expected '%v'", result.Direction, gctorder.CouldNotLong)
+ }
+
+ s.Direction = gctorder.UnknownSide
+ expectedError = nil
+ result, err = cannotPurchase(s, o)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ if result.Direction != gctorder.DoNothing {
+ t.Errorf("received '%v' expected '%v'", result.Direction, gctorder.DoNothing)
+ }
+}
+
+func TestCreateLiquidationOrdersForExchange(t *testing.T) {
+ t.Parallel()
+
+ p := &Portfolio{}
+ var expectedError = common.ErrNilEvent
+ _, err := p.CreateLiquidationOrdersForExchange(nil, nil)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ b := &event.Base{}
+
+ ev := &kline.Kline{
+ Base: b,
+ }
+ expectedError = common.ErrNilArguments
+ _, err = p.CreateLiquidationOrdersForExchange(ev, nil)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ funds := &funding.FundManager{}
+ expectedError = config.ErrExchangeNotFound
+ _, err = p.CreateLiquidationOrdersForExchange(ev, funds)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ ff := &ftx.FTX{}
+ ff.Name = testExchange
+ cp := currency.NewPair(currency.BTC, currency.USD)
+ expectedError = nil
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Futures, Pair: cp})
+ if err != nil {
+ t.Error(err)
+ }
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: cp})
+ if err != nil {
+ t.Error(err)
+ }
+ ev.Exchange = testExchange
+ _, err = p.CreateLiquidationOrdersForExchange(ev, funds)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ settings, err := p.getSettings(ff.Name, asset.Futures, cp)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ err = settings.FuturesTracker.TrackNewOrder(&gctorder.Detail{
+ Exchange: ff.Name,
+ AssetType: asset.Futures,
+ Pair: cp,
+ Side: gctorder.Long,
+ OrderID: "lol",
+ Date: time.Now(),
+ Amount: 1337,
+ Price: 1337,
+ })
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ ev.Exchange = ff.Name
+ ev.AssetType = asset.Futures
+ ev.CurrencyPair = cp
+ _, err = p.CreateLiquidationOrdersForExchange(ev, funds)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+
+ // spot order
+ item, err := funding.CreateItem(ff.Name, asset.Spot, currency.BTC, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ err = funds.AddItem(item)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ err = item.IncreaseAvailable(decimal.NewFromInt(1337))
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ orders, err := p.CreateLiquidationOrdersForExchange(ev, funds)
+ if !errors.Is(err, expectedError) {
+ t.Fatalf("received '%v' expected '%v'", err, expectedError)
+ }
+ if len(orders) != 2 {
+ t.Errorf("expected two orders generated, received '%v'", len(orders))
+ }
+}
+
+func TestGetPositionStatus(t *testing.T) {
+ t.Parallel()
+ p := PNLSummary{
+ Result: gctorder.PNLResult{
+ Status: gctorder.Rejected,
+ },
+ }
+ status := p.GetPositionStatus()
+ if gctorder.Rejected != status {
+ t.Errorf("expected '%v' received '%v'", gctorder.Rejected, status)
+ }
+}
+
+func TestCheckLiquidationStatus(t *testing.T) {
+ t.Parallel()
+ p := &Portfolio{}
+ var expectedError = common.ErrNilEvent
+ err := p.CheckLiquidationStatus(nil, nil, nil)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
+ }
+
+ ev := &kline.Kline{
+ Base: &event.Base{},
+ }
+ expectedError = common.ErrNilArguments
+ err = p.CheckLiquidationStatus(ev, nil, nil)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
+ }
+
+ item := asset.Futures
+ pair := currency.NewPair(currency.BTC, currency.USD)
+ expectedError = nil
+ contract, err := funding.CreateItem(testExchange, item, pair.Base, decimal.NewFromInt(100), decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+ collateral, err := funding.CreateItem(testExchange, item, pair.Quote, decimal.NewFromInt(100), decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+ collat, err := funding.CreateCollateral(contract, collateral)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v' expected '%v", err, expectedError)
+ }
+
+ expectedError = common.ErrNilArguments
+ err = p.CheckLiquidationStatus(ev, collat, nil)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
+ }
+
+ pnl := &PNLSummary{}
+ expectedError = gctorder.ErrNotFuturesAsset
+ err = p.CheckLiquidationStatus(ev, collat, pnl)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
+ }
+
+ pnl.Item = asset.Futures
+ ev.AssetType = asset.Futures
+ ev.Exchange = "ftx"
+ ev.CurrencyPair = pair
+ exch := &ftx.FTX{}
+ exch.Name = testExchange
+ expectedError = nil
+ err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: exch, Asset: asset.Futures, Pair: pair})
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
+ }
+ settings, err := p.getSettings(testExchange, ev.AssetType, ev.Pair())
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
+ }
+ od := &gctorder.Detail{
+ Price: 1336,
+ Amount: 20,
+ Exchange: exch.Name,
+ Side: gctorder.Short,
+ AssetType: ev.AssetType,
+ Date: time.Now(),
+ Pair: pair,
+ OrderID: "lol",
+ }
+ err = settings.FuturesTracker.TrackNewOrder(od)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
+ }
+ err = p.CheckLiquidationStatus(ev, collat, pnl)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("received '%v', expected '%v'", err, expectedError)
}
}
diff --git a/backtester/eventhandlers/portfolio/portfolio_types.go b/backtester/eventhandlers/portfolio/portfolio_types.go
index c7ab69054e4..64973407f3a 100644
--- a/backtester/eventhandlers/portfolio/portfolio_types.go
+++ b/backtester/eventhandlers/portfolio/portfolio_types.go
@@ -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
}
diff --git a/backtester/eventhandlers/portfolio/risk/risk.go b/backtester/eventhandlers/portfolio/risk/risk.go
index d66e799c641..36382035c53 100644
--- a/backtester/eventhandlers/portfolio/risk/risk.go
+++ b/backtester/eventhandlers/portfolio/risk/risk.go
@@ -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))
}
}
diff --git a/backtester/eventhandlers/portfolio/risk/risk_test.go b/backtester/eventhandlers/portfolio/risk/risk_test.go
index 92c585dcc9c..3136fb4b2d5 100644
--- a/backtester/eventhandlers/portfolio/risk/risk_test.go
+++ b/backtester/eventhandlers/portfolio/risk/risk_test.go
@@ -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,
},
},
diff --git a/backtester/eventhandlers/portfolio/setup.go b/backtester/eventhandlers/portfolio/setup.go
new file mode 100644
index 00000000000..50e4ab97072
--- /dev/null
+++ b/backtester/eventhandlers/portfolio/setup.go
@@ -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
+}
diff --git a/backtester/eventhandlers/portfolio/size/size.go b/backtester/eventhandlers/portfolio/size/size.go
index 75f75a46559..092b3be2dbd 100644
--- a/backtester/eventhandlers/portfolio/size/size.go
+++ b/backtester/eventhandlers/portfolio/size/size.go
@@ -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:
+
+ 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, decimal.Decimal{}, err
+ }
+ 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, decimal.Decimal{}, err
+ }
+ 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, estFee, err := s.calculateAmount(retOrder.Direction, retOrder.ClosePrice, amountAvailable, cs, o)
+ if err != nil {
+ return nil, decimal.Decimal{}, err
+ }
+ retOrder.SetAmount(amount)
+
+ 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, err = s.calculateBuySize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetBuyLimit(), cs.BuySide)
+ amount, fee, err = s.calculateBuySize(price, amountAvailable, cs.TakerFee, o.GetBuyLimit(), cs.BuySide)
if err != nil {
- return nil, err
+ return decimal.Decimal{}, decimal.Decimal{}, err
}
// check size against portfolio specific settings
- var portfolioSize decimal.Decimal
- portfolioSize, err = s.calculateBuySize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetBuyLimit(), s.BuySide)
+ portfolioAmount, portfolioFee, err = s.calculateBuySize(price, amountAvailable, cs.TakerFee, o.GetBuyLimit(), s.BuySide)
if err != nil {
- return nil, err
+ return decimal.Decimal{}, decimal.Decimal{}, err
}
// global settings overrule individual currency settings
- if amount.GreaterThan(portfolioSize) {
- amount = portfolioSize
+ if amount.GreaterThan(portfolioAmount) {
+ amount = portfolioAmount
+ fee = portfolioFee
}
-
- case gctorder.Sell:
+ case gctorder.Sell, gctorder.Short:
// check size against currency specific settings
- amount, err = s.calculateSellSize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetSellLimit(), cs.SellSide)
+ amount, fee, err = s.calculateSellSize(price, amountAvailable, cs.TakerFee, o.GetSellLimit(), cs.SellSide)
if err != nil {
- return nil, err
+ return decimal.Decimal{}, decimal.Decimal{}, err
}
// check size against portfolio specific settings
- portfolioSize, err := s.calculateSellSize(retOrder.Price, amountAvailable, cs.ExchangeFee, o.GetSellLimit(), s.SellSide)
+ portfolioAmount, portfolioFee, err = s.calculateSellSize(price, amountAvailable, cs.TakerFee, o.GetSellLimit(), s.SellSide)
if err != nil {
- return nil, err
+ return decimal.Decimal{}, decimal.Decimal{}, err
}
// global settings overrule individual currency settings
- if amount.GreaterThan(portfolioSize) {
- amount = portfolioSize
+ 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())
}
- 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())
+ 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())
}
- retOrder.SetAmount(amount)
- return retOrder, nil
+ 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
}
diff --git a/backtester/eventhandlers/portfolio/size/size_test.go b/backtester/eventhandlers/portfolio/size/size_test.go
index 757fe420060..c41f616f0ad 100644
--- a/backtester/eventhandlers/portfolio/size/size_test.go
+++ b/backtester/eventhandlers/portfolio/size/size_test.go
@@ -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
- o.Price = decimal.NewFromInt(1)
+ _, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received: %v, expected: %v", err, errCannotAllocate)
+ }
+
+ 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)
+ }
}
diff --git a/backtester/eventhandlers/statistics/common.go b/backtester/eventhandlers/statistics/common.go
new file mode 100644
index 00000000000..28208b3df0f
--- /dev/null
+++ b/backtester/eventhandlers/statistics/common.go
@@ -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
+}
diff --git a/backtester/eventhandlers/statistics/currencystatistics.go b/backtester/eventhandlers/statistics/currencystatistics.go
index 9473f409bdf..45b46d95db4 100644
--- a/backtester/eventhandlers/statistics/currencystatistics.go
+++ b/backtester/eventhandlers/statistics/currencystatistics.go
@@ -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")
- }
- 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())
- }
- }
-}
-
-// 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 {
- log.Error(log.BackTester, err)
- continue
+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
}
- 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() {
- 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
- }
- }
-}
-
-// 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
+ 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
}
- 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)),
- })
+ default:
+ return fmt.Errorf("%v %w", c.Asset, asset.ErrNotSupported)
}
+ return nil
+}
- var maxDrawdown Swing
- if len(swings) > 0 {
- maxDrawdown = swings[0]
+func (c *CurrencyPairStatistic) analysePNLGrowth() {
+ if !c.Asset.IsFutures() {
+ return
}
- for i := range swings {
- if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
- maxDrawdown = swings[i]
+ var lowestUnrealised, highestUnrealised, lowestRealised, highestRealised ValueAtTime
+ for i := range c.Events {
+ if c.Events[i].PNL == nil {
+ continue
}
- }
-
- return maxDrawdown, nil
+ 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
+ }
+ }
+ c.LowestRealisedPNL = lowestRealised
+ c.LowestUnrealisedPNL = lowestUnrealised
+ c.HighestUnrealisedPNL = highestUnrealised
+ c.HighestRealisedPNL = highestRealised
}
diff --git a/backtester/eventhandlers/statistics/currencystatistics_test.go b/backtester/eventhandlers/statistics/currencystatistics_test.go
index cb4f8a07218..9970dea5be8 100644
--- a/backtester/eventhandlers/statistics/currencystatistics_test.go
+++ b/backtester/eventhandlers/statistics/currencystatistics_test.go
@@ -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)
+ }
}
diff --git a/backtester/eventhandlers/statistics/fundingstatistics.go b/backtester/eventhandlers/statistics/fundingstatistics.go
index 28db210643d..5392bd5b5ee 100644
--- a/backtester/eventhandlers/statistics/fundingstatistics.go
+++ b/backtester/eventhandlers/statistics/fundingstatistics.go
@@ -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
}
diff --git a/backtester/eventhandlers/statistics/fundingstatistics_test.go b/backtester/eventhandlers/statistics/fundingstatistics_test.go
index adb009dfc83..3b0641a7b65 100644
--- a/backtester/eventhandlers/statistics/fundingstatistics_test.go
+++ b/backtester/eventhandlers/statistics/fundingstatistics_test.go
@@ -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)
diff --git a/backtester/eventhandlers/statistics/printresults.go b/backtester/eventhandlers/statistics/printresults.go
new file mode 100644
index 00000000000..208b599c515
--- /dev/null
+++ b/backtester/eventhandlers/statistics/printresults.go
@@ -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
+}
diff --git a/backtester/eventhandlers/statistics/statistics.go b/backtester/eventhandlers/statistics/statistics.go
index 9bbf3aa7cc7..23bc6dcd150 100644
--- a/backtester/eventhandlers/statistics/statistics.go
+++ b/backtester/eventhandlers/statistics/statistics.go
@@ -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
-}
diff --git a/backtester/eventhandlers/statistics/statistics_test.go b/backtester/eventhandlers/statistics/statistics_test.go
index 165b6d9574b..aadb871fd77 100644
--- a/backtester/eventhandlers/statistics/statistics_test.go
+++ b/backtester/eventhandlers/statistics/statistics_test.go
@@ -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) {
diff --git a/backtester/eventhandlers/statistics/statistics_types.go b/backtester/eventhandlers/statistics/statistics_types.go
index 72a7988d9e1..b72e9ebb0ce 100644
--- a/backtester/eventhandlers/statistics/statistics_types.go
+++ b/backtester/eventhandlers/statistics/statistics_types.go
@@ -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
diff --git a/backtester/eventhandlers/strategies/base/base.go b/backtester/eventhandlers/strategies/base/base.go
index 2ecf958f4e8..f71c19962f7 100644
--- a/backtester/eventhandlers/strategies/base/base.go
+++ b/backtester/eventhandlers/strategies/base/base.go
@@ -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(),
diff --git a/backtester/eventhandlers/strategies/base/base_test.go b/backtester/eventhandlers/strategies/base/base_test.go
index 091fca63f6b..0dd9ca7ae05 100644
--- a/backtester/eventhandlers/strategies/base/base_test.go
+++ b/backtester/eventhandlers/strategies/base/base_test.go
@@ -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")
+ }
+}
diff --git a/backtester/eventhandlers/strategies/base/base_types.go b/backtester/eventhandlers/strategies/base/base_types.go
index 21f854ce0d7..ad7ecfe26b6 100644
--- a/backtester/eventhandlers/strategies/base/base_types.go
+++ b/backtester/eventhandlers/strategies/base/base_types.go
@@ -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")
diff --git a/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage.go b/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage.go
index c5f58f11407..6103fe89f23 100644
--- a/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage.go
+++ b/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage.go
@@ -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 {
diff --git a/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage_test.go b/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage_test.go
index 22c4ee281ba..2a9d675e72e 100644
--- a/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage_test.go
+++ b/backtester/eventhandlers/strategies/dollarcostaverage/dollarcostaverage_test.go
@@ -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,
diff --git a/backtester/eventhandlers/strategies/ftxcashandcarry/README.md b/backtester/eventhandlers/strategies/ftxcashandcarry/README.md
new file mode 100644
index 00000000000..366ffd9e962
--- /dev/null
+++ b/backtester/eventhandlers/strategies/ftxcashandcarry/README.md
@@ -0,0 +1,68 @@
+# GoCryptoTrader Backtester: Ftxcashandcarry package
+
+
+
+
+[![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
+
+
+
+If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
+
+***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***
diff --git a/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry.go b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry.go
new file mode 100644
index 00000000000..cd1ed9edb63
--- /dev/null
+++ b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry.go
@@ -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
+}
diff --git a/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry_test.go b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry_test.go
new file mode 100644
index 00000000000..16df50182ec
--- /dev/null
+++ b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry_test.go
@@ -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)
+ }
+}
diff --git a/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry_types.go b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry_types.go
new file mode 100644
index 00000000000..7eb0e4bbbd7
--- /dev/null
+++ b/backtester/eventhandlers/strategies/ftxcashandcarry/ftxcashandcarry_types.go
@@ -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
+}
diff --git a/backtester/eventhandlers/strategies/rsi/rsi.go b/backtester/eventhandlers/strategies/rsi/rsi.go
index 6504c6f04d0..1dbf76b57df 100644
--- a/backtester/eventhandlers/strategies/rsi/rsi.go
+++ b/backtester/eventhandlers/strategies/rsi/rsi.go
@@ -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 {
diff --git a/backtester/eventhandlers/strategies/rsi/rsi_test.go b/backtester/eventhandlers/strategies/rsi/rsi_test.go
index df7b44a20c8..7f3ee802ec1 100644
--- a/backtester/eventhandlers/strategies/rsi/rsi_test.go
+++ b/backtester/eventhandlers/strategies/rsi/rsi_test.go
@@ -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,
diff --git a/backtester/eventhandlers/strategies/strategies.go b/backtester/eventhandlers/strategies/strategies.go
index 99982a734ab..e5e8d50c867 100644
--- a/backtester/eventhandlers/strategies/strategies.go
+++ b/backtester/eventhandlers/strategies/strategies.go
@@ -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),
}
}
diff --git a/backtester/eventhandlers/strategies/strategies_types.go b/backtester/eventhandlers/strategies/strategies_types.go
index 590694c8d1e..f7fdd4a59a7 100644
--- a/backtester/eventhandlers/strategies/strategies_types.go
+++ b/backtester/eventhandlers/strategies/strategies_types.go
@@ -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)
diff --git a/backtester/eventhandlers/strategies/top2bottom2/top2bottom2.go b/backtester/eventhandlers/strategies/top2bottom2/top2bottom2.go
index 56f4a5be67b..04870c836d1 100644
--- a/backtester/eventhandlers/strategies/top2bottom2/top2bottom2.go
+++ b/backtester/eventhandlers/strategies/top2bottom2/top2bottom2.go
@@ -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(),
})
}
diff --git a/backtester/eventhandlers/strategies/top2bottom2/top2bottom2_test.go b/backtester/eventhandlers/strategies/top2bottom2/top2bottom2_test.go
index 99b91b53f8c..989642d1c19 100644
--- a/backtester/eventhandlers/strategies/top2bottom2/top2bottom2_test.go
+++ b/backtester/eventhandlers/strategies/top2bottom2/top2bottom2_test.go
@@ -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")
}
}
diff --git a/backtester/eventtypes/event/event.go b/backtester/eventtypes/event/event.go
index 0d88d38c510..95946d41c64 100644
--- a/backtester/eventtypes/event/event.go
+++ b/backtester/eventtypes/event/event.go
@@ -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)
+}
+
+// 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
}
-// GetReason returns the why
-func (b *Base) GetReason() string {
- return b.Reason
+func (b *Base) GetBase() *Base {
+ return b
}
diff --git a/backtester/eventtypes/event/event_test.go b/backtester/eventtypes/event/event_test.go
index a3369ea2327..c3dea563096 100644
--- a/backtester/eventtypes/event/event_test.go
+++ b/backtester/eventtypes/event/event_test.go
@@ -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 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 TestEvent_GetAssetType(t *testing.T) {
+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())
+ }
+}
diff --git a/backtester/eventtypes/event/event_types.go b/backtester/eventtypes/event/event_types.go
index 9c0af60d53b..1d6b4806cc5 100644
--- a/backtester/eventtypes/event/event_types.go
+++ b/backtester/eventtypes/event/event_types.go
@@ -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"`
}
diff --git a/backtester/eventtypes/fill/fill.go b/backtester/eventtypes/fill/fill.go
index 609c6225ced..8f7ee27ff08 100644
--- a/backtester/eventtypes/fill/fill.go
+++ b/backtester/eventtypes/fill/fill.go
@@ -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
+}
diff --git a/backtester/eventtypes/fill/fill_test.go b/backtester/eventtypes/fill/fill_test.go
index 1d343d9ba6d..53788874f0f 100644
--- a/backtester/eventtypes/fill/fill_test.go
+++ b/backtester/eventtypes/fill/fill_test.go
@@ -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")
+ }
+}
diff --git a/backtester/eventtypes/fill/fill_types.go b/backtester/eventtypes/fill/fill_types.go
index f172357a267..15a81cd92e5 100644
--- a/backtester/eventtypes/fill/fill_types.go
+++ b/backtester/eventtypes/fill/fill_types.go
@@ -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
}
diff --git a/backtester/eventtypes/kline/kline.go b/backtester/eventtypes/kline/kline.go
index d215a596d92..d529c421ff0 100644
--- a/backtester/eventtypes/kline/kline.go
+++ b/backtester/eventtypes/kline/kline.go
@@ -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
+}
diff --git a/backtester/eventtypes/kline/kline_test.go b/backtester/eventtypes/kline/kline_test.go
index 663c0cc806b..c3418db893b 100644
--- a/backtester/eventtypes/kline/kline_test.go
+++ b/backtester/eventtypes/kline/kline_test.go
@@ -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)
+ }
+}
diff --git a/backtester/eventtypes/kline/kline_types.go b/backtester/eventtypes/kline/kline_types.go
index 71121e5b080..8fd8080d015 100644
--- a/backtester/eventtypes/kline/kline_types.go
+++ b/backtester/eventtypes/kline/kline_types.go
@@ -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
diff --git a/backtester/eventtypes/order/order.go b/backtester/eventtypes/order/order.go
index 102ba10ad84..8dd7edfa3dd 100644
--- a/backtester/eventtypes/order/order.go
+++ b/backtester/eventtypes/order/order.go
@@ -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
+}
diff --git a/backtester/eventtypes/order/order_test.go b/backtester/eventtypes/order/order_test.go
index e41f26841ed..449804aa9ee 100644
--- a/backtester/eventtypes/order/order_test.go
+++ b/backtester/eventtypes/order/order_test.go
@@ -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)
+ }
+}
diff --git a/backtester/eventtypes/order/order_types.go b/backtester/eventtypes/order/order_types.go
index 4f7c6aff3ff..c51729c7217 100644
--- a/backtester/eventtypes/order/order_types.go
+++ b/backtester/eventtypes/order/order_types.go
@@ -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
}
diff --git a/backtester/eventtypes/signal/signal.go b/backtester/eventtypes/signal/signal.go
index 36703086dad..47797069fe5 100644
--- a/backtester/eventtypes/signal/signal.go
+++ b/backtester/eventtypes/signal/signal.go
@@ -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
+}
diff --git a/backtester/eventtypes/signal/signal_test.go b/backtester/eventtypes/signal/signal_test.go
index d6e223eb9ff..ca0cc5b62e8 100644
--- a/backtester/eventtypes/signal/signal_test.go
+++ b/backtester/eventtypes/signal/signal_test.go
@@ -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")
+ }
+}
diff --git a/backtester/eventtypes/signal/signal_types.go b/backtester/eventtypes/signal/signal_types.go
index a07cd676f63..4235dfd0ea3 100644
--- a/backtester/eventtypes/signal/signal_types.go
+++ b/backtester/eventtypes/signal/signal_types.go
@@ -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
}
diff --git a/backtester/funding/README.md b/backtester/funding/README.md
index cc3eb3aef79..c593cbb76d3 100644
--- a/backtester/funding/README.md
+++ b/backtester/funding/README.md
@@ -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 |
diff --git a/backtester/funding/collateralpair.go b/backtester/funding/collateralpair.go
new file mode 100644
index 00000000000..fd69329fafc
--- /dev/null
+++ b/backtester/funding/collateralpair.go
@@ -0,0 +1,148 @@
+package funding
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/shopspring/decimal"
+ "github.com/thrasher-corp/gocryptotrader/currency"
+ gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
+)
+
+// collateral related errors
+var (
+ // ErrNotPair is returned when a user requests funding pair details when it is a collateral pair
+ ErrNotPair = errors.New("not a funding pair")
+ ErrIsCollateral = errors.New("is collateral pair")
+ ErrNilPair = errors.New("nil pair")
+ errUnhandled = errors.New("unhandled scenario")
+ errPositiveOnly = errors.New("reduces the amount by subtraction, positive numbers only")
+)
+
+// CanPlaceOrder checks if there is any collateral to spare
+func (c *CollateralPair) CanPlaceOrder(_ gctorder.Side) bool {
+ return c.collateral.CanPlaceOrder()
+}
+
+// TakeProfit handles both the reduction of contracts and the change in collateral
+func (c *CollateralPair) TakeProfit(contracts, positionReturns decimal.Decimal) error {
+ err := c.contract.ReduceContracts(contracts)
+ if err != nil {
+ return err
+ }
+ return c.collateral.TakeProfit(positionReturns)
+}
+
+// ContractCurrency returns the contract currency
+func (c *CollateralPair) ContractCurrency() currency.Code {
+ return c.contract.currency
+}
+
+// CollateralCurrency returns collateral currency
+func (c *CollateralPair) CollateralCurrency() currency.Code {
+ return c.collateral.currency
+}
+
+// InitialFunds returns initial funds of collateral
+func (c *CollateralPair) InitialFunds() decimal.Decimal {
+ return c.collateral.initialFunds
+}
+
+// AvailableFunds returns available funds of collateral
+func (c *CollateralPair) AvailableFunds() decimal.Decimal {
+ return c.collateral.available
+}
+
+// UpdateContracts adds or subtracts contracts based on order direction
+func (c *CollateralPair) UpdateContracts(s gctorder.Side, amount decimal.Decimal) error {
+ switch {
+ case c.currentDirection == nil:
+ c.currentDirection = &s
+ return c.contract.AddContracts(amount)
+ case *c.currentDirection == s:
+ return c.contract.AddContracts(amount)
+ case *c.currentDirection != s:
+ return c.contract.ReduceContracts(amount)
+ default:
+ return errUnhandled
+ }
+}
+
+// ReleaseContracts lowers the amount of available contracts
+func (c *CollateralPair) ReleaseContracts(amount decimal.Decimal) error {
+ if amount.LessThanOrEqual(decimal.Zero) {
+ return fmt.Errorf("release %w", errPositiveOnly)
+ }
+ if c.contract.available.LessThan(amount) {
+ return fmt.Errorf("%w amount '%v' larger than available '%v'", errCannotAllocate, amount, c.contract.available)
+ }
+ c.contract.available = c.contract.available.Sub(amount)
+ return nil
+}
+
+// Reserve reserves or releases collateral based on order side
+func (c *CollateralPair) Reserve(amount decimal.Decimal, side gctorder.Side) error {
+ switch side {
+ case gctorder.Long, gctorder.Short, gctorder.ClosePosition:
+ return c.collateral.Reserve(amount)
+ default:
+ return fmt.Errorf("%w for %v %v %v. Unknown side %v",
+ errCannotAllocate,
+ c.collateral.exchange,
+ c.collateral.asset,
+ c.collateral.currency,
+ side)
+ }
+}
+
+// Liquidate kills your funds and future
+// all value storage are reduced to zero when triggered
+func (c *CollateralPair) Liquidate() {
+ c.collateral.available = decimal.Zero
+ c.collateral.reserved = decimal.Zero
+ c.contract.available = decimal.Zero
+ c.contract.reserved = decimal.Zero
+ c.currentDirection = nil
+}
+
+// CurrentHoldings returns available contract holdings
+func (c *CollateralPair) CurrentHoldings() decimal.Decimal {
+ return c.contract.available
+}
+
+// FundReader returns a fund reader interface of collateral
+func (c *CollateralPair) FundReader() IFundReader {
+ return c
+}
+
+// FundReserver returns a fund reserver interface of CollateralPair
+func (c *CollateralPair) FundReserver() IFundReserver {
+ return c
+}
+
+// PairReleaser returns an error as there is no such thing for collateral
+func (c *CollateralPair) PairReleaser() (IPairReleaser, error) {
+ return nil, fmt.Errorf("could not get pair releaser for %v %v %v %v %w", c.contract.exchange, c.collateral.asset, c.ContractCurrency(), c.CollateralCurrency(), ErrNotPair)
+}
+
+// CollateralReleaser returns an ICollateralReleaser to interact with
+// collateral
+func (c *CollateralPair) CollateralReleaser() (ICollateralReleaser, error) {
+ return c, nil
+}
+
+// FundReleaser returns an IFundReleaser to interact with
+// collateral
+func (c *CollateralPair) FundReleaser() IFundReleaser {
+ return c
+}
+
+// GetPairReader returns an error because collateral isn't a pair
+func (c *CollateralPair) GetPairReader() (IPairReader, error) {
+ return nil, fmt.Errorf("could not return pair reader for %v %v %v %v %w", c.contract.exchange, c.collateral.asset, c.ContractCurrency(), c.CollateralCurrency(), ErrNotPair)
+}
+
+// GetCollateralReader returns a collateral reader interface of CollateralPair
+func (c *CollateralPair) GetCollateralReader() (ICollateralReader, error) {
+ return c, nil
+}
diff --git a/backtester/funding/collateralpair_test.go b/backtester/funding/collateralpair_test.go
new file mode 100644
index 00000000000..6273c85a626
--- /dev/null
+++ b/backtester/funding/collateralpair_test.go
@@ -0,0 +1,311 @@
+package funding
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/shopspring/decimal"
+ "github.com/thrasher-corp/gocryptotrader/currency"
+ "github.com/thrasher-corp/gocryptotrader/exchanges/asset"
+ gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
+)
+
+func TestCollateralCanPlaceOrder(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{available: decimal.NewFromInt(1337)},
+ }
+ if !c.CanPlaceOrder(gctorder.UnknownSide) {
+ t.Error("expected true")
+ }
+}
+
+func TestCollateralTakeProfit(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{
+ asset: asset.Futures,
+ isCollateral: true,
+ },
+ contract: &Item{asset: asset.Futures,
+ available: decimal.NewFromInt(1),
+ },
+ }
+ var expectedError error
+ err := c.TakeProfit(decimal.NewFromInt(1), decimal.NewFromInt(1))
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestCollateralCollateralCurrency(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{currency: currency.DOGE},
+ }
+ if !c.CollateralCurrency().Equal(currency.DOGE) {
+ t.Errorf("recevied '%v' expected '%v'", c.CollateralCurrency(), currency.DOGE)
+ }
+}
+
+func TestCollateralContractCurrency(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ contract: &Item{currency: currency.DOGE},
+ }
+ if !c.ContractCurrency().Equal(currency.DOGE) {
+ t.Errorf("recevied '%v' expected '%v'", c.ContractCurrency(), currency.DOGE)
+ }
+}
+
+func TestCollateralInitialFunds(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{initialFunds: decimal.NewFromInt(1337)},
+ }
+ if !c.InitialFunds().Equal(decimal.NewFromInt(1337)) {
+ t.Errorf("recevied '%v' expected '%v'", c.InitialFunds(), decimal.NewFromInt(1337))
+ }
+}
+
+func TestCollateralAvailableFunds(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{available: decimal.NewFromInt(1337)},
+ }
+ if !c.AvailableFunds().Equal(decimal.NewFromInt(1337)) {
+ t.Errorf("recevied '%v' expected '%v'", c.AvailableFunds(), decimal.NewFromInt(1337))
+ }
+}
+
+func TestCollateralGetPairReader(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ contract: &Item{},
+ collateral: &Item{},
+ }
+ if _, err := c.GetPairReader(); !errors.Is(err, ErrNotPair) {
+ t.Errorf("recevied '%v' expected '%v'", err, ErrNotPair)
+ }
+}
+
+func TestCollateralGetCollateralReader(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{available: decimal.NewFromInt(1337)},
+ }
+ var expectedError error
+ cr, err := c.GetCollateralReader()
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if cr != c {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestCollateralUpdateContracts(t *testing.T) {
+ t.Parallel()
+ b := gctorder.Buy
+ var expectedError error
+ c := &CollateralPair{
+ collateral: &Item{
+ asset: asset.Futures,
+ isCollateral: true,
+ },
+ contract: &Item{asset: asset.Futures},
+ currentDirection: &b,
+ }
+ leet := decimal.NewFromInt(1337)
+ err := c.UpdateContracts(gctorder.Buy, leet)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if !c.contract.available.Equal(leet) {
+ t.Errorf("recevied '%v' expected '%v'", c.contract.available, leet)
+ }
+ b = gctorder.Sell
+ err = c.UpdateContracts(gctorder.Buy, leet)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if !c.contract.available.Equal(decimal.Zero) {
+ t.Errorf("recevied '%v' expected '%v'", c.contract.available, decimal.Zero)
+ }
+
+ c.currentDirection = nil
+ err = c.UpdateContracts(gctorder.Buy, leet)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if !c.contract.available.Equal(leet) {
+ t.Errorf("recevied '%v' expected '%v'", c.contract.available, leet)
+ }
+}
+
+func TestCollateralReleaseContracts(t *testing.T) {
+ t.Parallel()
+ b := gctorder.Buy
+ c := &CollateralPair{
+ collateral: &Item{
+ asset: asset.Futures,
+ isCollateral: true,
+ },
+ contract: &Item{asset: asset.Futures},
+ currentDirection: &b,
+ }
+
+ expectedError := errPositiveOnly
+ err := c.ReleaseContracts(decimal.Zero)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+
+ expectedError = errCannotAllocate
+ err = c.ReleaseContracts(decimal.NewFromInt(1337))
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+
+ expectedError = nil
+ c.contract.available = decimal.NewFromInt(1337)
+ err = c.ReleaseContracts(decimal.NewFromInt(1337))
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestCollateralFundReader(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{available: decimal.NewFromInt(1337)},
+ }
+ if c.FundReader() != c {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestCollateralPairReleaser(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{},
+ contract: &Item{},
+ }
+ if _, err := c.PairReleaser(); !errors.Is(err, ErrNotPair) {
+ t.Errorf("recevied '%v' expected '%v'", err, ErrNotPair)
+ }
+}
+
+func TestCollateralFundReserver(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{available: decimal.NewFromInt(1337)},
+ }
+ if c.FundReserver() != c {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestCollateralCollateralReleaser(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{},
+ contract: &Item{},
+ }
+ var expectedError error
+ if _, err := c.CollateralReleaser(); !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestCollateralFundReleaser(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{available: decimal.NewFromInt(1337)},
+ }
+ if c.FundReleaser() != c {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestCollateralReserve(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{
+ asset: asset.Futures,
+ isCollateral: true,
+ available: decimal.NewFromInt(1337),
+ },
+ contract: &Item{asset: asset.Futures},
+ }
+ var expectedError error
+ err := c.Reserve(decimal.NewFromInt(1), gctorder.Long)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if !c.collateral.reserved.Equal(decimal.NewFromInt(1)) {
+ t.Errorf("recevied '%v' expected '%v'", c.collateral.reserved, decimal.NewFromInt(1))
+ }
+ if !c.collateral.available.Equal(decimal.NewFromInt(1336)) {
+ t.Errorf("recevied '%v' expected '%v'", c.collateral.available, decimal.NewFromInt(1336))
+ }
+
+ err = c.Reserve(decimal.NewFromInt(1), gctorder.Short)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if !c.collateral.reserved.Equal(decimal.NewFromInt(2)) {
+ t.Errorf("recevied '%v' expected '%v'", c.collateral.reserved, decimal.NewFromInt(2))
+ }
+ if !c.collateral.available.Equal(decimal.NewFromInt(1335)) {
+ t.Errorf("recevied '%v' expected '%v'", c.collateral.available, decimal.NewFromInt(1335))
+ }
+
+ err = c.Reserve(decimal.NewFromInt(2), gctorder.ClosePosition)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if !c.collateral.reserved.Equal(decimal.NewFromInt(4)) {
+ t.Errorf("recevied '%v' expected '%v'", c.collateral.reserved, decimal.Zero)
+ }
+ if !c.collateral.available.Equal(decimal.NewFromInt(1333)) {
+ t.Errorf("recevied '%v' expected '%v'", c.collateral.available, decimal.NewFromInt(1333))
+ }
+
+ expectedError = errCannotAllocate
+ err = c.Reserve(decimal.NewFromInt(2), gctorder.Buy)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestCollateralLiquidate(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ collateral: &Item{
+ asset: asset.Futures,
+ isCollateral: true,
+ available: decimal.NewFromInt(1337),
+ },
+ contract: &Item{asset: asset.Futures,
+ available: decimal.NewFromInt(1337),
+ },
+ }
+ c.Liquidate()
+ if !c.collateral.available.Equal(decimal.Zero) {
+ t.Errorf("recevied '%v' expected '%v'", c.collateral.available, decimal.Zero)
+ }
+ if !c.contract.available.Equal(decimal.Zero) {
+ t.Errorf("recevied '%v' expected '%v'", c.contract.available, decimal.Zero)
+ }
+}
+
+func TestCollateralCurrentHoldings(t *testing.T) {
+ t.Parallel()
+ c := &CollateralPair{
+ contract: &Item{available: decimal.NewFromInt(1337)},
+ }
+ if !c.CurrentHoldings().Equal(decimal.NewFromInt(1337)) {
+ t.Errorf("recevied '%v' expected '%v'", c.CurrentHoldings(), decimal.NewFromInt(1337))
+ }
+}
diff --git a/backtester/funding/funding.go b/backtester/funding/funding.go
index c2399620a16..65bed25a298 100644
--- a/backtester/funding/funding.go
+++ b/backtester/funding/funding.go
@@ -1,6 +1,7 @@
package funding
import (
+ "context"
"errors"
"fmt"
"sort"
@@ -12,9 +13,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/funding/trackingcurrencies"
"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"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
- "github.com/thrasher-corp/gocryptotrader/exchanges/order"
+ gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
var (
@@ -23,7 +26,8 @@ var (
// ErrAlreadyExists used when a matching item or pair is already in the funding manager
ErrAlreadyExists = errors.New("funding already exists")
// ErrUSDTrackingDisabled used when attempting to track USD values when disabled
- ErrUSDTrackingDisabled = errors.New("USD tracking disabled")
+ ErrUSDTrackingDisabled = errors.New("USD tracking disabled")
+
errCannotAllocate = errors.New("cannot allocate funds")
errZeroAmountReceived = errors.New("amount received less than or equal to zero")
errNegativeAmountReceived = errors.New("received negative decimal")
@@ -31,15 +35,30 @@ var (
errCannotTransferToSameFunds = errors.New("cannot send funds to self")
errTransferMustBeSameCurrency = errors.New("cannot transfer to different currency")
errCannotMatchTrackingToItem = errors.New("cannot match tracking data to funding items")
+ errNotFutures = errors.New("item linking collateral currencies must be a futures asset")
+ errExchangeManagerRequired = errors.New("exchange manager required")
)
// SetupFundingManager creates the funding holder. It carries knowledge about levels of funding
// across all execution handlers and enables fund transfers
-func SetupFundingManager(usingExchangeLevelFunding, disableUSDTracking bool) *FundManager {
+func SetupFundingManager(exchManager *engine.ExchangeManager, usingExchangeLevelFunding, disableUSDTracking bool) (*FundManager, error) {
+ if exchManager == nil {
+ return nil, errExchangeManagerRequired
+ }
return &FundManager{
usingExchangeLevelFunding: usingExchangeLevelFunding,
disableUSDTracking: disableUSDTracking,
- }
+ exchangeManager: exchManager,
+ }, nil
+}
+
+// CreateFuturesCurrencyCode converts a currency pair into a code
+// The main reasoning is that as a contract, it exists as an item even if
+// it is formatted as BTC-1231. To treat it as a pair in the funding system
+// would cause an increase in funds for BTC, when it is an increase in contracts
+// This function is basic, but is important be explicit in why this is occurring
+func CreateFuturesCurrencyCode(b, q currency.Code) currency.Code {
+ return currency.NewCode(fmt.Sprintf("%s-%s", b, q))
}
// CreateItem creates a new funding item
@@ -52,34 +71,72 @@ func CreateItem(exch string, a asset.Item, ci currency.Code, initialFunds, trans
}
return &Item{
- exchange: exch,
+ exchange: strings.ToLower(exch),
asset: a,
currency: ci,
initialFunds: initialFunds,
available: initialFunds,
transferFee: transferFee,
- snapshot: make(map[time.Time]ItemSnapshot),
+ snapshot: make(map[int64]ItemSnapshot),
}, nil
}
+// LinkCollateralCurrency links an item to an existing currency code
+// for collateral purposes
+func (f *FundManager) LinkCollateralCurrency(item *Item, code currency.Code) error {
+ if item == nil {
+ return fmt.Errorf("%w missing item", common.ErrNilArguments)
+ }
+ if code.IsEmpty() {
+ return fmt.Errorf("%w unset currency", common.ErrNilArguments)
+ }
+ if !item.asset.IsFutures() {
+ return errNotFutures
+ }
+ if item.pairedWith != nil {
+ return fmt.Errorf("%w item already paired with %v", ErrAlreadyExists, item.pairedWith.currency)
+ }
+
+ for i := range f.items {
+ if f.items[i].currency.Equal(code) && f.items[i].asset == item.asset {
+ item.pairedWith = f.items[i]
+ return nil
+ }
+ }
+ collateral := &Item{
+ exchange: item.exchange,
+ asset: item.asset,
+ currency: code,
+ pairedWith: item,
+ isCollateral: true,
+ }
+ if err := f.AddItem(collateral); err != nil {
+ return err
+ }
+ item.pairedWith = collateral
+ return nil
+}
+
// CreateSnapshot creates a Snapshot for an event's point in time
// as funding.snapshots is a map, it allows for the last event
// in the chronological list to establish the canon at X time
func (f *FundManager) CreateSnapshot(t time.Time) {
for i := range f.items {
if f.items[i].snapshot == nil {
- f.items[i].snapshot = make(map[time.Time]ItemSnapshot)
+ f.items[i].snapshot = make(map[int64]ItemSnapshot)
}
+
iss := ItemSnapshot{
Available: f.items[i].available,
Time: t,
}
+
if !f.disableUSDTracking {
var usdClosePrice decimal.Decimal
- if f.items[i].usdTrackingCandles == nil {
+ if f.items[i].trackingCandles == nil {
continue
}
- usdCandles := f.items[i].usdTrackingCandles.GetStream()
+ usdCandles := f.items[i].trackingCandles.GetStream()
for j := range usdCandles {
if usdCandles[j].GetTime().Equal(t) {
usdClosePrice = usdCandles[j].GetClosePrice()
@@ -90,7 +147,7 @@ func (f *FundManager) CreateSnapshot(t time.Time) {
iss.USDValue = usdClosePrice.Mul(f.items[i].available)
}
- f.items[i].snapshot[t] = iss
+ f.items[i].snapshot[t.UnixNano()] = iss
}
}
@@ -110,12 +167,25 @@ func (f *FundManager) AddUSDTrackingData(k *kline.DataFromKline) error {
if baseSet && quoteSet {
return nil
}
+ if f.items[i].asset.IsFutures() && k.Item.Asset.IsFutures() {
+ if f.items[i].isCollateral {
+ err := f.setUSDCandles(k, i)
+ if err != nil {
+ return err
+ }
+ } else {
+ f.items[i].trackingCandles = k
+ baseSet = true
+ }
+ continue
+ }
+
if strings.EqualFold(f.items[i].exchange, k.Item.Exchange) &&
f.items[i].asset == k.Item.Asset {
if f.items[i].currency.Equal(k.Item.Pair.Base) {
- if f.items[i].usdTrackingCandles == nil &&
+ if f.items[i].trackingCandles == nil &&
trackingcurrencies.CurrencyIsUSDTracked(k.Item.Pair.Quote) {
- f.items[i].usdTrackingCandles = k
+ f.items[i].trackingCandles = k
if f.items[i].pairedWith != nil {
basePairedWith = f.items[i].pairedWith.currency
}
@@ -126,31 +196,11 @@ func (f *FundManager) AddUSDTrackingData(k *kline.DataFromKline) error {
if f.items[i].pairedWith != nil && !f.items[i].currency.Equal(basePairedWith) {
continue
}
- if f.items[i].usdTrackingCandles == nil {
- usdCandles := gctkline.Item{
- Exchange: k.Item.Exchange,
- Pair: currency.Pair{Delimiter: k.Item.Pair.Delimiter, Base: f.items[i].currency, Quote: currency.USD},
- Asset: k.Item.Asset,
- Interval: k.Item.Interval,
- Candles: make([]gctkline.Candle, len(k.Item.Candles)),
- }
- copy(usdCandles.Candles, k.Item.Candles)
- for j := range usdCandles.Candles {
- // usd stablecoins do not always match in value,
- // this is a simplified implementation that can allow
- // USD tracking for many different currencies across many exchanges
- // without retrieving n candle history and exchange rates
- usdCandles.Candles[j].Open = 1
- usdCandles.Candles[j].High = 1
- usdCandles.Candles[j].Low = 1
- usdCandles.Candles[j].Close = 1
- }
- cpy := *k
- cpy.Item = usdCandles
- if err := cpy.Load(); err != nil {
+ if f.items[i].trackingCandles == nil {
+ err := f.setUSDCandles(k, i)
+ if err != nil {
return err
}
- f.items[i].usdTrackingCandles = &cpy
}
quoteSet = true
}
@@ -162,11 +212,41 @@ func (f *FundManager) AddUSDTrackingData(k *kline.DataFromKline) error {
return fmt.Errorf("%w %v %v %v", errCannotMatchTrackingToItem, k.Item.Exchange, k.Item.Asset, k.Item.Pair)
}
+// setUSDCandles sets usd tracking candles
+// usd stablecoins do not always match in value,
+// this is a simplified implementation that can allow
+// USD tracking for many currencies across many exchanges
+func (f *FundManager) setUSDCandles(k *kline.DataFromKline, i int) error {
+ usdCandles := gctkline.Item{
+ Exchange: k.Item.Exchange,
+ Pair: currency.Pair{Delimiter: k.Item.Pair.Delimiter, Base: f.items[i].currency, Quote: currency.USD},
+ Asset: k.Item.Asset,
+ Interval: k.Item.Interval,
+ Candles: make([]gctkline.Candle, len(k.Item.Candles)),
+ }
+ for j := range usdCandles.Candles {
+ usdCandles.Candles[j] = gctkline.Candle{
+ Time: k.Item.Candles[j].Time,
+ Open: 1,
+ High: 1,
+ Low: 1,
+ Close: 1,
+ }
+ }
+ cpy := *k
+ cpy.Item = usdCandles
+ if err := cpy.Load(); err != nil {
+ return err
+ }
+ f.items[i].trackingCandles = &cpy
+ return nil
+}
+
// CreatePair adds two funding items and associates them with one another
// the association allows for the same currency to be used multiple times when
// usingExchangeLevelFunding is false. eg BTC-USDT and LTC-USDT do not share the same
// USDT level funding
-func CreatePair(base, quote *Item) (*Pair, error) {
+func CreatePair(base, quote *Item) (*SpotPair, error) {
if base == nil {
return nil, fmt.Errorf("base %w", common.ErrNilArguments)
}
@@ -179,7 +259,27 @@ func CreatePair(base, quote *Item) (*Pair, error) {
qCopy := *quote
bCopy.pairedWith = &qCopy
qCopy.pairedWith = &bCopy
- return &Pair{Base: &bCopy, Quote: &qCopy}, nil
+ return &SpotPair{base: &bCopy, quote: &qCopy}, nil
+}
+
+// CreateCollateral adds two funding items and associates them with one another
+// the association allows for the same currency to be used multiple times when
+// usingExchangeLevelFunding is false. eg BTC-USDT and LTC-USDT do not share the same
+// USDT level funding
+func CreateCollateral(contract, collateral *Item) (*CollateralPair, error) {
+ if contract == nil {
+ return nil, fmt.Errorf("base %w", common.ErrNilArguments)
+ }
+ if collateral == nil {
+ return nil, fmt.Errorf("quote %w", common.ErrNilArguments)
+ }
+ // copy to prevent the off chance of sending in the same base OR quote
+ // to create a new pair with a new base OR quote
+ bCopy := *contract
+ qCopy := *collateral
+ bCopy.pairedWith = &qCopy
+ qCopy.pairedWith = &bCopy
+ return &CollateralPair{contract: &bCopy, collateral: &qCopy}, nil
}
// Reset clears all settings
@@ -195,56 +295,88 @@ func (f *FundManager) USDTrackingDisabled() bool {
// GenerateReport builds report data for result HTML report
func (f *FundManager) GenerateReport() *Report {
report := Report{
- USDTotalsOverTime: make(map[time.Time]ItemSnapshot),
UsingExchangeLevelFunding: f.usingExchangeLevelFunding,
DisableUSDTracking: f.disableUSDTracking,
}
items := make([]ReportItem, len(f.items))
- for i := range f.items {
+ for x := range f.items {
item := ReportItem{
- Exchange: f.items[i].exchange,
- Asset: f.items[i].asset,
- Currency: f.items[i].currency,
- InitialFunds: f.items[i].initialFunds,
- TransferFee: f.items[i].transferFee,
- FinalFunds: f.items[i].available,
+ Exchange: f.items[x].exchange,
+ Asset: f.items[x].asset,
+ Currency: f.items[x].currency,
+ InitialFunds: f.items[x].initialFunds,
+ TransferFee: f.items[x].transferFee,
+ FinalFunds: f.items[x].available,
+ IsCollateral: f.items[x].isCollateral,
}
+
if !f.disableUSDTracking &&
- f.items[i].usdTrackingCandles != nil {
- usdStream := f.items[i].usdTrackingCandles.GetStream()
- item.USDInitialFunds = f.items[i].initialFunds.Mul(usdStream[0].GetClosePrice())
- item.USDFinalFunds = f.items[i].available.Mul(usdStream[len(usdStream)-1].GetClosePrice())
+ f.items[x].trackingCandles != nil {
+ usdStream := f.items[x].trackingCandles.GetStream()
+ item.USDInitialFunds = f.items[x].initialFunds.Mul(usdStream[0].GetClosePrice())
+ item.USDFinalFunds = f.items[x].available.Mul(usdStream[len(usdStream)-1].GetClosePrice())
item.USDInitialCostForOne = usdStream[0].GetClosePrice()
item.USDFinalCostForOne = usdStream[len(usdStream)-1].GetClosePrice()
- item.USDPairCandle = f.items[i].usdTrackingCandles
+ item.USDPairCandle = f.items[x].trackingCandles
}
+ // create a breakdown of USD values and currency contributions over the span of run
var pricingOverTime []ItemSnapshot
- for _, v := range f.items[i].snapshot {
- pricingOverTime = append(pricingOverTime, v)
- if !f.disableUSDTracking {
- usdTotalForPeriod := report.USDTotalsOverTime[v.Time]
- usdTotalForPeriod.Time = v.Time
- usdTotalForPeriod.USDValue = usdTotalForPeriod.USDValue.Add(v.USDValue)
- report.USDTotalsOverTime[v.Time] = usdTotalForPeriod
+ snaps:
+ for _, snapshot := range f.items[x].snapshot {
+ pricingOverTime = append(pricingOverTime, snapshot)
+ if f.items[x].asset.IsFutures() || f.disableUSDTracking {
+ // futures contracts / collateral does not contribute to USD value
+ // no USD tracking means no USD values to breakdown
+ continue
+ }
+ for y := range report.USDTotalsOverTime {
+ if report.USDTotalsOverTime[y].Time.Equal(snapshot.Time) {
+ report.USDTotalsOverTime[y].USDValue = report.USDTotalsOverTime[y].USDValue.Add(snapshot.USDValue)
+ report.USDTotalsOverTime[y].Breakdown = append(report.USDTotalsOverTime[y].Breakdown, CurrencyContribution{
+ Currency: f.items[x].currency,
+ USDContribution: snapshot.USDValue,
+ })
+ continue snaps
+ }
}
+ report.USDTotalsOverTime = append(report.USDTotalsOverTime, ItemSnapshot{
+ Time: snapshot.Time,
+ USDValue: snapshot.USDValue,
+ Breakdown: []CurrencyContribution{
+ {
+ Currency: f.items[x].currency,
+ USDContribution: snapshot.USDValue,
+ },
+ },
+ })
}
+
sort.Slice(pricingOverTime, func(i, j int) bool {
return pricingOverTime[i].Time.Before(pricingOverTime[j].Time)
})
item.Snapshots = pricingOverTime
- if f.items[i].initialFunds.IsZero() {
+ if f.items[x].initialFunds.IsZero() {
item.ShowInfinite = true
} else {
- item.Difference = f.items[i].available.Sub(f.items[i].initialFunds).Div(f.items[i].initialFunds).Mul(decimal.NewFromInt(100))
+ item.Difference = f.items[x].available.Sub(f.items[x].initialFunds).Div(f.items[x].initialFunds).Mul(decimal.NewFromInt(100))
}
- if f.items[i].pairedWith != nil {
- item.PairedWith = f.items[i].pairedWith.currency
+ if f.items[x].pairedWith != nil {
+ item.PairedWith = f.items[x].pairedWith.currency
}
+ report.InitialFunds = report.InitialFunds.Add(item.USDInitialFunds)
- items[i] = item
+ items[x] = item
}
+
+ if len(report.USDTotalsOverTime) > 0 {
+ sort.Slice(report.USDTotalsOverTime, func(i, j int) bool {
+ return report.USDTotalsOverTime[i].Time.Before(report.USDTotalsOverTime[j].Time)
+ })
+ report.FinalFunds = report.USDTotalsOverTime[len(report.USDTotalsOverTime)-1].USDValue
+ }
+
report.Items = items
return &report
}
@@ -287,7 +419,10 @@ func (f *FundManager) Transfer(amount decimal.Decimal, sender, receiver *Item, i
if err != nil {
return err
}
- receiver.IncreaseAvailable(receiveAmount)
+ err = receiver.IncreaseAvailable(receiveAmount)
+ if err != nil {
+ return err
+ }
return sender.Release(sendAmount, decimal.Zero)
}
@@ -312,14 +447,14 @@ func (f *FundManager) Exists(item *Item) bool {
}
// AddPair adds a pair to the fund manager if it does not exist
-func (f *FundManager) AddPair(p *Pair) error {
- if f.Exists(p.Base) {
- return fmt.Errorf("%w %v", ErrAlreadyExists, p.Base)
+func (f *FundManager) AddPair(p *SpotPair) error {
+ if f.Exists(p.base) {
+ return fmt.Errorf("%w %v", ErrAlreadyExists, p.base)
}
- if f.Exists(p.Quote) {
- return fmt.Errorf("%w %v", ErrAlreadyExists, p.Quote)
+ if f.Exists(p.quote) {
+ return fmt.Errorf("%w %v", ErrAlreadyExists, p.quote)
}
- f.items = append(f.items, p.Base, p.Quote)
+ f.items = append(f.items, p.base, p.quote)
return nil
}
@@ -329,12 +464,46 @@ func (f *FundManager) IsUsingExchangeLevelFunding() bool {
}
// GetFundingForEvent This will construct a funding based on a backtesting event
-func (f *FundManager) GetFundingForEvent(ev common.EventHandler) (*Pair, error) {
- return f.GetFundingForEAP(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
+func (f *FundManager) GetFundingForEvent(ev common.EventHandler) (IFundingPair, error) {
+ return f.getFundingForEAP(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
+}
+
+// GetFundingForEAP This will construct a funding based on the exchange, asset, currency pair
+func (f *FundManager) getFundingForEAP(exch string, a asset.Item, p currency.Pair) (IFundingPair, error) {
+ if a.IsFutures() {
+ var collat CollateralPair
+ for i := range f.items {
+ if f.items[i].MatchesCurrency(currency.NewCode(p.String())) {
+ collat.contract = f.items[i]
+ collat.collateral = f.items[i].pairedWith
+ return &collat, nil
+ }
+ }
+ } else {
+ var resp SpotPair
+ for i := range f.items {
+ if f.items[i].BasicEqual(exch, a, p.Base, p.Quote) {
+ resp.base = f.items[i]
+ continue
+ }
+ if f.items[i].BasicEqual(exch, a, p.Quote, p.Base) {
+ resp.quote = f.items[i]
+ }
+ }
+ if resp.base == nil {
+ return nil, fmt.Errorf("base %v %w", p.Base, ErrFundsNotFound)
+ }
+ if resp.quote == nil {
+ return nil, fmt.Errorf("quote %v %w", p.Quote, ErrFundsNotFound)
+ }
+ return &resp, nil
+ }
+
+ return nil, fmt.Errorf("%v %v %v %w", exch, a, p, ErrFundsNotFound)
}
// GetFundingForEAC This will construct a funding based on the exchange, asset, currency code
-func (f *FundManager) GetFundingForEAC(exch string, a asset.Item, c currency.Code) (*Item, error) {
+func (f *FundManager) getFundingForEAC(exch string, a asset.Item, c currency.Code) (*Item, error) {
for i := range f.items {
if f.items[i].BasicEqual(exch, a, c, currency.EMPTYCODE) {
return f.items[i], nil
@@ -343,216 +512,151 @@ func (f *FundManager) GetFundingForEAC(exch string, a asset.Item, c currency.Cod
return nil, ErrFundsNotFound
}
-// GetFundingForEAP This will construct a funding based on the exchange, asset, currency pair
-func (f *FundManager) GetFundingForEAP(exch string, a asset.Item, p currency.Pair) (*Pair, error) {
- var resp Pair
+// Liquidate will remove all funding for all items belonging to an exchange
+func (f *FundManager) Liquidate(ev common.EventHandler) {
+ if ev == nil {
+ return
+ }
for i := range f.items {
- if f.items[i].BasicEqual(exch, a, p.Base, p.Quote) {
- resp.Base = f.items[i]
- continue
- }
- if f.items[i].BasicEqual(exch, a, p.Quote, p.Base) {
- resp.Quote = f.items[i]
+ if f.items[i].exchange == ev.GetExchange() {
+ f.items[i].reserved = decimal.Zero
+ f.items[i].available = decimal.Zero
+ f.items[i].isLiquidated = true
}
}
- if resp.Base == nil {
- return nil, fmt.Errorf("base %w", ErrFundsNotFound)
- }
- if resp.Quote == nil {
- return nil, fmt.Errorf("quote %w", ErrFundsNotFound)
- }
- return &resp, nil
-}
-
-// BaseInitialFunds returns the initial funds
-// from the base in a currency pair
-func (p *Pair) BaseInitialFunds() decimal.Decimal {
- return p.Base.initialFunds
-}
-
-// QuoteInitialFunds returns the initial funds
-// from the quote in a currency pair
-func (p *Pair) QuoteInitialFunds() decimal.Decimal {
- return p.Quote.initialFunds
-}
-
-// BaseAvailable returns the available funds
-// from the base in a currency pair
-func (p *Pair) BaseAvailable() decimal.Decimal {
- return p.Base.available
-}
-
-// QuoteAvailable returns the available funds
-// from the quote in a currency pair
-func (p *Pair) QuoteAvailable() decimal.Decimal {
- return p.Quote.available
-}
-
-// Reserve allocates an amount of funds to be used at a later time
-// it prevents multiple events from claiming the same resource
-// changes which currency to affect based on the order side
-func (p *Pair) Reserve(amount decimal.Decimal, side order.Side) error {
- switch side {
- case order.Buy:
- return p.Quote.Reserve(amount)
- case order.Sell:
- return p.Base.Reserve(amount)
- default:
- return fmt.Errorf("%w for %v %v %v. Unknown side %v",
- errCannotAllocate,
- p.Base.exchange,
- p.Base.asset,
- p.Base.currency,
- side)
- }
}
-// Release reduces the amount of funding reserved and adds any difference
-// back to the available amount
-// changes which currency to affect based on the order side
-func (p *Pair) Release(amount, diff decimal.Decimal, side order.Side) error {
- switch side {
- case order.Buy:
- return p.Quote.Release(amount, diff)
- case order.Sell:
- return p.Base.Release(amount, diff)
- default:
- return fmt.Errorf("%w for %v %v %v. Unknown side %v",
- errCannotAllocate,
- p.Base.exchange,
- p.Base.asset,
- p.Base.currency,
- side)
+// GetAllFunding returns basic representations of all current
+// holdings from the latest point
+func (f *FundManager) GetAllFunding() []BasicItem {
+ result := make([]BasicItem, len(f.items))
+ for i := range f.items {
+ var usd decimal.Decimal
+ if f.items[i].trackingCandles != nil {
+ latest := f.items[i].trackingCandles.Latest()
+ if latest != nil {
+ usd = latest.GetClosePrice()
+ }
+ }
+ result[i] = BasicItem{
+ Exchange: f.items[i].exchange,
+ Asset: f.items[i].asset,
+ Currency: f.items[i].currency,
+ InitialFunds: f.items[i].initialFunds,
+ Available: f.items[i].available,
+ Reserved: f.items[i].reserved,
+ USDPrice: usd,
+ }
}
+ return result
}
-// IncreaseAvailable adds funding to the available amount
-// changes which currency to affect based on the order side
-func (p *Pair) IncreaseAvailable(amount decimal.Decimal, side order.Side) {
- switch side {
- case order.Buy:
- p.Base.IncreaseAvailable(amount)
- case order.Sell:
- p.Quote.IncreaseAvailable(amount)
+// UpdateCollateral will recalculate collateral for an exchange
+// based on the event passed in
+func (f *FundManager) UpdateCollateral(ev common.EventHandler) error {
+ if ev == nil {
+ return common.ErrNilEvent
}
-}
-
-// CanPlaceOrder does a > 0 check to see if there are any funds
-// to place an order with
-// changes which currency to affect based on the order side
-func (p *Pair) CanPlaceOrder(side order.Side) bool {
- switch side {
- case order.Buy:
- return p.Quote.CanPlaceOrder()
- case order.Sell:
- return p.Base.CanPlaceOrder()
+ exchMap := make(map[string]exchange.IBotExchange)
+ var collateralAmount decimal.Decimal
+ var err error
+ calculator := gctorder.TotalCollateralCalculator{
+ CalculateOffline: true,
}
- return false
-}
-// Reserve allocates an amount of funds to be used at a later time
-// it prevents multiple events from claiming the same resource
-func (i *Item) Reserve(amount decimal.Decimal) error {
- if amount.LessThanOrEqual(decimal.Zero) {
- return errZeroAmountReceived
- }
- if amount.GreaterThan(i.available) {
- return fmt.Errorf("%w for %v %v %v. Requested %v Available: %v",
- errCannotAllocate,
- i.exchange,
- i.asset,
- i.currency,
- amount,
- i.available)
- }
- i.available = i.available.Sub(amount)
- i.reserved = i.reserved.Add(amount)
- return nil
-}
+ for i := range f.items {
+ if f.items[i].asset.IsFutures() {
+ // futures positions aren't collateral, they use it
+ continue
+ }
+ _, ok := exchMap[f.items[i].exchange]
+ if !ok {
+ var exch exchange.IBotExchange
+ exch, err = f.exchangeManager.GetExchangeByName(f.items[i].exchange)
+ if err != nil {
+ return err
+ }
+ exchMap[f.items[i].exchange] = exch
+ }
+ var usd decimal.Decimal
+ if f.items[i].trackingCandles != nil {
+ latest := f.items[i].trackingCandles.Latest()
+ if latest != nil {
+ usd = latest.GetClosePrice()
+ }
+ }
+ if usd.IsZero() {
+ continue
+ }
+ var side = gctorder.Buy
+ if !f.items[i].available.GreaterThan(decimal.Zero) {
+ side = gctorder.Sell
+ }
-// Release reduces the amount of funding reserved and adds any difference
-// back to the available amount
-func (i *Item) Release(amount, diff decimal.Decimal) error {
- if amount.LessThanOrEqual(decimal.Zero) {
- return errZeroAmountReceived
+ calculator.CollateralAssets = append(calculator.CollateralAssets, gctorder.CollateralCalculator{
+ CalculateOffline: true,
+ CollateralCurrency: f.items[i].currency,
+ Asset: f.items[i].asset,
+ Side: side,
+ FreeCollateral: f.items[i].available,
+ LockedCollateral: f.items[i].reserved,
+ USDPrice: usd,
+ })
}
- if diff.IsNegative() {
- return fmt.Errorf("%w diff", errNegativeAmountReceived)
+ exch, ok := exchMap[ev.GetExchange()]
+ if !ok {
+ return fmt.Errorf("%v %w", ev.GetExchange(), engine.ErrExchangeNotFound)
}
- if amount.GreaterThan(i.reserved) {
- return fmt.Errorf("%w for %v %v %v. Requested %v Reserved: %v",
- errCannotAllocate,
- i.exchange,
- i.asset,
- i.currency,
- amount,
- i.reserved)
+ futureCurrency, futureAsset, err := exch.GetCollateralCurrencyForContract(ev.GetAssetType(), ev.Pair())
+ if err != nil {
+ return err
}
- i.reserved = i.reserved.Sub(amount)
- i.available = i.available.Add(diff)
- return nil
-}
-// IncreaseAvailable adds funding to the available amount
-func (i *Item) IncreaseAvailable(amount decimal.Decimal) {
- if amount.IsNegative() || amount.IsZero() {
- return
+ collat, err := exchMap[ev.GetExchange()].CalculateTotalCollateral(context.TODO(), &calculator)
+ if err != nil {
+ return err
}
- i.available = i.available.Add(amount)
-}
-// CanPlaceOrder checks if the item has any funds available
-func (i *Item) CanPlaceOrder() bool {
- return i.available.GreaterThan(decimal.Zero)
+ for i := range f.items {
+ if f.items[i].exchange == ev.GetExchange() &&
+ f.items[i].asset == futureAsset &&
+ f.items[i].currency.Equal(futureCurrency) {
+ f.items[i].available = collat.AvailableCollateral
+ return nil
+ }
+ }
+ return fmt.Errorf("%w to allocate %v to %v %v %v", ErrFundsNotFound, collateralAmount, ev.GetExchange(), ev.GetAssetType(), futureCurrency)
}
-// Equal checks for equality via an Item to compare to
-func (i *Item) Equal(item *Item) bool {
- if i == nil && item == nil {
- return true
- }
- if item == nil || i == nil {
- return false
- }
- if i.currency.Equal(item.currency) &&
- i.asset == item.asset &&
- i.exchange == item.exchange {
- if i.pairedWith == nil && item.pairedWith == nil {
- return true
- }
- if i.pairedWith == nil || item.pairedWith == nil {
- return false
- }
- if i.pairedWith.currency.Equal(item.pairedWith.currency) &&
- i.pairedWith.asset == item.pairedWith.asset &&
- i.pairedWith.exchange == item.pairedWith.exchange {
+// HasFutures returns whether the funding manager contains any futures assets
+func (f *FundManager) HasFutures() bool {
+ for i := range f.items {
+ if f.items[i].isCollateral || f.items[i].asset.IsFutures() {
return true
}
}
return false
}
-// BasicEqual checks for equality via passed in values
-func (i *Item) BasicEqual(exch string, a asset.Item, currency, pairedCurrency currency.Code) bool {
- return i != nil &&
- i.exchange == exch &&
- i.asset == a &&
- i.currency.Equal(currency) &&
- (i.pairedWith == nil ||
- (i.pairedWith != nil && i.pairedWith.currency.Equal(pairedCurrency)))
-}
-
-// MatchesCurrency checks that an item's currency is equal
-func (i *Item) MatchesCurrency(c currency.Code) bool {
- return i != nil && i.currency.Equal(c)
-}
-
-// MatchesItemCurrency checks that an item's currency is equal
-func (i *Item) MatchesItemCurrency(item *Item) bool {
- return i != nil && item != nil && i.currency.Equal(item.currency)
+// RealisePNL adds the realised PNL to a receiving exchange asset pair
+func (f *FundManager) RealisePNL(receivingExchange string, receivingAsset asset.Item, receivingCurrency currency.Code, realisedPNL decimal.Decimal) error {
+ for i := range f.items {
+ if f.items[i].exchange == receivingExchange &&
+ f.items[i].asset == receivingAsset &&
+ f.items[i].currency.Equal(receivingCurrency) {
+ return f.items[i].TakeProfit(realisedPNL)
+ }
+ }
+ return fmt.Errorf("%w to allocate %v to %v %v %v", ErrFundsNotFound, realisedPNL, receivingExchange, receivingAsset, receivingCurrency)
}
-// MatchesExchange checks that an item's exchange is equal
-func (i *Item) MatchesExchange(item *Item) bool {
- return i != nil && item != nil && i.exchange == item.exchange
+// HasExchangeBeenLiquidated checks for any items with a matching exchange
+// and returns whether it has been liquidated
+func (f *FundManager) HasExchangeBeenLiquidated(ev common.EventHandler) bool {
+ for i := range f.items {
+ if ev.GetExchange() == f.items[i].exchange {
+ return f.items[i].isLiquidated
+ }
+ }
+ return false
}
diff --git a/backtester/funding/funding_test.go b/backtester/funding/funding_test.go
index 66f1fb463ed..533eb733786 100644
--- a/backtester/funding/funding_test.go
+++ b/backtester/funding/funding_test.go
@@ -8,33 +8,61 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
+ "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
+ "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"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"
- "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
var (
- elite = decimal.NewFromInt(1337)
- neg = decimal.NewFromInt(-1)
- one = decimal.NewFromInt(1)
- exch = "exch"
- a = asset.Spot
- base = currency.DOGE
- quote = currency.XRP
- pair = currency.NewPair(base, quote)
+ elite = decimal.NewFromInt(1337)
+ neg = decimal.NewFromInt(-1)
+ one = decimal.NewFromInt(1)
+ exchName = "exchname"
+ a = asset.Spot
+ base = currency.DOGE
+ quote = currency.XRP
+ pair = currency.NewPair(base, quote)
)
+// fakeEvent implements common.EventHandler without
+// caring about the response, or dealing with import cycles
+type fakeEvent struct{}
+
+func (f *fakeEvent) GetOffset() int64 { return 0 }
+func (f *fakeEvent) SetOffset(int64) {}
+func (f *fakeEvent) IsEvent() bool { return true }
+func (f *fakeEvent) GetTime() time.Time { return time.Now() }
+func (f *fakeEvent) Pair() currency.Pair { return pair }
+func (f *fakeEvent) GetExchange() string { return exchName }
+func (f *fakeEvent) GetInterval() gctkline.Interval { return gctkline.OneMin }
+func (f *fakeEvent) GetAssetType() asset.Item { return asset.Spot }
+func (f *fakeEvent) AppendReason(string) {}
+func (f *fakeEvent) GetClosePrice() decimal.Decimal { return elite }
+func (f *fakeEvent) AppendReasonf(s string, i ...interface{}) {}
+func (f *fakeEvent) GetBase() *event.Base { return &event.Base{} }
+func (f *fakeEvent) GetUnderlyingPair() currency.Pair { return pair }
+func (f *fakeEvent) GetConcatReasons() string { return "" }
+func (f *fakeEvent) GetReasons() []string { return nil }
+
func TestSetupFundingManager(t *testing.T) {
t.Parallel()
- f := SetupFundingManager(true, false)
+ f, err := SetupFundingManager(&engine.ExchangeManager{}, true, false)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
if !f.usingExchangeLevelFunding {
t.Errorf("expected '%v received '%v'", true, false)
}
if f.disableUSDTracking {
t.Errorf("expected '%v received '%v'", false, true)
}
- f = SetupFundingManager(false, true)
+ f, err = SetupFundingManager(&engine.ExchangeManager{}, false, true)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
if f.usingExchangeLevelFunding {
t.Errorf("expected '%v received '%v'", false, true)
}
@@ -45,8 +73,11 @@ func TestSetupFundingManager(t *testing.T) {
func TestReset(t *testing.T) {
t.Parallel()
- f := SetupFundingManager(true, false)
- baseItem, err := CreateItem(exch, a, base, decimal.Zero, decimal.Zero)
+ f, err := SetupFundingManager(&engine.ExchangeManager{}, true, false)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem, err := CreateItem(exchName, a, base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -65,7 +96,10 @@ func TestReset(t *testing.T) {
func TestIsUsingExchangeLevelFunding(t *testing.T) {
t.Parallel()
- f := SetupFundingManager(true, false)
+ f, err := SetupFundingManager(&engine.ExchangeManager{}, true, false)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
if !f.IsUsingExchangeLevelFunding() {
t.Errorf("expected '%v received '%v'", true, false)
}
@@ -114,7 +148,7 @@ func TestTransfer(t *testing.T) {
if !item2.available.Equal(elite) {
t.Errorf("received '%v' expected '%v'", item2.available, elite)
}
- if !item1.available.Equal(decimal.Zero) {
+ if !item1.available.IsZero() {
t.Errorf("received '%v' expected '%v'", item1.available, decimal.Zero)
}
@@ -136,7 +170,7 @@ func TestAddItem(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- baseItem, err := CreateItem(exch, a, base, decimal.Zero, decimal.Zero)
+ baseItem, err := CreateItem(exchName, a, base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -154,11 +188,10 @@ func TestAddItem(t *testing.T) {
func TestExists(t *testing.T) {
t.Parallel()
f := FundManager{}
- exists := f.Exists(nil)
- if exists {
- t.Errorf("received '%v' expected '%v'", exists, false)
+ if f.Exists(nil) {
+ t.Errorf("received '%v' expected '%v'", true, false)
}
- conflictingSingleItem, err := CreateItem(exch, a, base, decimal.Zero, decimal.Zero)
+ conflictingSingleItem, err := CreateItem(exchName, a, base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -166,15 +199,14 @@ func TestExists(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- exists = f.Exists(conflictingSingleItem)
- if !exists {
- t.Errorf("received '%v' expected '%v'", exists, true)
+ if !f.Exists(conflictingSingleItem) {
+ t.Errorf("received '%v' expected '%v'", false, true)
}
- baseItem, err := CreateItem(exch, a, base, decimal.Zero, decimal.Zero)
+ baseItem, err := CreateItem(exchName, a, base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- quoteItem, err := CreateItem(exch, a, quote, elite, decimal.Zero)
+ quoteItem, err := CreateItem(exchName, a, quote, elite, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -186,34 +218,51 @@ func TestExists(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- pairItems, err := f.GetFundingForEAP(exch, a, pair)
+ _, err = f.getFundingForEAP(exchName, a, pair)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- exists = f.Exists(pairItems.Base)
- if !exists {
- t.Errorf("received '%v' expected '%v'", exists, true)
- }
- exists = f.Exists(pairItems.Quote)
- if !exists {
- t.Errorf("received '%v' expected '%v'", exists, true)
- }
- funds, err := f.GetFundingForEAP(exch, a, pair)
+ _, err = f.getFundingForEAP(exchName, a, pair)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
// demonstration that you don't need the original *Items
// to check for existence, just matching fields
- baseCopy := *funds.Base
- quoteCopy := *funds.Quote
+ baseCopy := Item{
+ exchange: baseItem.exchange,
+ asset: baseItem.asset,
+ currency: baseItem.currency,
+ initialFunds: baseItem.initialFunds,
+ available: baseItem.available,
+ reserved: baseItem.reserved,
+ transferFee: baseItem.transferFee,
+ pairedWith: baseItem.pairedWith,
+ trackingCandles: baseItem.trackingCandles,
+ snapshot: baseItem.snapshot,
+ isCollateral: baseItem.isCollateral,
+ collateralCandles: baseItem.collateralCandles,
+ }
+ quoteCopy := Item{
+ exchange: quoteItem.exchange,
+ asset: quoteItem.asset,
+ currency: quoteItem.currency,
+ initialFunds: quoteItem.initialFunds,
+ available: quoteItem.available,
+ reserved: quoteItem.reserved,
+ transferFee: quoteItem.transferFee,
+ pairedWith: quoteItem.pairedWith,
+ trackingCandles: quoteItem.trackingCandles,
+ snapshot: quoteItem.snapshot,
+ isCollateral: quoteItem.isCollateral,
+ collateralCandles: quoteItem.collateralCandles,
+ }
quoteCopy.pairedWith = &baseCopy
- exists = f.Exists(&baseCopy)
- if !exists {
- t.Errorf("received '%v' expected '%v'", exists, true)
+ if !f.Exists(&baseCopy) {
+ t.Errorf("received '%v' expected '%v'", false, true)
}
- currFunds, err := f.GetFundingForEAC(exch, a, base)
+ currFunds, err := f.getFundingForEAC(exchName, a, base)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -225,11 +274,11 @@ func TestExists(t *testing.T) {
func TestAddPair(t *testing.T) {
t.Parallel()
f := FundManager{}
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -241,33 +290,10 @@ func TestAddPair(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- resp, err := f.GetFundingForEAP(exch, a, pair)
+ _, err = f.getFundingForEAP(exchName, a, pair)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- if resp.Base.exchange != exch ||
- resp.Base.asset != a ||
- !resp.Base.currency.Equal(pair.Base) {
- t.Error("woah nelly")
- }
- if resp.Quote.exchange != exch ||
- resp.Quote.asset != a ||
- !resp.Quote.currency.Equal(pair.Quote) {
- t.Error("woah nelly")
- }
- if resp.Quote.pairedWith != resp.Base {
- t.Errorf("received '%v' expected '%v'", resp.Base, resp.Quote.pairedWith)
- }
- if resp.Base.pairedWith != resp.Quote {
- t.Errorf("received '%v' expected '%v'", resp.Quote, resp.Base.pairedWith)
- }
- if !resp.Base.initialFunds.Equal(decimal.Zero) {
- t.Errorf("received '%v' expected '%v'", resp.Base.initialFunds, decimal.Zero)
- }
- if !resp.Quote.initialFunds.Equal(elite) {
- t.Errorf("received '%v' expected '%v'", resp.Quote.initialFunds, elite)
- }
-
p, err = CreatePair(baseItem, quoteItem)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
@@ -278,21 +304,6 @@ func TestAddPair(t *testing.T) {
}
}
-// fakeEvent implements common.EventHandler without
-// caring about the response, or dealing with import cycles
-type fakeEvent struct{}
-
-func (f *fakeEvent) GetOffset() int64 { return 0 }
-func (f *fakeEvent) SetOffset(int64) {}
-func (f *fakeEvent) IsEvent() bool { return true }
-func (f *fakeEvent) GetTime() time.Time { return time.Now() }
-func (f *fakeEvent) Pair() currency.Pair { return pair }
-func (f *fakeEvent) GetExchange() string { return exch }
-func (f *fakeEvent) GetInterval() gctkline.Interval { return gctkline.OneMin }
-func (f *fakeEvent) GetAssetType() asset.Item { return asset.Spot }
-func (f *fakeEvent) GetReason() string { return "" }
-func (f *fakeEvent) AppendReason(string) {}
-
func TestGetFundingForEvent(t *testing.T) {
t.Parallel()
e := &fakeEvent{}
@@ -301,11 +312,11 @@ func TestGetFundingForEvent(t *testing.T) {
if !errors.Is(err, ErrFundsNotFound) {
t.Errorf("received '%v' expected '%v'", err, ErrFundsNotFound)
}
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -326,11 +337,11 @@ func TestGetFundingForEvent(t *testing.T) {
func TestGetFundingForEAC(t *testing.T) {
t.Parallel()
f := FundManager{}
- _, err := f.GetFundingForEAC(exch, a, base)
+ _, err := f.getFundingForEAC(exchName, a, base)
if !errors.Is(err, ErrFundsNotFound) {
t.Errorf("received '%v' expected '%v'", err, ErrFundsNotFound)
}
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -339,7 +350,7 @@ func TestGetFundingForEAC(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- fundo, err := f.GetFundingForEAC(exch, a, base)
+ fundo, err := f.getFundingForEAC(exchName, a, base)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -352,15 +363,15 @@ func TestGetFundingForEAC(t *testing.T) {
func TestGetFundingForEAP(t *testing.T) {
t.Parallel()
f := FundManager{}
- _, err := f.GetFundingForEAP(exch, a, pair)
+ _, err := f.getFundingForEAP(exchName, a, pair)
if !errors.Is(err, ErrFundsNotFound) {
t.Errorf("received '%v' expected '%v'", err, ErrFundsNotFound)
}
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -372,7 +383,7 @@ func TestGetFundingForEAP(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- _, err = f.GetFundingForEAP(exch, a, pair)
+ _, err = f.getFundingForEAP(exchName, a, pair)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -395,364 +406,6 @@ func TestGetFundingForEAP(t *testing.T) {
}
}
-func TestBaseInitialFunds(t *testing.T) {
- t.Parallel()
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- baseItem.pairedWith = quoteItem
- quoteItem.pairedWith = baseItem
- pairItems := Pair{Base: baseItem, Quote: quoteItem}
- funds := pairItems.BaseInitialFunds()
- if !funds.IsZero() {
- t.Errorf("received '%v' expected '%v'", funds, baseItem.available)
- }
-}
-
-func TestQuoteInitialFunds(t *testing.T) {
- t.Parallel()
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- baseItem.pairedWith = quoteItem
- quoteItem.pairedWith = baseItem
- pairItems := Pair{Base: baseItem, Quote: quoteItem}
- funds := pairItems.QuoteInitialFunds()
- if !funds.Equal(elite) {
- t.Errorf("received '%v' expected '%v'", funds, elite)
- }
-}
-
-func TestBaseAvailable(t *testing.T) {
- t.Parallel()
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- baseItem.pairedWith = quoteItem
- quoteItem.pairedWith = baseItem
- pairItems := Pair{Base: baseItem, Quote: quoteItem}
- funds := pairItems.BaseAvailable()
- if !funds.IsZero() {
- t.Errorf("received '%v' expected '%v'", funds, baseItem.available)
- }
-}
-
-func TestQuoteAvailable(t *testing.T) {
- t.Parallel()
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- baseItem.pairedWith = quoteItem
- quoteItem.pairedWith = baseItem
- pairItems := Pair{Base: baseItem, Quote: quoteItem}
- funds := pairItems.QuoteAvailable()
- if !funds.Equal(elite) {
- t.Errorf("received '%v' expected '%v'", funds, elite)
- }
-}
-
-func TestReservePair(t *testing.T) {
- t.Parallel()
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- baseItem.pairedWith = quoteItem
- quoteItem.pairedWith = baseItem
- pairItems := Pair{Base: baseItem, Quote: quoteItem}
- err = pairItems.Reserve(decimal.Zero, order.Buy)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = pairItems.Reserve(elite, order.Buy)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- err = pairItems.Reserve(decimal.Zero, order.Sell)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = pairItems.Reserve(elite, order.Sell)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
- err = pairItems.Reserve(elite, order.DoNothing)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
-}
-
-func TestReleasePair(t *testing.T) {
- t.Parallel()
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- baseItem.pairedWith = quoteItem
- quoteItem.pairedWith = baseItem
- pairItems := Pair{Base: baseItem, Quote: quoteItem}
- err = pairItems.Reserve(decimal.Zero, order.Buy)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = pairItems.Reserve(elite, order.Buy)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- err = pairItems.Reserve(decimal.Zero, order.Sell)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = pairItems.Reserve(elite, order.Sell)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
-
- err = pairItems.Release(decimal.Zero, decimal.Zero, order.Buy)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = pairItems.Release(elite, decimal.Zero, order.Buy)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- err = pairItems.Release(elite, decimal.Zero, order.Buy)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
-
- err = pairItems.Release(elite, decimal.Zero, order.DoNothing)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
-
- err = pairItems.Release(elite, decimal.Zero, order.Sell)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
- err = pairItems.Release(decimal.Zero, decimal.Zero, order.Sell)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
-}
-
-func TestIncreaseAvailablePair(t *testing.T) {
- t.Parallel()
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- baseItem.pairedWith = quoteItem
- quoteItem.pairedWith = baseItem
- pairItems := Pair{Base: baseItem, Quote: quoteItem}
- pairItems.IncreaseAvailable(decimal.Zero, order.Buy)
- if !pairItems.Quote.available.Equal(elite) {
- t.Errorf("received '%v' expected '%v'", elite, pairItems.Quote.available)
- }
- pairItems.IncreaseAvailable(decimal.Zero, order.Sell)
- if !pairItems.Base.available.Equal(decimal.Zero) {
- t.Errorf("received '%v' expected '%v'", decimal.Zero, pairItems.Base.available)
- }
-
- pairItems.IncreaseAvailable(elite.Neg(), order.Sell)
- if !pairItems.Quote.available.Equal(elite) {
- t.Errorf("received '%v' expected '%v'", elite, pairItems.Quote.available)
- }
- pairItems.IncreaseAvailable(elite, order.Buy)
- if !pairItems.Base.available.Equal(elite) {
- t.Errorf("received '%v' expected '%v'", elite, pairItems.Base.available)
- }
-
- pairItems.IncreaseAvailable(elite, order.DoNothing)
- if !pairItems.Base.available.Equal(elite) {
- t.Errorf("received '%v' expected '%v'", elite, pairItems.Base.available)
- }
-}
-
-func TestCanPlaceOrderPair(t *testing.T) {
- t.Parallel()
- p := Pair{
- Base: &Item{},
- Quote: &Item{},
- }
- if p.CanPlaceOrder(order.DoNothing) {
- t.Error("expected false")
- }
- if p.CanPlaceOrder(order.Buy) {
- t.Error("expected false")
- }
- if p.CanPlaceOrder(order.Sell) {
- t.Error("expected false")
- }
-
- p.Quote.available = decimal.NewFromInt(32)
- if !p.CanPlaceOrder(order.Buy) {
- t.Error("expected true")
- }
- p.Base.available = decimal.NewFromInt(32)
- if !p.CanPlaceOrder(order.Sell) {
- t.Error("expected true")
- }
-}
-
-func TestIncreaseAvailable(t *testing.T) {
- t.Parallel()
- i := Item{}
- i.IncreaseAvailable(elite)
- if !i.available.Equal(elite) {
- t.Errorf("expected %v", elite)
- }
- i.IncreaseAvailable(decimal.Zero)
- i.IncreaseAvailable(neg)
- if !i.available.Equal(elite) {
- t.Errorf("expected %v", elite)
- }
-}
-
-func TestRelease(t *testing.T) {
- t.Parallel()
- i := Item{}
- err := i.Release(decimal.Zero, decimal.Zero)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = i.Release(elite, decimal.Zero)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
- i.reserved = elite
- err = i.Release(elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
-
- i.reserved = elite
- err = i.Release(elite, one)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
-
- err = i.Release(neg, decimal.Zero)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = i.Release(elite, neg)
- if !errors.Is(err, errNegativeAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errNegativeAmountReceived)
- }
-}
-
-func TestReserve(t *testing.T) {
- t.Parallel()
- i := Item{}
- err := i.Reserve(decimal.Zero)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
- err = i.Reserve(elite)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
-
- i.reserved = elite
- err = i.Reserve(elite)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
-
- i.available = elite
- err = i.Reserve(elite)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
-
- err = i.Reserve(elite)
- if !errors.Is(err, errCannotAllocate) {
- t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
- }
-
- err = i.Reserve(neg)
- if !errors.Is(err, errZeroAmountReceived) {
- t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
- }
-}
-
-func TestMatchesItemCurrency(t *testing.T) {
- t.Parallel()
- i := Item{}
- if i.MatchesItemCurrency(nil) {
- t.Errorf("received '%v' expected '%v'", true, false)
- }
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- if baseItem.MatchesItemCurrency(quoteItem) {
- t.Errorf("received '%v' expected '%v'", true, false)
- }
- if !baseItem.MatchesItemCurrency(baseItem) {
- t.Errorf("received '%v' expected '%v'", false, true)
- }
-}
-
-func TestMatchesExchange(t *testing.T) {
- t.Parallel()
- i := Item{}
- if i.MatchesExchange(nil) {
- t.Errorf("received '%v' expected '%v'", true, false)
- }
- baseItem, err := CreateItem(exch, a, pair.Base, decimal.Zero, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
- if !errors.Is(err, nil) {
- t.Errorf("received '%v' expected '%v'", err, nil)
- }
- if !baseItem.MatchesExchange(quoteItem) {
- t.Errorf("received '%v' expected '%v'", false, true)
- }
- if !baseItem.MatchesExchange(baseItem) {
- t.Errorf("received '%v' expected '%v'", false, true)
- }
-}
-
func TestGenerateReport(t *testing.T) {
t.Parallel()
f := FundManager{}
@@ -764,7 +417,7 @@ func TestGenerateReport(t *testing.T) {
t.Error("expected 0")
}
item := &Item{
- exchange: exch,
+ exchange: exchName,
initialFunds: decimal.NewFromInt(100),
available: decimal.NewFromInt(200),
currency: currency.BTC,
@@ -784,7 +437,7 @@ func TestGenerateReport(t *testing.T) {
f.usingExchangeLevelFunding = true
err = f.AddItem(&Item{
- exchange: exch,
+ exchange: exchName,
initialFunds: decimal.NewFromInt(100),
available: decimal.NewFromInt(200),
currency: currency.USD,
@@ -796,7 +449,7 @@ func TestGenerateReport(t *testing.T) {
dfk := &kline.DataFromKline{
Item: gctkline.Item{
- Exchange: exch,
+ Exchange: exchName,
Pair: currency.NewPair(currency.BTC, currency.USD),
Asset: a,
Interval: gctkline.OneHour,
@@ -815,7 +468,7 @@ func TestGenerateReport(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- f.items[0].usdTrackingCandles = dfk
+ f.items[0].trackingCandles = dfk
f.CreateSnapshot(dfk.Item.Candles[0].Time)
report = f.GenerateReport()
@@ -830,26 +483,8 @@ func TestGenerateReport(t *testing.T) {
}
}
-func TestMatchesCurrency(t *testing.T) {
- t.Parallel()
- i := Item{
- currency: currency.BTC,
- }
- if i.MatchesCurrency(currency.USDT) {
- t.Error("expected false")
- }
- if !i.MatchesCurrency(currency.BTC) {
- t.Error("expected true")
- }
- if i.MatchesCurrency(currency.EMPTYCODE) {
- t.Error("expected false")
- }
- if i.MatchesCurrency(currency.NewCode("")) {
- t.Error("expected false")
- }
-}
-
func TestCreateSnapshot(t *testing.T) {
+ t.Parallel()
f := FundManager{}
f.CreateSnapshot(time.Time{})
f.items = append(f.items, &Item{})
@@ -868,19 +503,20 @@ func TestCreateSnapshot(t *testing.T) {
t.Error(err)
}
f.items = append(f.items, &Item{
- exchange: "test",
- asset: asset.Spot,
- currency: currency.BTC,
- initialFunds: decimal.NewFromInt(1337),
- available: decimal.NewFromInt(1337),
- reserved: decimal.NewFromInt(1337),
- transferFee: decimal.NewFromInt(1337),
- usdTrackingCandles: dfk,
+ exchange: "test",
+ asset: asset.Spot,
+ currency: currency.BTC,
+ initialFunds: decimal.NewFromInt(1337),
+ available: decimal.NewFromInt(1337),
+ reserved: decimal.NewFromInt(1337),
+ transferFee: decimal.NewFromInt(1337),
+ trackingCandles: dfk,
})
f.CreateSnapshot(dfk.Item.Candles[0].Time)
}
func TestAddUSDTrackingData(t *testing.T) {
+ t.Parallel()
f := FundManager{}
err := f.AddUSDTrackingData(nil)
if !errors.Is(err, common.ErrNilArguments) {
@@ -905,7 +541,7 @@ func TestAddUSDTrackingData(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -928,7 +564,7 @@ func TestAddUSDTrackingData(t *testing.T) {
dfk = &kline.DataFromKline{
Item: gctkline.Item{
- Exchange: exch,
+ Exchange: exchName,
Pair: currency.NewPair(pair.Quote, currency.USD),
Asset: a,
Interval: gctkline.OneHour,
@@ -948,7 +584,7 @@ func TestAddUSDTrackingData(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
- usdtItem, err := CreateItem(exch, a, currency.USDT, elite, decimal.Zero)
+ usdtItem, err := CreateItem(exchName, a, currency.USDT, elite, decimal.Zero)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -963,6 +599,7 @@ func TestAddUSDTrackingData(t *testing.T) {
}
func TestUSDTrackingDisabled(t *testing.T) {
+ t.Parallel()
f := FundManager{}
if f.USDTrackingDisabled() {
t.Error("received true, expected false")
@@ -972,3 +609,259 @@ func TestUSDTrackingDisabled(t *testing.T) {
t.Error("received false, expected true")
}
}
+
+func TestFundingLiquidate(t *testing.T) {
+ t.Parallel()
+ f := FundManager{}
+ f.Liquidate(nil)
+ f.items = append(f.items, &Item{
+ exchange: "test",
+ asset: asset.Spot,
+ currency: currency.BTC,
+ available: decimal.NewFromInt(1337),
+ })
+
+ f.Liquidate(&signal.Signal{
+ Base: &event.Base{
+ Exchange: "test",
+ AssetType: asset.Spot,
+ CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
+ },
+ })
+ if !f.items[0].available.IsZero() {
+ t.Errorf("received '%v' expected '%v'", f.items[0].available, "0")
+ }
+}
+
+func TestHasExchangeBeenLiquidated(t *testing.T) {
+ t.Parallel()
+ f := FundManager{}
+ f.Liquidate(nil)
+ f.items = append(f.items, &Item{
+ exchange: "test",
+ asset: asset.Spot,
+ currency: currency.BTC,
+ available: decimal.NewFromInt(1337),
+ })
+ ev := &signal.Signal{
+ Base: &event.Base{
+ Exchange: "test",
+ AssetType: asset.Spot,
+ CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
+ },
+ }
+ f.Liquidate(ev)
+ if !f.items[0].available.IsZero() {
+ t.Errorf("received '%v' expected '%v'", f.items[0].available, "0")
+ }
+ if has := f.HasExchangeBeenLiquidated(ev); !has {
+ t.Errorf("received '%v' expected '%v'", has, true)
+ }
+}
+
+func TestGetAllFunding(t *testing.T) {
+ t.Parallel()
+ f := FundManager{}
+ resp := f.GetAllFunding()
+ if len(resp) != 0 {
+ t.Errorf("received '%v' expected '%v'", len(resp), 0)
+ }
+
+ f.items = append(f.items, &Item{
+ exchange: "test",
+ asset: asset.Spot,
+ currency: currency.BTC,
+ available: decimal.NewFromInt(1337),
+ })
+
+ resp = f.GetAllFunding()
+ if len(resp) != 1 {
+ t.Errorf("received '%v' expected '%v'", len(resp), 1)
+ }
+}
+
+func TestHasFutures(t *testing.T) {
+ t.Parallel()
+ f := FundManager{}
+ if has := f.HasFutures(); has {
+ t.Errorf("received '%v' expected '%v'", has, false)
+ }
+
+ f.items = append(f.items, &Item{
+ exchange: "test",
+ asset: asset.Futures,
+ currency: currency.BTC,
+ available: decimal.NewFromInt(1337),
+ })
+ if has := f.HasFutures(); !has {
+ t.Errorf("received '%v' expected '%v'", has, true)
+ }
+}
+
+func TestRealisePNL(t *testing.T) {
+ t.Parallel()
+ f := FundManager{}
+ f.items = append(f.items, &Item{
+ exchange: "test",
+ asset: asset.Futures,
+ currency: currency.BTC,
+ available: decimal.NewFromInt(1336),
+ isCollateral: true,
+ })
+
+ var expectedError error
+ err := f.RealisePNL("test", asset.Futures, currency.BTC, decimal.NewFromInt(1))
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if !f.items[0].available.Equal(decimal.NewFromInt(1337)) {
+ t.Errorf("recevied '%v' expected '%v'", f.items[0].available, decimal.NewFromInt(1337))
+ }
+
+ expectedError = ErrFundsNotFound
+ err = f.RealisePNL("test2", asset.Futures, currency.BTC, decimal.NewFromInt(1))
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestCreateCollateral(t *testing.T) {
+ t.Parallel()
+ collat := &Item{
+ exchange: "test",
+ asset: asset.Futures,
+ currency: currency.BTC,
+ available: decimal.NewFromInt(1336),
+ isCollateral: true,
+ }
+ contract := &Item{
+ exchange: "test",
+ asset: asset.Futures,
+ currency: currency.DOGE,
+ available: decimal.NewFromInt(1336),
+ }
+
+ var expectedError error
+ _, err := CreateCollateral(collat, contract)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+
+ expectedError = common.ErrNilArguments
+ _, err = CreateCollateral(nil, contract)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+
+ _, err = CreateCollateral(collat, nil)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestUpdateCollateral(t *testing.T) {
+ t.Parallel()
+ f := &FundManager{}
+ expectedError := common.ErrNilEvent
+ err := f.UpdateCollateral(nil)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+
+ ev := &signal.Signal{
+ Base: &event.Base{
+ Exchange: "ftx",
+ AssetType: asset.Futures,
+ CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
+ },
+ }
+ f.items = append(f.items, &Item{
+ exchange: "ftx",
+ asset: asset.Spot,
+ currency: currency.BTC,
+ available: decimal.NewFromInt(1336),
+ })
+ em := engine.SetupExchangeManager()
+ exch, err := em.NewExchangeByName("ftx")
+ if err != nil {
+ t.Fatal(err)
+ }
+ exch.SetDefaults()
+ cfg, err := exch.GetDefaultConfig()
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = exch.Setup(cfg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ em.Add(exch)
+ f.exchangeManager = em
+
+ expectedError = ErrFundsNotFound
+ err = f.UpdateCollateral(ev)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+
+ expectedError = nil
+ f.items = append(f.items, &Item{
+ exchange: "ftx",
+ asset: asset.Futures,
+ currency: currency.USD,
+ available: decimal.NewFromInt(1336),
+ isCollateral: true,
+ })
+ err = f.UpdateCollateral(ev)
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+}
+
+func TestCreateFuturesCurrencyCode(t *testing.T) {
+ t.Parallel()
+ if result := CreateFuturesCurrencyCode(currency.BTC, currency.USDT); result != currency.NewCode("BTC-USDT") {
+ t.Errorf("received '%v', expected '%v'", result, "BTC-USDT")
+ }
+}
+
+func TestLinkCollateralCurrency(t *testing.T) {
+ t.Parallel()
+ f := FundManager{}
+ err := f.LinkCollateralCurrency(nil, currency.EMPTYCODE)
+ if !errors.Is(err, common.ErrNilArguments) {
+ t.Errorf("received '%v', expected '%v'", err, common.ErrNilArguments)
+ }
+
+ item := &Item{}
+ err = f.LinkCollateralCurrency(item, currency.EMPTYCODE)
+ if !errors.Is(err, common.ErrNilArguments) {
+ t.Errorf("received '%v', expected '%v'", err, common.ErrNilArguments)
+ }
+
+ err = f.LinkCollateralCurrency(item, currency.BTC)
+ if !errors.Is(err, errNotFutures) {
+ t.Errorf("received '%v', expected '%v'", err, errNotFutures)
+ }
+
+ item.asset = asset.Futures
+ err = f.LinkCollateralCurrency(item, currency.BTC)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v', expected '%v'", err, nil)
+ }
+ if !item.pairedWith.currency.Equal(currency.BTC) {
+ t.Errorf("received '%v', expected '%v'", currency.BTC, item.pairedWith.currency)
+ }
+
+ err = f.LinkCollateralCurrency(item, currency.LTC)
+ if !errors.Is(err, ErrAlreadyExists) {
+ t.Errorf("received '%v', expected '%v'", err, ErrAlreadyExists)
+ }
+
+ f.items = append(f.items, item.pairedWith)
+ item.pairedWith = nil
+ err = f.LinkCollateralCurrency(item, currency.BTC)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v', expected '%v'", err, nil)
+ }
+}
diff --git a/backtester/funding/funding_types.go b/backtester/funding/funding_types.go
index 0b54d2b44bc..e30245846cb 100644
--- a/backtester/funding/funding_types.go
+++ b/backtester/funding/funding_types.go
@@ -7,40 +7,74 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/currency"
+ "github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
-// FundManager is the benevolent holder of all funding levels across all
-// currencies used in the backtester
-type FundManager struct {
- usingExchangeLevelFunding bool
- disableUSDTracking bool
- items []*Item
-}
-
// IFundingManager limits funding usage for portfolio event handling
type IFundingManager interface {
Reset()
IsUsingExchangeLevelFunding() bool
- GetFundingForEAC(string, asset.Item, currency.Code) (*Item, error)
- GetFundingForEvent(common.EventHandler) (*Pair, error)
- GetFundingForEAP(string, asset.Item, currency.Pair) (*Pair, error)
+ GetFundingForEvent(common.EventHandler) (IFundingPair, error)
Transfer(decimal.Decimal, *Item, *Item, bool) error
GenerateReport() *Report
AddUSDTrackingData(*kline.DataFromKline) error
CreateSnapshot(time.Time)
USDTrackingDisabled() bool
+ Liquidate(common.EventHandler)
+ GetAllFunding() []BasicItem
+ UpdateCollateral(common.EventHandler) error
+ HasFutures() bool
+ HasExchangeBeenLiquidated(handler common.EventHandler) bool
+ RealisePNL(receivingExchange string, receivingAsset asset.Item, receivingCurrency currency.Code, realisedPNL decimal.Decimal) error
}
-// IFundTransferer allows for funding amounts to be transferred
+// IFundingTransferer allows for funding amounts to be transferred
// implementation can be swapped for live transferring
-type IFundTransferer interface {
+type IFundingTransferer interface {
IsUsingExchangeLevelFunding() bool
Transfer(decimal.Decimal, *Item, *Item, bool) error
- GetFundingForEAC(string, asset.Item, currency.Code) (*Item, error)
- GetFundingForEvent(common.EventHandler) (*Pair, error)
- GetFundingForEAP(string, asset.Item, currency.Pair) (*Pair, error)
+ GetFundingForEvent(common.EventHandler) (IFundingPair, error)
+ HasExchangeBeenLiquidated(handler common.EventHandler) bool
+}
+
+// IFundingReader is a simple interface of
+// IFundingManager for readonly access at portfolio
+// manager
+type IFundingReader interface {
+ GetFundingForEvent(common.EventHandler) (IFundingPair, error)
+ GetAllFunding() []BasicItem
+}
+
+// IFundingPair allows conversion into various
+// funding interfaces
+type IFundingPair interface {
+ FundReader() IFundReader
+ FundReserver() IFundReserver
+ FundReleaser() IFundReleaser
+}
+
+// IFundReader can read
+// either collateral or pair details
+type IFundReader interface {
+ GetPairReader() (IPairReader, error)
+ GetCollateralReader() (ICollateralReader, error)
+}
+
+// IFundReserver limits funding usage for portfolio event handling
+type IFundReserver interface {
+ IFundReader
+ CanPlaceOrder(order.Side) bool
+ Reserve(decimal.Decimal, order.Side) error
+}
+
+// IFundReleaser can read
+// or release pair or collateral funds
+type IFundReleaser interface {
+ IFundReader
+ PairReleaser() (IPairReleaser, error)
+ CollateralReleaser() (ICollateralReleaser, error)
}
// IPairReader is used to limit pair funding functions
@@ -52,37 +86,82 @@ type IPairReader interface {
QuoteAvailable() decimal.Decimal
}
-// IPairReserver limits funding usage for portfolio event handling
-type IPairReserver interface {
- IPairReader
- CanPlaceOrder(order.Side) bool
- Reserve(decimal.Decimal, order.Side) error
+// ICollateralReader is used to read data from
+// collateral pairs
+type ICollateralReader interface {
+ ContractCurrency() currency.Code
+ CollateralCurrency() currency.Code
+ InitialFunds() decimal.Decimal
+ AvailableFunds() decimal.Decimal
+ CurrentHoldings() decimal.Decimal
}
// IPairReleaser limits funding usage for exchange event handling
type IPairReleaser interface {
- IncreaseAvailable(decimal.Decimal, order.Side)
+ IPairReader
+ IncreaseAvailable(decimal.Decimal, order.Side) error
Release(decimal.Decimal, decimal.Decimal, order.Side) error
+ Liquidate()
+}
+
+// ICollateralReleaser limits funding usage for exchange event handling
+type ICollateralReleaser interface {
+ ICollateralReader
+ UpdateContracts(order.Side, decimal.Decimal) error
+ TakeProfit(contracts, positionReturns decimal.Decimal) error
+ ReleaseContracts(decimal.Decimal) error
+ Liquidate()
+}
+
+// FundManager is the benevolent holder of all funding levels across all
+// currencies used in the backtester
+type FundManager struct {
+ usingExchangeLevelFunding bool
+ disableUSDTracking bool
+ items []*Item
+ exchangeManager *engine.ExchangeManager
}
// Item holds funding data per currency item
type Item struct {
- exchange string
- asset asset.Item
- currency currency.Code
- initialFunds decimal.Decimal
- available decimal.Decimal
- reserved decimal.Decimal
- transferFee decimal.Decimal
- pairedWith *Item
- usdTrackingCandles *kline.DataFromKline
- snapshot map[time.Time]ItemSnapshot
+ exchange string
+ asset asset.Item
+ currency currency.Code
+ initialFunds decimal.Decimal
+ available decimal.Decimal
+ reserved decimal.Decimal
+ transferFee decimal.Decimal
+ pairedWith *Item
+ trackingCandles *kline.DataFromKline
+ snapshot map[int64]ItemSnapshot
+ isCollateral bool
+ isLiquidated bool
+ collateralCandles map[currency.Code]kline.DataFromKline
+}
+
+// SpotPair holds two currencies that are associated with each other
+type SpotPair struct {
+ base *Item
+ quote *Item
}
-// Pair holds two currencies that are associated with each other
-type Pair struct {
- Base *Item
- Quote *Item
+// CollateralPair consists of a currency pair for a futures contract
+// and associates it with an addition collateral pair to take funding from
+type CollateralPair struct {
+ currentDirection *order.Side
+ contract *Item
+ collateral *Item
+}
+
+// BasicItem is a representation of Item
+type BasicItem struct {
+ Exchange string
+ Asset asset.Item
+ Currency currency.Code
+ InitialFunds decimal.Decimal
+ Available decimal.Decimal
+ Reserved decimal.Decimal
+ USDPrice decimal.Decimal
}
// Report holds all funding data for result reporting
@@ -90,7 +169,9 @@ type Report struct {
DisableUSDTracking bool
UsingExchangeLevelFunding bool
Items []ReportItem
- USDTotalsOverTime map[time.Time]ItemSnapshot
+ USDTotalsOverTime []ItemSnapshot
+ InitialFunds decimal.Decimal
+ FinalFunds decimal.Decimal
}
// ReportItem holds reporting fields
@@ -106,11 +187,11 @@ type ReportItem struct {
USDFinalFunds decimal.Decimal
USDFinalCostForOne decimal.Decimal
Snapshots []ItemSnapshot
-
- USDPairCandle *kline.DataFromKline
- Difference decimal.Decimal
- ShowInfinite bool
- PairedWith currency.Code
+ USDPairCandle *kline.DataFromKline
+ Difference decimal.Decimal
+ ShowInfinite bool
+ PairedWith currency.Code
+ IsCollateral bool
}
// ItemSnapshot holds USD values to allow for tracking
@@ -120,4 +201,12 @@ type ItemSnapshot struct {
Available decimal.Decimal
USDClosePrice decimal.Decimal
USDValue decimal.Decimal
+ Breakdown []CurrencyContribution
+}
+
+// CurrencyContribution helps breakdown how a USD value
+// determines its number
+type CurrencyContribution struct {
+ Currency currency.Code
+ USDContribution decimal.Decimal
}
diff --git a/backtester/funding/item.go b/backtester/funding/item.go
new file mode 100644
index 00000000000..7da21c86474
--- /dev/null
+++ b/backtester/funding/item.go
@@ -0,0 +1,167 @@
+package funding
+
+import (
+ "fmt"
+
+ "github.com/shopspring/decimal"
+ "github.com/thrasher-corp/gocryptotrader/currency"
+ "github.com/thrasher-corp/gocryptotrader/exchanges/asset"
+)
+
+// Reserve allocates an amount of funds to be used at a later time
+// it prevents multiple events from claiming the same resource
+func (i *Item) Reserve(amount decimal.Decimal) error {
+ if amount.LessThanOrEqual(decimal.Zero) {
+ return errZeroAmountReceived
+ }
+ if amount.GreaterThan(i.available) {
+ return fmt.Errorf("%w for %v %v %v. Requested %v Available: %v",
+ errCannotAllocate,
+ i.exchange,
+ i.asset,
+ i.currency,
+ amount,
+ i.available)
+ }
+ i.available = i.available.Sub(amount)
+ i.reserved = i.reserved.Add(amount)
+ return nil
+}
+
+// Release reduces the amount of funding reserved and adds any difference
+// back to the available amount
+func (i *Item) Release(amount, diff decimal.Decimal) error {
+ if amount.LessThanOrEqual(decimal.Zero) {
+ return errZeroAmountReceived
+ }
+ if diff.IsNegative() && !i.asset.IsFutures() {
+ return fmt.Errorf("%w diff %v", errNegativeAmountReceived, diff)
+ }
+ if amount.GreaterThan(i.reserved) {
+ return fmt.Errorf("%w for %v %v %v. Requested %v Reserved: %v",
+ errCannotAllocate,
+ i.exchange,
+ i.asset,
+ i.currency,
+ amount,
+ i.reserved)
+ }
+ i.reserved = i.reserved.Sub(amount)
+ i.available = i.available.Add(diff)
+ return nil
+}
+
+// IncreaseAvailable adds funding to the available amount
+func (i *Item) IncreaseAvailable(amount decimal.Decimal) error {
+ if amount.IsNegative() || amount.IsZero() {
+ return fmt.Errorf("%w amount <= zero", errZeroAmountReceived)
+ }
+ i.available = i.available.Add(amount)
+ return nil
+}
+
+// CanPlaceOrder checks if the item has any funds available
+func (i *Item) CanPlaceOrder() bool {
+ return i.available.GreaterThan(decimal.Zero)
+}
+
+// Equal checks for equality via an Item to compare to
+func (i *Item) Equal(item *Item) bool {
+ if i == nil && item == nil {
+ return true
+ }
+ if item == nil || i == nil {
+ return false
+ }
+ if i.currency == item.currency &&
+ i.asset == item.asset &&
+ i.exchange == item.exchange {
+ if i.pairedWith == nil && item.pairedWith == nil {
+ return true
+ }
+ if i.pairedWith == nil || item.pairedWith == nil {
+ return false
+ }
+ if i.pairedWith.currency == item.pairedWith.currency &&
+ i.pairedWith.asset == item.pairedWith.asset &&
+ i.pairedWith.exchange == item.pairedWith.exchange {
+ return true
+ }
+ }
+ return false
+}
+
+// BasicEqual checks for equality via passed in values
+func (i *Item) BasicEqual(exch string, a asset.Item, currency, pairedCurrency currency.Code) bool {
+ return i != nil &&
+ i.exchange == exch &&
+ i.asset == a &&
+ i.currency.Equal(currency) &&
+ (i.pairedWith == nil ||
+ (i.pairedWith != nil && i.pairedWith.currency.Equal(pairedCurrency)))
+}
+
+// MatchesCurrency checks that an item's currency is equal
+func (i *Item) MatchesCurrency(c currency.Code) bool {
+ return i != nil && i.currency.Equal(c)
+}
+
+// MatchesItemCurrency checks that an item's currency is equal
+func (i *Item) MatchesItemCurrency(item *Item) bool {
+ return i != nil && item != nil && i.currency.Equal(item.currency)
+}
+
+// MatchesExchange checks that an item's exchange is equal
+func (i *Item) MatchesExchange(item *Item) bool {
+ return i != nil && item != nil && i.exchange == item.exchange
+}
+
+// TakeProfit increases/decreases available funds for a futures collateral item
+func (i *Item) TakeProfit(amount decimal.Decimal) error {
+ if i.asset.IsFutures() && !i.isCollateral {
+ return fmt.Errorf("%v %v %v %w cannot add profit to contracts", i.exchange, i.asset, i.currency, ErrNotCollateral)
+ }
+ i.available = i.available.Add(amount)
+ return nil
+}
+
+// AddContracts allocates an amount of funds to be used at a later time
+// it prevents multiple events from claiming the same resource
+func (i *Item) AddContracts(amount decimal.Decimal) error {
+ if !i.asset.IsFutures() {
+ return fmt.Errorf("%v %v %v %w", i.exchange, i.asset, i.currency, errNotFutures)
+ }
+ if i.isCollateral {
+ return fmt.Errorf("%v %v %v %w cannot add contracts to collateral", i.exchange, i.asset, i.currency, ErrIsCollateral)
+ }
+ if amount.LessThanOrEqual(decimal.Zero) {
+ return errZeroAmountReceived
+ }
+ i.available = i.available.Add(amount)
+ return nil
+}
+
+// ReduceContracts allocates an amount of funds to be used at a later time
+// it prevents multiple events from claiming the same resource
+func (i *Item) ReduceContracts(amount decimal.Decimal) error {
+ if !i.asset.IsFutures() {
+ return fmt.Errorf("%v %v %v %w", i.exchange, i.asset, i.currency, errNotFutures)
+ }
+ if i.isCollateral {
+ return fmt.Errorf("%v %v %v %w cannot add contracts to collateral", i.exchange, i.asset, i.currency, ErrIsCollateral)
+ }
+ if amount.LessThanOrEqual(decimal.Zero) {
+ return errZeroAmountReceived
+ }
+ if amount.GreaterThan(i.available) {
+ return fmt.Errorf("%w for %v %v %v. Requested %v Reserved: %v",
+ errCannotAllocate,
+ i.exchange,
+ i.asset,
+ i.currency,
+ amount,
+ i.reserved)
+ }
+ i.available = i.available.Sub(amount)
+ return nil
+}
diff --git a/backtester/funding/item_test.go b/backtester/funding/item_test.go
new file mode 100644
index 00000000000..82d97158180
--- /dev/null
+++ b/backtester/funding/item_test.go
@@ -0,0 +1,160 @@
+package funding
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/shopspring/decimal"
+ "github.com/thrasher-corp/gocryptotrader/currency"
+)
+
+func TestMatchesExchange(t *testing.T) {
+ t.Parallel()
+ i := Item{}
+ if i.MatchesExchange(nil) {
+ t.Errorf("received '%v' expected '%v'", true, false)
+ }
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ if !baseItem.MatchesExchange(quoteItem) {
+ t.Errorf("received '%v' expected '%v'", false, true)
+ }
+ if !baseItem.MatchesExchange(baseItem) {
+ t.Errorf("received '%v' expected '%v'", false, true)
+ }
+}
+
+func TestMatchesItemCurrency(t *testing.T) {
+ t.Parallel()
+ i := Item{}
+ if i.MatchesItemCurrency(nil) {
+ t.Errorf("received '%v' expected '%v'", true, false)
+ }
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ if baseItem.MatchesItemCurrency(quoteItem) {
+ t.Errorf("received '%v' expected '%v'", true, false)
+ }
+ if !baseItem.MatchesItemCurrency(baseItem) {
+ t.Errorf("received '%v' expected '%v'", false, true)
+ }
+}
+
+func TestReserve(t *testing.T) {
+ t.Parallel()
+ i := Item{}
+ err := i.Reserve(decimal.Zero)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = i.Reserve(elite)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+
+ i.reserved = elite
+ err = i.Reserve(elite)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+
+ i.available = elite
+ err = i.Reserve(elite)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+
+ err = i.Reserve(elite)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+
+ err = i.Reserve(neg)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+}
+
+func TestIncreaseAvailable(t *testing.T) {
+ t.Parallel()
+ i := Item{}
+ err := i.IncreaseAvailable(elite)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ if !i.available.Equal(elite) {
+ t.Errorf("expected %v", elite)
+ }
+ err = i.IncreaseAvailable(decimal.Zero)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = i.IncreaseAvailable(neg)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+}
+
+func TestRelease(t *testing.T) {
+ t.Parallel()
+ i := Item{}
+ err := i.Release(decimal.Zero, decimal.Zero)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = i.Release(elite, decimal.Zero)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+ i.reserved = elite
+ err = i.Release(elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+
+ i.reserved = elite
+ err = i.Release(elite, one)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+
+ err = i.Release(neg, decimal.Zero)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = i.Release(elite, neg)
+ if !errors.Is(err, errNegativeAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errNegativeAmountReceived)
+ }
+}
+
+func TestMatchesCurrency(t *testing.T) {
+ t.Parallel()
+ i := Item{
+ currency: currency.BTC,
+ }
+ if i.MatchesCurrency(currency.USDT) {
+ t.Error("expected false")
+ }
+ if !i.MatchesCurrency(currency.BTC) {
+ t.Error("expected true")
+ }
+ if i.MatchesCurrency(currency.EMPTYCODE) {
+ t.Error("expected false")
+ }
+ if i.MatchesCurrency(currency.NewCode("")) {
+ t.Error("expected false")
+ }
+}
diff --git a/backtester/funding/spotpair.go b/backtester/funding/spotpair.go
new file mode 100644
index 00000000000..17595eb0142
--- /dev/null
+++ b/backtester/funding/spotpair.go
@@ -0,0 +1,152 @@
+package funding
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/shopspring/decimal"
+ "github.com/thrasher-corp/gocryptotrader/exchanges/order"
+)
+
+var (
+ // ErrNotCollateral is returned when a user requests collateral pair details when it is a funding pair
+ ErrNotCollateral = errors.New("not a collateral pair")
+)
+
+// BaseInitialFunds returns the initial funds
+// from the base in a currency pair
+func (p *SpotPair) BaseInitialFunds() decimal.Decimal {
+ return p.base.initialFunds
+}
+
+// QuoteInitialFunds returns the initial funds
+// from the quote in a currency pair
+func (p *SpotPair) QuoteInitialFunds() decimal.Decimal {
+ return p.quote.initialFunds
+}
+
+// BaseAvailable returns the available funds
+// from the base in a currency pair
+func (p *SpotPair) BaseAvailable() decimal.Decimal {
+ return p.base.available
+}
+
+// QuoteAvailable returns the available funds
+// from the quote in a currency pair
+func (p *SpotPair) QuoteAvailable() decimal.Decimal {
+ return p.quote.available
+}
+
+// Reserve allocates an amount of funds to be used at a later time
+// it prevents multiple events from claiming the same resource
+// changes which currency to affect based on the order side
+func (p *SpotPair) Reserve(amount decimal.Decimal, side order.Side) error {
+ switch side {
+ case order.Buy, order.Bid:
+ return p.quote.Reserve(amount)
+ case order.Sell, order.Ask, order.ClosePosition:
+ return p.base.Reserve(amount)
+ default:
+ return fmt.Errorf("%w for %v %v %v. Unknown side %v",
+ errCannotAllocate,
+ p.base.exchange,
+ p.base.asset,
+ p.base.currency,
+ side)
+ }
+}
+
+// Release reduces the amount of funding reserved and adds any difference
+// back to the available amount
+// changes which currency to affect based on the order side
+func (p *SpotPair) Release(amount, diff decimal.Decimal, side order.Side) error {
+ switch side {
+ case order.Buy, order.Bid:
+ return p.quote.Release(amount, diff)
+ case order.Sell, order.Ask:
+ return p.base.Release(amount, diff)
+ }
+ return fmt.Errorf("%w for %v %v %v. Unknown side %v",
+ errCannotAllocate,
+ p.base.exchange,
+ p.base.asset,
+ p.base.currency,
+ side)
+}
+
+// IncreaseAvailable adds funding to the available amount
+// changes which currency to affect based on the order side
+func (p *SpotPair) IncreaseAvailable(amount decimal.Decimal, side order.Side) error {
+ switch side {
+ case order.Buy, order.Bid:
+ return p.base.IncreaseAvailable(amount)
+ case order.Sell, order.Ask, order.ClosePosition:
+ return p.quote.IncreaseAvailable(amount)
+ }
+ return fmt.Errorf("%w for %v %v %v. Unknown side %v",
+ errCannotAllocate,
+ p.base.exchange,
+ p.base.asset,
+ p.base.currency,
+ side)
+}
+
+// CanPlaceOrder does a > 0 check to see if there are any funds
+// to place an order with
+// changes which currency to affect based on the order side
+func (p *SpotPair) CanPlaceOrder(side order.Side) bool {
+ switch side {
+ case order.Buy, order.Bid:
+ return p.quote.CanPlaceOrder()
+ case order.Sell, order.Ask, order.ClosePosition:
+ return p.base.CanPlaceOrder()
+ }
+ return false
+}
+
+// Liquidate basic liquidation response to remove
+// all asset value
+func (p *SpotPair) Liquidate() {
+ p.base.available = decimal.Zero
+ p.base.reserved = decimal.Zero
+ p.quote.available = decimal.Zero
+ p.quote.reserved = decimal.Zero
+}
+
+// FundReserver returns a fund reserver interface of the pair
+func (p *SpotPair) FundReserver() IFundReserver {
+ return p
+}
+
+// PairReleaser returns a pair releaser interface of the pair
+func (p *SpotPair) PairReleaser() (IPairReleaser, error) {
+ if p == nil {
+ return nil, ErrNilPair
+ }
+ return p, nil
+}
+
+// CollateralReleaser returns an error because a pair is not collateral
+func (p *SpotPair) CollateralReleaser() (ICollateralReleaser, error) {
+ return nil, ErrNotCollateral
+}
+
+// FundReleaser returns a pair releaser interface of the pair
+func (p *SpotPair) FundReleaser() IFundReleaser {
+ return p
+}
+
+// FundReader returns a fund reader interface of the pair
+func (p *SpotPair) FundReader() IFundReader {
+ return p
+}
+
+// GetPairReader returns an interface of a SpotPair
+func (p *SpotPair) GetPairReader() (IPairReader, error) {
+ return p, nil
+}
+
+// GetCollateralReader returns an error because its not collateral
+func (p *SpotPair) GetCollateralReader() (ICollateralReader, error) {
+ return nil, ErrNotCollateral
+}
diff --git a/backtester/funding/spotpair_test.go b/backtester/funding/spotpair_test.go
new file mode 100644
index 00000000000..f0de925c4b7
--- /dev/null
+++ b/backtester/funding/spotpair_test.go
@@ -0,0 +1,350 @@
+package funding
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/shopspring/decimal"
+ gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
+)
+
+func TestBaseInitialFunds(t *testing.T) {
+ t.Parallel()
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem.pairedWith = quoteItem
+ quoteItem.pairedWith = baseItem
+ pairItems := SpotPair{base: baseItem, quote: quoteItem}
+ funds := pairItems.BaseInitialFunds()
+ if !funds.IsZero() {
+ t.Errorf("received '%v' expected '%v'", funds, baseItem.available)
+ }
+}
+
+func TestQuoteInitialFunds(t *testing.T) {
+ t.Parallel()
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem.pairedWith = quoteItem
+ quoteItem.pairedWith = baseItem
+ pairItems := SpotPair{base: baseItem, quote: quoteItem}
+ funds := pairItems.QuoteInitialFunds()
+ if !funds.Equal(elite) {
+ t.Errorf("received '%v' expected '%v'", funds, elite)
+ }
+}
+
+func TestBaseAvailable(t *testing.T) {
+ t.Parallel()
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem.pairedWith = quoteItem
+ quoteItem.pairedWith = baseItem
+ pairItems := SpotPair{base: baseItem, quote: quoteItem}
+ funds := pairItems.BaseAvailable()
+ if !funds.IsZero() {
+ t.Errorf("received '%v' expected '%v'", funds, baseItem.available)
+ }
+}
+
+func TestQuoteAvailable(t *testing.T) {
+ t.Parallel()
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem.pairedWith = quoteItem
+ quoteItem.pairedWith = baseItem
+ pairItems := SpotPair{base: baseItem, quote: quoteItem}
+ funds := pairItems.QuoteAvailable()
+ if !funds.Equal(elite) {
+ t.Errorf("received '%v' expected '%v'", funds, elite)
+ }
+}
+
+func TestReservePair(t *testing.T) {
+ t.Parallel()
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem.pairedWith = quoteItem
+ quoteItem.pairedWith = baseItem
+ pairItems := SpotPair{base: baseItem, quote: quoteItem}
+ err = pairItems.Reserve(decimal.Zero, gctorder.Buy)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = pairItems.Reserve(elite, gctorder.Buy)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ err = pairItems.Reserve(decimal.Zero, gctorder.Sell)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = pairItems.Reserve(elite, gctorder.Sell)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+ err = pairItems.Reserve(elite, gctorder.DoNothing)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+}
+
+func TestReleasePair(t *testing.T) {
+ t.Parallel()
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem.pairedWith = quoteItem
+ quoteItem.pairedWith = baseItem
+ pairItems := SpotPair{base: baseItem, quote: quoteItem}
+ err = pairItems.Reserve(decimal.Zero, gctorder.Buy)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = pairItems.Reserve(elite, gctorder.Buy)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ err = pairItems.Reserve(decimal.Zero, gctorder.Sell)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = pairItems.Reserve(elite, gctorder.Sell)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+
+ err = pairItems.Release(decimal.Zero, decimal.Zero, gctorder.Buy)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ err = pairItems.Release(elite, decimal.Zero, gctorder.Buy)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ err = pairItems.Release(elite, decimal.Zero, gctorder.Buy)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+
+ err = pairItems.Release(elite, decimal.Zero, gctorder.DoNothing)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+
+ err = pairItems.Release(elite, decimal.Zero, gctorder.Sell)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+ err = pairItems.Release(decimal.Zero, decimal.Zero, gctorder.Sell)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+}
+
+func TestIncreaseAvailablePair(t *testing.T) {
+ t.Parallel()
+ baseItem, err := CreateItem(exchName, a, pair.Base, decimal.Zero, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ quoteItem, err := CreateItem(exchName, a, pair.Quote, elite, decimal.Zero)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ baseItem.pairedWith = quoteItem
+ quoteItem.pairedWith = baseItem
+ pairItems := SpotPair{base: baseItem, quote: quoteItem}
+ err = pairItems.IncreaseAvailable(decimal.Zero, gctorder.Buy)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ if !pairItems.quote.available.Equal(elite) {
+ t.Errorf("received '%v' expected '%v'", elite, pairItems.quote.available)
+ }
+ err = pairItems.IncreaseAvailable(decimal.Zero, gctorder.Sell)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ if !pairItems.base.available.IsZero() {
+ t.Errorf("received '%v' expected '%v'", decimal.Zero, pairItems.base.available)
+ }
+
+ err = pairItems.IncreaseAvailable(elite.Neg(), gctorder.Sell)
+ if !errors.Is(err, errZeroAmountReceived) {
+ t.Errorf("received '%v' expected '%v'", err, errZeroAmountReceived)
+ }
+ if !pairItems.quote.available.Equal(elite) {
+ t.Errorf("received '%v' expected '%v'", elite, pairItems.quote.available)
+ }
+ err = pairItems.IncreaseAvailable(elite, gctorder.Buy)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ if !pairItems.base.available.Equal(elite) {
+ t.Errorf("received '%v' expected '%v'", elite, pairItems.base.available)
+ }
+
+ err = pairItems.IncreaseAvailable(elite, gctorder.DoNothing)
+ if !errors.Is(err, errCannotAllocate) {
+ t.Errorf("received '%v' expected '%v'", err, errCannotAllocate)
+ }
+ if !pairItems.base.available.Equal(elite) {
+ t.Errorf("received '%v' expected '%v'", elite, pairItems.base.available)
+ }
+}
+
+func TestCanPlaceOrderPair(t *testing.T) {
+ t.Parallel()
+ p := SpotPair{
+ base: &Item{},
+ quote: &Item{},
+ }
+ if p.CanPlaceOrder(gctorder.DoNothing) {
+ t.Error("expected false")
+ }
+ if p.CanPlaceOrder(gctorder.Buy) {
+ t.Error("expected false")
+ }
+ if p.CanPlaceOrder(gctorder.Sell) {
+ t.Error("expected false")
+ }
+
+ p.quote.available = decimal.NewFromInt(32)
+ if !p.CanPlaceOrder(gctorder.Buy) {
+ t.Error("expected true")
+ }
+ p.base.available = decimal.NewFromInt(32)
+ if !p.CanPlaceOrder(gctorder.Sell) {
+ t.Error("expected true")
+ }
+}
+
+func TestGetPairReader(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{exchange: "hello"},
+ }
+ var expectedError error
+ ip, err := p.GetPairReader()
+ if !errors.Is(err, expectedError) {
+ t.Errorf("recevied '%v' expected '%v'", err, expectedError)
+ }
+ if ip != p {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestGetCollateralReader(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{exchange: "hello"},
+ }
+ if _, err := p.GetCollateralReader(); !errors.Is(err, ErrNotCollateral) {
+ t.Errorf("recevied '%v' expected '%v'", err, ErrNotCollateral)
+ }
+}
+
+func TestFundReader(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{exchange: "hello"},
+ }
+ if p.FundReader() != p {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestFundReserver(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{exchange: "hello"},
+ }
+ if p.FundReserver() != p {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestFundReleaser(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{exchange: "hello"},
+ }
+ if p.FundReleaser() != p {
+ t.Error("expected the same thing")
+ }
+}
+
+func TestPairReleaser(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{exchange: "hello"},
+ }
+ if _, err := p.PairReleaser(); !errors.Is(err, nil) {
+ t.Errorf("recevied '%v' expected '%v'", err, nil)
+ }
+}
+
+func TestCollateralReleaser(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{exchange: "hello"},
+ }
+ if _, err := p.CollateralReleaser(); !errors.Is(err, ErrNotCollateral) {
+ t.Errorf("recevied '%v' expected '%v'", err, ErrNotCollateral)
+ }
+}
+
+func TestLiquidate(t *testing.T) {
+ t.Parallel()
+ p := &SpotPair{
+ base: &Item{
+ available: decimal.NewFromInt(1337),
+ },
+ quote: &Item{
+ available: decimal.NewFromInt(1337),
+ },
+ }
+ p.Liquidate()
+ if !p.base.available.IsZero() {
+ t.Errorf("received '%v' expected '%v'", p.base.available, "0")
+ }
+ if !p.quote.available.IsZero() {
+ t.Errorf("received '%v' expected '%v'", p.quote.available, "0")
+ }
+}
diff --git a/backtester/funding/trackingcurrencies/trackingcurrencies.go b/backtester/funding/trackingcurrencies/trackingcurrencies.go
index dca46171aa9..0d85e31c966 100644
--- a/backtester/funding/trackingcurrencies/trackingcurrencies.go
+++ b/backtester/funding/trackingcurrencies/trackingcurrencies.go
@@ -41,9 +41,9 @@ var rankedUSDs = []currency.Code{
// a USD equivalent
type TrackingPair struct {
Exchange string
- Asset string
- Base string
- Quote string
+ Asset asset.Item
+ Base currency.Code
+ Quote currency.Code
}
// CreateUSDTrackingPairs is responsible for loading exchanges,
@@ -63,18 +63,19 @@ func CreateUSDTrackingPairs(tp []TrackingPair, em *engine.ExchangeManager) ([]Tr
if err != nil {
return nil, err
}
- pair, err := currency.NewPairFromStrings(tp[i].Base, tp[i].Quote)
- if err != nil {
- return nil, err
- }
+ pair := currency.NewPair(tp[i].Base, tp[i].Quote)
if pairContainsUSD(pair) {
resp = append(resp, tp[i])
} else {
b := exch.GetBase()
- a, err := asset.New(tp[i].Asset)
+ a := tp[i].Asset
if err != nil {
return nil, err
}
+ if a.IsFutures() {
+ // futures matches to spot, not like this
+ continue
+ }
pairs := b.CurrencyPairs.Pairs[a]
basePair, quotePair, err := findMatchingUSDPairs(pair, pairs)
if err != nil {
@@ -85,14 +86,14 @@ func CreateUSDTrackingPairs(tp []TrackingPair, em *engine.ExchangeManager) ([]Tr
TrackingPair{
Exchange: tp[i].Exchange,
Asset: tp[i].Asset,
- Base: basePair.Base.String(),
- Quote: basePair.Quote.String(),
+ Base: basePair.Base,
+ Quote: basePair.Quote,
},
TrackingPair{
Exchange: tp[i].Exchange,
Asset: tp[i].Asset,
- Base: quotePair.Base.String(),
- Quote: quotePair.Quote.String(),
+ Base: quotePair.Base,
+ Quote: quotePair.Quote,
},
)
}
diff --git a/backtester/funding/trackingcurrencies/trackingcurrencies_test.go b/backtester/funding/trackingcurrencies/trackingcurrencies_test.go
index 72447f390e7..aeaad61d39d 100644
--- a/backtester/funding/trackingcurrencies/trackingcurrencies_test.go
+++ b/backtester/funding/trackingcurrencies/trackingcurrencies_test.go
@@ -6,13 +6,14 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
+ "github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
var (
exch = "binance"
- a = "spot"
- b = "BTC"
- q = "USDT"
+ a = asset.Spot
+ b = currency.BTC
+ q = currency.USDT
)
func TestCreateUSDTrackingPairs(t *testing.T) {
@@ -56,8 +57,8 @@ func TestCreateUSDTrackingPairs(t *testing.T) {
if len(resp) != 1 {
t.Error("expected 1 currency setting as it contains a USD equiv")
}
- s1.Base = "LTC"
- s1.Quote = "BTC"
+ s1.Base = currency.LTC
+ s1.Quote = currency.BTC
resp, err = CreateUSDTrackingPairs([]TrackingPair{s1}, em)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
diff --git a/backtester/main.go b/backtester/main.go
index f19cf14c7c7..ae1c9491669 100644
--- a/backtester/main.go
+++ b/backtester/main.go
@@ -6,16 +6,17 @@ import (
"os"
"path/filepath"
- "github.com/thrasher-corp/gocryptotrader/backtester/backtest"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
+ backtest "github.com/thrasher-corp/gocryptotrader/backtester/engine"
+ "github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/signaler"
)
func main() {
var configPath, templatePath, reportOutput string
- var printLogo, generateReport, darkReport bool
+ var printLogo, generateReport, darkReport, verbose, colourOutput, logSubHeader bool
wd, err := os.Getwd()
if err != nil {
fmt.Printf("Could not get working directory. Error: %v.\n", err)
@@ -28,7 +29,7 @@ func main() {
wd,
"config",
"examples",
- "dca-api-candles.strat"),
+ "ftx-cash-carry.strat"),
"the config containing strategy params")
flag.StringVar(
&templatePath,
@@ -60,24 +61,52 @@ func main() {
"darkreport",
false,
"sets the output report to use a dark theme by default")
+ flag.BoolVar(
+ &verbose,
+ "verbose",
+ false,
+ "if enabled, will set exchange requests to verbose for debugging purposes")
+ flag.BoolVar(
+ &colourOutput,
+ "colouroutput",
+ false,
+ "if enabled, will print in colours, if your terminal supports \033[38;5;99m[colours like this]\u001b[0m")
+ flag.BoolVar(
+ &logSubHeader,
+ "logsubheader",
+ true,
+ "displays logging subheader to track where activity originates")
flag.Parse()
-
+ if !colourOutput {
+ common.PurgeColours()
+ }
var bt *backtest.BackTest
var cfg *config.Config
log.GlobalLogConfig = log.GenDefaultSettings()
+ log.GlobalLogConfig.AdvancedSettings.ShowLogSystemName = convert.BoolPtr(logSubHeader)
+ log.GlobalLogConfig.AdvancedSettings.Headers.Info = common.ColourInfo + "[INFO]" + common.ColourDefault
+ log.GlobalLogConfig.AdvancedSettings.Headers.Warn = common.ColourWarn + "[WARN]" + common.ColourDefault
+ log.GlobalLogConfig.AdvancedSettings.Headers.Debug = common.ColourDebug + "[DEBUG]" + common.ColourDefault
+ log.GlobalLogConfig.AdvancedSettings.Headers.Error = common.ColourError + "[ERROR]" + common.ColourDefault
err = log.SetupGlobalLogger()
if err != nil {
fmt.Printf("Could not setup global logger. Error: %v.\n", err)
os.Exit(1)
}
+ err = common.RegisterBacktesterSubLoggers()
+ if err != nil {
+ fmt.Printf("Could not register subloggers. Error: %v.\n", err)
+ os.Exit(1)
+ }
+
cfg, err = config.ReadConfigFromFile(configPath)
if err != nil {
fmt.Printf("Could not read config. Error: %v.\n", err)
os.Exit(1)
}
if printLogo {
- fmt.Print(common.ASCIILogo)
+ fmt.Println(common.Logo())
}
err = cfg.Validate()
@@ -85,7 +114,7 @@ func main() {
fmt.Printf("Could not read config. Error: %v.\n", err)
os.Exit(1)
}
- bt, err = backtest.NewFromConfig(cfg, templatePath, reportOutput)
+ bt, err = backtest.NewFromConfig(cfg, templatePath, reportOutput, verbose)
if err != nil {
fmt.Printf("Could not setup backtester from config. Error: %v.\n", err)
os.Exit(1)
@@ -102,16 +131,12 @@ func main() {
log.Infof(log.Global, "Captured %v, shutdown requested.\n", interrupt)
bt.Stop()
} else {
- err = bt.Run()
- if err != nil {
- fmt.Printf("Could not complete run. Error: %v.\n", err)
- os.Exit(1)
- }
+ bt.Run()
}
err = bt.Statistic.CalculateAllResults()
if err != nil {
- log.Error(log.BackTester, err)
+ log.Error(log.Global, err)
os.Exit(1)
}
@@ -119,7 +144,7 @@ func main() {
bt.Reports.UseDarkMode(darkReport)
err = bt.Reports.GenerateReport()
if err != nil {
- log.Error(log.BackTester, err)
+ log.Error(log.Global, err)
}
}
}
diff --git a/backtester/report/chart.go b/backtester/report/chart.go
new file mode 100644
index 00000000000..49d6d94f2a2
--- /dev/null
+++ b/backtester/report/chart.go
@@ -0,0 +1,188 @@
+package report
+
+import (
+ "fmt"
+
+ "github.com/shopspring/decimal"
+ "github.com/thrasher-corp/gocryptotrader/backtester/common"
+ "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
+ "github.com/thrasher-corp/gocryptotrader/currency"
+ "github.com/thrasher-corp/gocryptotrader/exchanges/asset"
+)
+
+// createUSDTotalsChart used for creating a chart in the HTML report
+// to show how much the overall assets are worth over time
+func createUSDTotalsChart(items []statistics.ValueAtTime, stats []statistics.FundingItemStatistics) (*Chart, error) {
+ if items == nil {
+ return nil, fmt.Errorf("%w missing values at time", common.ErrNilArguments)
+ }
+ if stats == nil {
+ return nil, fmt.Errorf("%w missing funding item statistics", common.ErrNilArguments)
+ }
+ response := &Chart{
+ AxisType: "logarithmic",
+ }
+ usdTotalChartPlot := make([]LinePlot, len(items))
+ for i := range items {
+ usdTotalChartPlot[i] = LinePlot{
+ Value: items[i].Value.InexactFloat64(),
+ UnixMilli: items[i].Time.UTC().UnixMilli(),
+ }
+ }
+ response.Data = append(response.Data, ChartLine{
+ Name: "Total USD value",
+ LinePlots: usdTotalChartPlot,
+ })
+
+ for i := range stats {
+ var plots []LinePlot
+ for j := range stats[i].ReportItem.Snapshots {
+ if stats[i].ReportItem.Snapshots[j].Available.IsZero() {
+ response.ShowZeroDisclaimer = true
+ }
+ plots = append(plots, LinePlot{
+ Value: stats[i].ReportItem.Snapshots[j].USDValue.InexactFloat64(),
+ UnixMilli: stats[i].ReportItem.Snapshots[j].Time.UTC().UnixMilli(),
+ })
+ }
+ response.Data = append(response.Data, ChartLine{
+ Name: fmt.Sprintf("%v %v %v USD value", stats[i].ReportItem.Exchange, stats[i].ReportItem.Asset, stats[i].ReportItem.Currency),
+ LinePlots: plots,
+ })
+ }
+
+ return response, nil
+}
+
+// createHoldingsOverTimeChart used for creating a chart in the HTML report
+// to show how many holdings of each type was held over the time of backtesting
+func createHoldingsOverTimeChart(stats []statistics.FundingItemStatistics) (*Chart, error) {
+ if stats == nil {
+ return nil, fmt.Errorf("%w missing funding item statistics", common.ErrNilArguments)
+ }
+ response := &Chart{
+ AxisType: "logarithmic",
+ }
+ for i := range stats {
+ var plots []LinePlot
+ for j := range stats[i].ReportItem.Snapshots {
+ if stats[i].ReportItem.Snapshots[j].Available.IsZero() {
+ response.ShowZeroDisclaimer = true
+ }
+ plots = append(plots, LinePlot{
+ UnixMilli: stats[i].ReportItem.Snapshots[j].Time.UTC().UnixMilli(),
+ Value: stats[i].ReportItem.Snapshots[j].Available.InexactFloat64(),
+ })
+ }
+ response.Data = append(response.Data, ChartLine{
+ Name: fmt.Sprintf("%v %v %v holdings", stats[i].ReportItem.Exchange, stats[i].ReportItem.Asset, stats[i].ReportItem.Currency),
+ LinePlots: plots,
+ })
+ }
+
+ return response, nil
+}
+
+// createPNLCharts shows a running history of all realised and unrealised PNL values
+// over time
+func createPNLCharts(items map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic) (*Chart, error) {
+ if items == nil {
+ return nil, fmt.Errorf("%w missing currency pair statistics", common.ErrNilArguments)
+ }
+ response := &Chart{
+ AxisType: "linear",
+ }
+ for exch, assetMap := range items {
+ for item, pairMap := range assetMap {
+ for pair, result := range pairMap {
+ id := fmt.Sprintf("%v %v %v",
+ exch,
+ item,
+ pair)
+ uPNLName := fmt.Sprintf("%v Unrealised PNL", id)
+ rPNLName := fmt.Sprintf("%v Realised PNL", id)
+
+ unrealisedPNL := ChartLine{Name: uPNLName}
+ realisedPNL := ChartLine{Name: rPNLName}
+ for i := range result.Events {
+ if result.Events[i].PNL != nil {
+ realisedPNL.LinePlots = append(realisedPNL.LinePlots, LinePlot{
+ Value: result.Events[i].PNL.GetRealisedPNL().PNL.InexactFloat64(),
+ UnixMilli: result.Events[i].Time.UnixMilli(),
+ })
+ unrealisedPNL.LinePlots = append(unrealisedPNL.LinePlots, LinePlot{
+ Value: result.Events[i].PNL.GetUnrealisedPNL().PNL.InexactFloat64(),
+ UnixMilli: result.Events[i].Time.UnixMilli(),
+ })
+ }
+ }
+ if len(unrealisedPNL.LinePlots) == 0 || len(realisedPNL.LinePlots) == 0 {
+ continue
+ }
+ response.Data = append(response.Data, unrealisedPNL, realisedPNL)
+ }
+ }
+ }
+ return response, nil
+}
+
+// createFuturesSpotDiffChart highlights the difference in futures and spot prices
+// over time
+func createFuturesSpotDiffChart(items map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic) (*Chart, error) {
+ if items == nil {
+ return nil, fmt.Errorf("%w missing currency pair statistics", common.ErrNilArguments)
+ }
+ currs := make(map[currency.Pair]linkCurrencyDiff)
+ response := &Chart{
+ AxisType: "linear",
+ }
+
+ for _, assetMap := range items {
+ for item, pairMap := range assetMap {
+ for pair, result := range pairMap {
+ if item.IsFutures() {
+ p := result.UnderlyingPair.Format("", true)
+ diff, ok := currs[p]
+ if !ok {
+ diff = linkCurrencyDiff{}
+ }
+ diff.FuturesPair = pair
+ diff.SpotPair = p
+ diff.FuturesEvents = result.Events
+ currs[p] = diff
+ } else {
+ p := pair.Format("", true)
+ diff, ok := currs[p]
+ if !ok {
+ diff = linkCurrencyDiff{}
+ }
+ diff.SpotEvents = result.Events
+ currs[p] = diff
+ }
+ }
+ }
+ }
+
+ for i := range currs {
+ if currs[i].FuturesEvents == nil || currs[i].SpotEvents == nil {
+ continue
+ }
+ if len(currs[i].SpotEvents) != len(currs[i].FuturesEvents) {
+ continue
+ }
+ line := ChartLine{
+ Name: fmt.Sprintf("%v %v diff %%", currs[i].FuturesPair, currs[i].SpotPair),
+ }
+ for j := range currs[i].SpotEvents {
+ spotPrice := currs[i].SpotEvents[j].DataEvent.GetClosePrice()
+ futuresPrice := currs[i].FuturesEvents[j].DataEvent.GetClosePrice()
+ diff := futuresPrice.Sub(spotPrice).Div(spotPrice).Mul(decimal.NewFromInt(100))
+ line.LinePlots = append(line.LinePlots, LinePlot{
+ Value: diff.InexactFloat64(),
+ UnixMilli: currs[i].SpotEvents[j].Time.UnixMilli(),
+ })
+ }
+ response.Data = append(response.Data, line)
+ }
+ return response, nil
+}
diff --git a/backtester/report/chart_test.go b/backtester/report/chart_test.go
new file mode 100644
index 00000000000..642961cfa64
--- /dev/null
+++ b/backtester/report/chart_test.go
@@ -0,0 +1,223 @@
+package report
+
+import (
+ "errors"
+ "testing"
+ "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/statistics"
+ evkline "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
+ "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 TestCreateUSDTotalsChart(t *testing.T) {
+ t.Parallel()
+ _, err := createUSDTotalsChart(nil, nil)
+ if !errors.Is(err, common.ErrNilArguments) {
+ t.Errorf("received '%v' expected '%v'", err, common.ErrNilArguments)
+ }
+ tt := time.Now()
+ items := []statistics.ValueAtTime{
+ {
+ Time: tt,
+ Value: decimal.NewFromInt(1337),
+ Set: true,
+ },
+ }
+ _, err = createUSDTotalsChart(items, nil)
+ if !errors.Is(err, common.ErrNilArguments) {
+ t.Errorf("received '%v' expected '%v'", err, common.ErrNilArguments)
+ }
+ stats := []statistics.FundingItemStatistics{
+ {
+ ReportItem: &funding.ReportItem{
+ Snapshots: []funding.ItemSnapshot{
+ {
+ Time: tt,
+ USDValue: decimal.NewFromInt(1337),
+ },
+ },
+ },
+ },
+ }
+ resp, err := createUSDTotalsChart(items, stats)
+ if !errors.Is(err, nil) {
+ t.Fatalf("received '%v' expected '%v'", err, nil)
+ }
+ if len(resp.Data) == 0 {
+ t.Fatal("expected not nil")
+ }
+ if resp.Data[0].Name != "Total USD value" {
+ t.Error("expected not nil")
+ }
+ if resp.Data[0].LinePlots[0].Value != 1337 {
+ t.Error("expected not nil")
+ }
+}
+
+func TestCreateHoldingsOverTimeChart(t *testing.T) {
+ t.Parallel()
+ _, err := createHoldingsOverTimeChart(nil)
+ if !errors.Is(err, common.ErrNilArguments) {
+ t.Errorf("received '%v' expected '%v'", err, common.ErrNilArguments)
+ }
+ tt := time.Now()
+ items := []statistics.FundingItemStatistics{
+ {
+ ReportItem: &funding.ReportItem{
+ Exchange: "hello",
+ Asset: asset.Spot,
+ Currency: currency.BTC,
+ Snapshots: []funding.ItemSnapshot{
+ {
+ Time: tt,
+ Available: decimal.NewFromInt(1337),
+ },
+ {
+ Time: tt,
+ },
+ },
+ },
+ },
+ }
+ resp, err := createHoldingsOverTimeChart(items)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+
+ if !resp.ShowZeroDisclaimer {
+ t.Error("expected ShowZeroDisclaimer")
+ }
+}
+
+func TestCreatePNLCharts(t *testing.T) {
+ t.Parallel()
+ _, err := createPNLCharts(nil)
+ if !errors.Is(err, common.ErrNilArguments) {
+ t.Errorf("received '%v' expected '%v'", err, common.ErrNilArguments)
+ }
+
+ tt := time.Now()
+ var d Data
+ d.Statistics = &statistics.Statistic{}
+ d.Statistics.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
+ d.Statistics.ExchangeAssetPairStatistics[testExchange] = make(map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
+ d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
+ d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.NewPair(currency.BTC, currency.USDT)] = &statistics.CurrencyPairStatistic{
+ Events: []statistics.DataAtOffset{
+ {
+ PNL: &portfolio.PNLSummary{
+ Result: gctorder.PNLResult{
+ Time: tt,
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1337),
+ RealisedPNL: decimal.NewFromInt(1337),
+ Price: decimal.NewFromInt(1337),
+ Exposure: decimal.NewFromInt(1337),
+ Direction: gctorder.Short,
+ },
+ },
+ },
+ },
+ }
+
+ d.AddKlineItem(&gctkline.Item{
+ Exchange: testExchange,
+ Pair: currency.NewPair(currency.BTC, currency.USDT),
+ Asset: asset.Spot,
+ Interval: gctkline.OneDay,
+ Candles: []gctkline.Candle{
+ {
+ Time: tt,
+ Open: 1336,
+ High: 1338,
+ Low: 1336,
+ Close: 1337,
+ Volume: 1337,
+ },
+ },
+ })
+ err = d.enhanceCandles()
+ if err != nil {
+ t.Error(err)
+ }
+
+ _, err = createPNLCharts(d.Statistics.ExchangeAssetPairStatistics)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+}
+
+func TestCreateFuturesSpotDiffChart(t *testing.T) {
+ t.Parallel()
+ _, err := createFuturesSpotDiffChart(nil)
+ if !errors.Is(err, common.ErrNilArguments) {
+ t.Errorf("received '%v' expected '%v'", err, common.ErrNilArguments)
+ }
+
+ tt := time.Now()
+ cp := currency.NewPair(currency.BTC, currency.USD)
+ cp2 := currency.NewPair(currency.BTC, currency.DOGE)
+ var d Data
+ d.Statistics = &statistics.Statistic{}
+ d.Statistics.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
+ d.Statistics.ExchangeAssetPairStatistics[testExchange] = make(map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
+ d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
+ d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][cp] = &statistics.CurrencyPairStatistic{
+ Currency: cp,
+ Events: []statistics.DataAtOffset{
+ {
+ Time: tt,
+ DataEvent: &evkline.Kline{Close: decimal.NewFromInt(1337)},
+ PNL: &portfolio.PNLSummary{
+ Result: gctorder.PNLResult{
+ Time: tt,
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1337),
+ RealisedPNL: decimal.NewFromInt(1337),
+ Price: decimal.NewFromInt(1337),
+ Exposure: decimal.NewFromInt(1337),
+ Direction: gctorder.Buy,
+ },
+ },
+ },
+ },
+ }
+ d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Futures] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
+ d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Futures][cp2] = &statistics.CurrencyPairStatistic{
+ UnderlyingPair: cp,
+ Currency: cp2,
+ Events: []statistics.DataAtOffset{
+ {
+ Time: tt,
+ DataEvent: &evkline.Kline{Close: decimal.NewFromInt(1337)},
+ PNL: &portfolio.PNLSummary{
+ Result: gctorder.PNLResult{
+ Time: tt,
+ UnrealisedPNL: decimal.NewFromInt(1337),
+ RealisedPNLBeforeFees: decimal.NewFromInt(1337),
+ RealisedPNL: decimal.NewFromInt(1337),
+ Price: decimal.NewFromInt(1337),
+ Exposure: decimal.NewFromInt(1337),
+ Direction: gctorder.Short,
+ },
+ },
+ },
+ },
+ }
+
+ charty, err := createFuturesSpotDiffChart(d.Statistics.ExchangeAssetPairStatistics)
+ if !errors.Is(err, nil) {
+ t.Errorf("received '%v' expected '%v'", err, nil)
+ }
+ if len(charty.Data) == 0 {
+ t.Error("expected data")
+ }
+}
diff --git a/backtester/report/report.go b/backtester/report/report.go
index c50b68d87b4..1505d65156d 100644
--- a/backtester/report/report.go
+++ b/backtester/report/report.go
@@ -9,6 +9,7 @@ import (
"time"
"github.com/shopspring/decimal"
+ "github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
@@ -17,7 +18,7 @@ import (
// GenerateReport sends final data from statistics to a template
// to create a lovely final report for someone to view
func (d *Data) GenerateReport() error {
- log.Info(log.BackTester, "generating report")
+ log.Info(common.Report, "generating report")
err := d.enhanceCandles()
if err != nil {
return err
@@ -41,21 +42,44 @@ func (d *Data) GenerateReport() error {
d.EnhancedCandles[i].Candles = d.EnhancedCandles[i].Candles[:maxChartLimit]
}
}
- d.USDTotalsChart = d.CreateUSDTotalsChart()
- d.HoldingsOverTimeChart = d.CreateHoldingsOverTimeChart()
+ if d.Statistics.FundingStatistics != nil {
+ d.HoldingsOverTimeChart, err = createHoldingsOverTimeChart(d.Statistics.FundingStatistics.Items)
+ if err != nil {
+ return err
+ }
+ if !d.Statistics.FundingStatistics.Report.DisableUSDTracking {
+ d.USDTotalsChart, err = createUSDTotalsChart(d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues, d.Statistics.FundingStatistics.Items)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ if d.Statistics.HasCollateral {
+ d.PNLOverTimeChart, err = createPNLCharts(d.Statistics.ExchangeAssetPairStatistics)
+ if err != nil {
+ return err
+ }
+ d.FuturesSpotDiffChart, err = createFuturesSpotDiffChart(d.Statistics.ExchangeAssetPairStatistics)
+ if err != nil {
+ return err
+ }
+ }
tmpl := template.Must(
template.ParseFiles(d.TemplatePath),
)
- var nickName string
- if d.Config.Nickname != "" {
- nickName = d.Config.Nickname + "-"
+ fn := d.Config.Nickname
+ if fn != "" {
+ fn += "-"
+ }
+ fn += d.Statistics.StrategyName + "-"
+ fn += time.Now().Format("2006-01-02-15-04-05")
+
+ fileName, err := common.GenerateFileName(fn, "html")
+ if err != nil {
+ return err
}
- fileName := fmt.Sprintf(
- "%v%v-%v.html",
- nickName,
- d.Statistics.StrategyName,
- time.Now().Format("2006-01-02-15-04-05"))
var f *os.File
f, err = os.Create(
filepath.Join(d.OutputPath,
@@ -68,7 +92,7 @@ func (d *Data) GenerateReport() error {
defer func() {
err = f.Close()
if err != nil {
- log.Error(log.BackTester, err)
+ log.Error(common.Report, err)
}
}()
@@ -76,72 +100,10 @@ func (d *Data) GenerateReport() error {
if err != nil {
return err
}
- log.Infof(log.BackTester, "successfully saved report to %v", filepath.Join(d.OutputPath, fileName))
+ log.Infof(common.Report, "successfully saved report to %v", filepath.Join(d.OutputPath, fileName))
return nil
}
-// CreateUSDTotalsChart used for creating a chart in the HTML report
-// to show how much the overall assets are worth over time
-func (d *Data) CreateUSDTotalsChart() []TotalsChart {
- if d.Statistics.FundingStatistics == nil || d.Statistics.FundingStatistics.Report.DisableUSDTracking {
- return nil
- }
-
- usdTotalChartPlot := make([]ChartPlot, len(d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues))
- for i := range d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues {
- usdTotalChartPlot[i] = ChartPlot{
- Value: d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues[i].Value.InexactFloat64(),
- UnixMilli: d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues[i].Time.UTC().UnixMilli(),
- }
- }
-
- response := make([]TotalsChart, len(d.Statistics.FundingStatistics.Items)+1)
- response[0] = TotalsChart{
- Name: "Total USD value",
- DataPoints: usdTotalChartPlot,
- }
-
- for i := range d.Statistics.FundingStatistics.Items {
- plots := make([]ChartPlot, len(d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots))
- for j := range d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots {
- plots[j] = ChartPlot{
- Value: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].USDValue.InexactFloat64(),
- UnixMilli: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].Time.UTC().UnixMilli(),
- }
- }
- response[i+1] = TotalsChart{
- Name: fmt.Sprintf("%v %v %v USD value", d.Statistics.FundingStatistics.Items[i].ReportItem.Exchange, d.Statistics.FundingStatistics.Items[i].ReportItem.Asset, d.Statistics.FundingStatistics.Items[i].ReportItem.Currency),
- DataPoints: plots,
- }
- }
-
- return response
-}
-
-// CreateHoldingsOverTimeChart used for creating a chart in the HTML report
-// to show how many holdings of each type was held over the time of backtesting
-func (d *Data) CreateHoldingsOverTimeChart() []TotalsChart {
- if d.Statistics.FundingStatistics == nil {
- return nil
- }
- response := make([]TotalsChart, len(d.Statistics.FundingStatistics.Items))
- for i := range d.Statistics.FundingStatistics.Items {
- plots := make([]ChartPlot, len(d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots))
- for j := range d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots {
- plots[j] = ChartPlot{
- Value: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].Available.InexactFloat64(),
- UnixMilli: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].Time.UTC().UnixMilli(),
- }
- }
- response[i] = TotalsChart{
- Name: fmt.Sprintf("%v %v %v holdings", d.Statistics.FundingStatistics.Items[i].ReportItem.Exchange, d.Statistics.FundingStatistics.Items[i].ReportItem.Asset, d.Statistics.FundingStatistics.Items[i].ReportItem.Currency),
- DataPoints: plots,
- }
- }
-
- return response
-}
-
// AddKlineItem appends a SET of candles for the report to enhance upon
// generation
func (d *Data) AddKlineItem(k *kline.Item) {
@@ -171,7 +133,7 @@ func (d *Data) enhanceCandles() error {
for intVal := range d.OriginalCandles {
lookup := d.OriginalCandles[intVal]
- enhancedKline := DetailedKline{
+ enhancedKline := EnhancedKline{
Exchange: lookup.Exchange,
Asset: lookup.Asset,
Pair: lookup.Pair,
@@ -207,8 +169,8 @@ func (d *Data) enhanceCandles() error {
}
}
if !requiresIteration {
- if statsForCandles.Events[intVal].SignalEvent.GetTime().Equal(d.OriginalCandles[intVal].Candles[j].Time) &&
- statsForCandles.Events[intVal].SignalEvent.GetDirection() == order.MissingData &&
+ if statsForCandles.Events[intVal].Time.Equal(d.OriginalCandles[intVal].Candles[j].Time) &&
+ (statsForCandles.Events[intVal].SignalEvent == nil || statsForCandles.Events[intVal].SignalEvent.GetDirection() == order.MissingData) &&
len(enhancedKline.Candles) > 0 {
enhancedCandle.copyCloseFromPreviousEvent(&enhancedKline)
}
@@ -222,15 +184,15 @@ func (d *Data) enhanceCandles() error {
}
}
for k := range statsForCandles.FinalOrders.Orders {
- if statsForCandles.FinalOrders.Orders[k].Detail == nil ||
- !statsForCandles.FinalOrders.Orders[k].Date.Equal(d.OriginalCandles[intVal].Candles[j].Time) {
+ if statsForCandles.FinalOrders.Orders[k].Order == nil ||
+ !statsForCandles.FinalOrders.Orders[k].Order.Date.Equal(d.OriginalCandles[intVal].Candles[j].Time) {
continue
}
// an order was placed here, can enhance chart!
enhancedCandle.MadeOrder = true
- enhancedCandle.OrderAmount = decimal.NewFromFloat(statsForCandles.FinalOrders.Orders[k].Amount)
- enhancedCandle.PurchasePrice = statsForCandles.FinalOrders.Orders[k].Price
- enhancedCandle.OrderDirection = statsForCandles.FinalOrders.Orders[k].Side
+ enhancedCandle.OrderAmount = decimal.NewFromFloat(statsForCandles.FinalOrders.Orders[k].Order.Amount)
+ enhancedCandle.PurchasePrice = statsForCandles.FinalOrders.Orders[k].Order.Price
+ enhancedCandle.OrderDirection = statsForCandles.FinalOrders.Orders[k].Order.Side
if enhancedCandle.OrderDirection == order.Buy {
enhancedCandle.Colour = "green"
enhancedCandle.Position = "aboveBar"
@@ -251,12 +213,12 @@ func (d *Data) enhanceCandles() error {
return nil
}
-func (d *DetailedCandle) copyCloseFromPreviousEvent(enhancedKline *DetailedKline) {
+func (d *DetailedCandle) copyCloseFromPreviousEvent(ek *EnhancedKline) {
// if the data is missing, ensure that all values just continue the previous candle's close price visually
- d.Open = enhancedKline.Candles[len(enhancedKline.Candles)-1].Close
- d.High = enhancedKline.Candles[len(enhancedKline.Candles)-1].Close
- d.Low = enhancedKline.Candles[len(enhancedKline.Candles)-1].Close
- d.Close = enhancedKline.Candles[len(enhancedKline.Candles)-1].Close
+ d.Open = ek.Candles[len(ek.Candles)-1].Close
+ d.High = ek.Candles[len(ek.Candles)-1].Close
+ d.Low = ek.Candles[len(ek.Candles)-1].Close
+ d.Close = ek.Candles[len(ek.Candles)-1].Close
d.Colour = "white"
d.Position = "aboveBar"
d.Shape = "arrowDown"
diff --git a/backtester/report/report_test.go b/backtester/report/report_test.go
index 85f4a09ce55..b0df8c37f7b 100644
--- a/backtester/report/report_test.go
+++ b/backtester/report/report_test.go
@@ -8,7 +8,6 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"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/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
@@ -47,7 +46,7 @@ func TestGenerateReport(t *testing.T) {
},
},
},
- EnhancedCandles: []DetailedKline{
+ EnhancedCandles: []EnhancedKline{
{
Exchange: e,
Asset: a,
@@ -222,22 +221,30 @@ func TestGenerateReport(t *testing.T) {
},
},
Statistics: &statistics.Statistic{
+ FundingStatistics: &statistics.FundingStatistics{
+ Report: &funding.Report{
+ DisableUSDTracking: true,
+ },
+ Items: []statistics.FundingItemStatistics{
+ {
+ ReportItem: &funding.ReportItem{Snapshots: []funding.ItemSnapshot{{Time: time.Now()}}},
+ },
+ },
+ TotalUSDStatistics: &statistics.TotalFundingStatistics{},
+ },
StrategyName: "testStrat",
RiskFreeRate: decimal.NewFromFloat(0.03),
ExchangeAssetPairStatistics: map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic{
e: {
a: {
p: &statistics.CurrencyPairStatistic{
- MaxDrawdown: statistics.Swing{},
- LowestClosePrice: decimal.NewFromInt(100),
- HighestClosePrice: decimal.NewFromInt(200),
+ LowestClosePrice: statistics.ValueAtTime{Value: decimal.NewFromInt(100)},
+ HighestClosePrice: statistics.ValueAtTime{Value: decimal.NewFromInt(200)},
MarketMovement: decimal.NewFromInt(100),
StrategyMovement: decimal.NewFromInt(100),
CompoundAnnualGrowthRate: decimal.NewFromInt(1),
BuyOrders: 1,
SellOrders: 1,
- FinalHoldings: holdings.Holding{},
- FinalOrders: compliance.Snapshot{},
ArithmeticRatios: &statistics.Ratios{},
GeometricRatios: &statistics.Ratios{},
},
@@ -301,13 +308,6 @@ func TestGenerateReport(t *testing.T) {
MarketMovement: decimal.NewFromInt(1337),
StrategyMovement: decimal.NewFromInt(1337),
},
- CurrencyPairStatistics: nil,
- WasAnyDataMissing: false,
- FundingStatistics: &statistics.FundingStatistics{
- Report: &funding.Report{
- DisableUSDTracking: true,
- },
- },
},
}
if err := d.GenerateReport(); err != nil {
@@ -397,7 +397,7 @@ func TestEnhanceCandles(t *testing.T) {
VolumeAdjustedPrice: decimal.NewFromInt(1337),
SlippageRate: decimal.NewFromInt(1),
CostBasis: decimal.NewFromInt(1337),
- Detail: nil,
+ Order: nil,
},
},
Timestamp: tt,
@@ -414,7 +414,7 @@ func TestEnhanceCandles(t *testing.T) {
VolumeAdjustedPrice: decimal.NewFromInt(1337),
SlippageRate: decimal.NewFromInt(1),
CostBasis: decimal.NewFromInt(1337),
- Detail: &gctorder.Detail{
+ Order: &gctorder.Detail{
Date: tt,
Side: gctorder.Buy,
},
@@ -434,7 +434,7 @@ func TestEnhanceCandles(t *testing.T) {
VolumeAdjustedPrice: decimal.NewFromInt(1337),
SlippageRate: decimal.NewFromInt(1),
CostBasis: decimal.NewFromInt(1337),
- Detail: &gctorder.Detail{
+ Order: &gctorder.Detail{
Date: tt,
Side: gctorder.Sell,
},
@@ -451,3 +451,67 @@ func TestEnhanceCandles(t *testing.T) {
t.Error("expected enhanced candles")
}
}
+
+func TestUpdateItem(t *testing.T) {
+ t.Parallel()
+ d := Data{}
+ tt := time.Now()
+ d.UpdateItem(&gctkline.Item{
+ Candles: []gctkline.Candle{
+ {
+ Time: tt,
+ },
+ },
+ })
+ if len(d.OriginalCandles) != 1 {
+ t.Fatal("expected Original Candles len of 1")
+ }
+ if len(d.OriginalCandles[0].Candles) != 1 {
+ t.Error("expected one candle")
+ }
+ d.UpdateItem(&gctkline.Item{
+ Candles: []gctkline.Candle{
+ {
+ Time: tt,
+ },
+ },
+ })
+ if len(d.OriginalCandles[0].Candles) != 1 {
+ t.Error("expected one candle")
+ }
+
+ d.UpdateItem(&gctkline.Item{
+ Candles: []gctkline.Candle{
+ {
+ Time: tt.Add(1),
+ },
+ },
+ })
+ if len(d.OriginalCandles[0].Candles) != 2 {
+ t.Error("expected two candles")
+ }
+}
+
+func TestCopyCloseFromPreviousEvent(t *testing.T) {
+ t.Parallel()
+ d := DetailedCandle{}
+ d.copyCloseFromPreviousEvent(&EnhancedKline{
+ Candles: []DetailedCandle{
+ {
+ Close: 1337,
+ },
+ },
+ })
+ if d.Close != 1337 {
+ t.Error("expected 1337")
+ }
+}
+
+func TestUseDarkMode(t *testing.T) {
+ t.Parallel()
+ d := Data{}
+ d.UseDarkMode(true)
+ if !d.UseDarkTheme {
+ t.Error("expected true")
+ }
+}
diff --git a/backtester/report/report_types.go b/backtester/report/report_types.go
index d751609b995..867c6901f80 100644
--- a/backtester/report/report_types.go
+++ b/backtester/report/report_types.go
@@ -32,28 +32,37 @@ type Handler interface {
// Data holds all statistical information required to output detailed backtesting results
type Data struct {
OriginalCandles []*kline.Item
- EnhancedCandles []DetailedKline
+ EnhancedCandles []EnhancedKline
Statistics *statistics.Statistic
Config *config.Config
TemplatePath string
OutputPath string
Warnings []Warning
UseDarkTheme bool
- USDTotalsChart []TotalsChart
- HoldingsOverTimeChart []TotalsChart
+ USDTotalsChart *Chart
+ HoldingsOverTimeChart *Chart
+ PNLOverTimeChart *Chart
+ FuturesSpotDiffChart *Chart
Prettify PrettyNumbers
}
-// TotalsChart holds chart plot data
+// Chart holds chart data along with an axis
+type Chart struct {
+ AxisType string
+ ShowZeroDisclaimer bool
+ Data []ChartLine
+}
+
+// ChartLine holds chart plot data
// to render charts in the report
-type TotalsChart struct {
- Name string
- DataPoints []ChartPlot
+type ChartLine struct {
+ Name string
+ LinePlots []LinePlot
}
-// ChartPlot holds value data
+// LinePlot holds value data
// for a chart
-type ChartPlot struct {
+type LinePlot struct {
Value float64
UnixMilli int64
Flag string
@@ -67,8 +76,8 @@ type Warning struct {
Message string
}
-// DetailedKline enhances kline details for the purpose of rich reporting results
-type DetailedKline struct {
+// EnhancedKline enhances kline details for the purpose of rich reporting results
+type EnhancedKline struct {
IsOverLimit bool
Watermark string
Exchange string
@@ -97,6 +106,14 @@ type DetailedCandle struct {
PurchasePrice float64
}
+type linkCurrencyDiff struct {
+ FuturesPair currency.Pair
+ SpotPair currency.Pair
+ FuturesEvents []statistics.DataAtOffset
+ SpotEvents []statistics.DataAtOffset
+ DiffPercent []decimal.Decimal
+}
+
// PrettyNumbers is used for report rendering
// one cannot access packages when rendering data in a template
// this struct exists purely to help make numbers look pretty
diff --git a/backtester/report/tpl.gohtml b/backtester/report/tpl.gohtml
index 9573a3fa669..ee553f17ec4 100644
--- a/backtester/report/tpl.gohtml
+++ b/backtester/report/tpl.gohtml
@@ -1,3 +1,4 @@
+
{{.Config.Nickname}} Results
@@ -35,12 +36,7 @@
-
+
@@ -88,9 +84,9 @@
Config Settings
{{ if .Warnings}}
-
- Warnings
-
+
+ Warnings
+
{{end}}
Charts
@@ -101,9 +97,9 @@
Pair Statistics
-
- Funding Statistics
-
+
+ Funding Statistics
+
Orders
@@ -132,101 +128,103 @@
Results for {{.Statistics.StrategyName}} {{.Config.Nickname }}
-
-
-
-
-
-
Goal
-
{{.Config.Goal}}
-
Strategy Description
-
{{.Statistics.StrategyDescription}}
- {{ if or .Config.DataSettings.APIData .Config.DataSettings.DatabaseData }}
-
-
- {{ if .Config.DataSettings.APIData}}
-
- Start Date |
-
- {{.Config.DataSettings.APIData.StartDate}}
- |
-
-
- End Date |
-
-
- {{.Config.DataSettings.APIData.EndDate}}
- |
-
-
- Interval |
-
- {{.Config.DataSettings.Interval}}
- |
-
- {{end}}
- {{ if .Config.DataSettings.DatabaseData}}
-
- Start Date |
-
- {{.Config.DataSettings.DatabaseData.StartDate}}
- |
-
-
- End Date |
-
-
- {{.Config.DataSettings.DatabaseData.EndDate}}
- |
-
-
- Interval |
-
- {{.Config.DataSettings.Interval}}
- |
-
- {{if .Statistics.WasAnyDataMissing}}
-
- Was any data missing? |
- {{ .Statistics.WasAnyDataMissing}} |
-
- {{end}}
+
+
+
+
+
+
Goal
+
{{.Config.Goal}}
+
Strategy Description
+
{{.Statistics.StrategyDescription}}
+ {{ if or .Config.DataSettings.APIData .Config.DataSettings.DatabaseData }}
+
+
+ {{ if .Config.DataSettings.APIData}}
+
+ Start Date |
+
+ {{.Config.DataSettings.APIData.StartDate}}
+ |
+
+
+ End Date |
- {{end}}
-
-
- {{ end }}
- {{ if or .Config.DataSettings.CSVData .Config.DataSettings.LiveData }}
-
-
+
+ {{.Config.DataSettings.APIData.EndDate}}
+ |
+
Interval |
{{.Config.DataSettings.Interval}}
|
-
-
- {{ end }}
- {{if .Config.StrategySettings.UseExchangeLevelFunding}}
-
Funding results
-
-
+ {{end}}
+ {{ if .Config.DataSettings.DatabaseData}}
- Exchange |
- Asset |
- Currency |
- Initial Funds |
- Final Funds |
- {{ if eq .Config.StrategySettings.DisableUSDTracking false }}
- Initial Fund in USD |
- Final Funds in USD |
- {{end}}
- Difference |
+ Start Date |
+
+ {{.Config.DataSettings.DatabaseData.StartDate}}
+ |
-
-
- {{ range .Statistics.FundingStatistics.Report.Items}}
+
+ End Date |
+
+
+ {{.Config.DataSettings.DatabaseData.EndDate}}
+ |
+
+
+ Interval |
+
+ {{.Config.DataSettings.Interval}}
+ |
+
+ {{if .Statistics.WasAnyDataMissing}}
+
+ Was any data missing? |
+ {{ .Statistics.WasAnyDataMissing}} |
+
+ {{end}}
+
+ {{end}}
+
+
+ {{ end }}
+ {{ if or .Config.DataSettings.CSVData .Config.DataSettings.LiveData }}
+
+
+
+ Interval |
+
+ {{.Config.DataSettings.Interval}}
+ |
+
+
+
+ {{ end }}
+ {{if .Config.FundingSettings.UseExchangeLevelFunding}}
+
Funding results
+
+
+
+ Exchange |
+ Asset |
+ Currency |
+ Initial Funds |
+ Final Funds |
+ {{ if eq .Config.StrategySettings.DisableUSDTracking false }}
+ Initial Fund in USD |
+ Final Funds in USD |
+ {{end}}
+ Difference |
+
+
+
+ {{ range .Statistics.FundingStatistics.Report.Items}}
+ {{ if .IsCollateral}}
+ {{else }}
{{.Exchange}} |
{{.Asset}} |
@@ -244,265 +242,295 @@
{{ end }}
{{end}}
-
-
-
-
Pair market movement
+ {{end}}
+
+
+ {{ if .Statistics.HasCollateral}}
+
Collateral details
Exchange |
Asset |
Currency |
- Market Movement |
-
-
-
- {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}}
- {{ range $asset, $unused := .}}
- {{ range $pair, $unused := .}}
-
- {{ $exchange}} |
- {{ $asset}} |
- {{ $pair}} |
- {{ $.Prettify.Decimal8 .MarketMovement}}% |
-
- {{end}}
- {{end}}
- {{end}}
-
-
- {{else}}
-
Pair market movement
-
-
-
- Exchange |
- Asset |
- Currency |
- Initial Base funds |
- Initial Quote funds |
- Initial Total value |
- Resulting Base funds |
- Resulting Quote funds |
- Resulting Total value |
- Did it make profit? |
- Did it beat the market? |
- Strategy Movement |
- Market Movement |
+ Initial Collateral |
+ Final Collateral |
+ Difference |
- {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}}
- {{ range $asset, $unused := .}}
- {{ range $pair, $unused := .}}
-
- {{ $exchange}} |
- {{ $asset}} |
- {{ $pair}} |
- {{ $.Prettify.Decimal8 .InitialHoldings.BaseInitialFunds }} {{.FinalHoldings.Pair.Base}} |
- {{ $.Prettify.Decimal8 .InitialHoldings.QuoteInitialFunds }} {{.FinalHoldings.Pair.Quote}} |
- {{ $.Prettify.Decimal8 .InitialHoldings.TotalInitialValue }} {{.FinalHoldings.Pair.Quote}} |
- {{ $.Prettify.Decimal8 .FinalHoldings.BaseSize }} {{ .FinalHoldings.Pair.Base}} |
- {{ $.Prettify.Decimal8 .FinalHoldings.QuoteSize }} {{ .FinalHoldings.Pair.Quote}} |
- {{ $.Prettify.Decimal8 .FinalHoldings.TotalValue }} {{ .FinalHoldings.Pair.Quote}} |
- {{ .IsStrategyProfitable }} |
- {{ .DoesPerformanceBeatTheMarket }} |
- {{ $.Prettify.Decimal8 .StrategyMovement }}% |
- {{ $.Prettify.Decimal8 .MarketMovement}}% |
-
- {{end}}
+ {{ range .Statistics.FundingStatistics.Report.Items}}
+ {{ if .IsCollateral}}
+
+ {{.Exchange}} |
+ {{.Asset}} |
+ {{.Currency}} |
+ {{ $.Prettify.Decimal8 .InitialFunds }} {{.Currency}} |
+ {{ $.Prettify.Decimal8 .FinalFunds }} {{.Currency}} |
+ {{if .ShowInfinite}}
+ Infinity% |
+ {{ else }}
+ {{ .Difference}}% |
+ {{ end }}
+
+ {{else }}
+
{{end}}
{{end}}
- {{end}}
- {{ if eq .Config.StrategySettings.DisableUSDTracking false}}
-
USD Totals
-
-
-
- Initial Total Funds in USD |
- ${{ $.Prettify.Decimal2 .Statistics.FundingStatistics.TotalUSDStatistics.InitialHoldingValue.Value}} |
-
-
- Final Total Funds in USD |
- ${{ $.Prettify.Decimal2 .Statistics.FundingStatistics.TotalUSDStatistics.FinalHoldingValue.Value}} |
-
-
- Difference |
- {{ $.Prettify.Decimal8 .Statistics.FundingStatistics.TotalUSDStatistics.HoldingValueDifference}}% |
-
-
-
- {{end}}
-
-
-
-
-
-
-
-
+ {{ end }}
+
+
Pair market movement
-
-
- Strategy name |
- {{.Config.StrategySettings.Name}} |
-
-
- Is multi currency |
- {{.Config.StrategySettings.SimultaneousSignalProcessing}} |
-
+
- Custom settings |
- {{.Config.StrategySettings.CustomSettings}} |
+ Exchange |
+ Asset |
+ Currency |
+ Market Movement |
+
+
+ {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}}
+ {{ range $asset, $unused := .}}
+ {{ range $pair, $unused := .}}
+
+ {{ $exchange}} |
+ {{ $asset}} |
+ {{ $pair}} |
+ {{ $.Prettify.Decimal8 .MarketMovement}}% |
+
+ {{end}}
+ {{end}}
+ {{end}}
-
-
-
-
-
-
-
+ {{else}}
+
Pair market movement
- Exchange Name |
+ Exchange |
Asset |
- Currency Base |
- Currency Quote |
- Buy side Min Amount |
- Buy side Max Amount |
- Buy side Max Total |
- Sell side Min Amount |
- Sell side Max Amount |
- Sell side Max Total |
- Min Slippage Percent |
- Max Slippage Percent |
- Taker Fee |
- Maximum Holdings Ratio |
+ Currency |
+ Initial Base funds |
+ Initial Quote funds |
+ Initial Total value |
+ Resulting Base funds |
+ Resulting Quote funds |
+ Resulting Total value |
+ Did it make profit? |
+ Did it beat the market? |
+ Strategy Movement |
+ Market Movement |
- {{ range .Config.CurrencySettings}}
- {{if .USDTrackingPair}}
- {{else}}
-
- {{.ExchangeName}} |
- {{.Asset}} |
- {{.Base}} |
- {{.Quote}} |
- {{ $.Prettify.Decimal64 .BuySide.MinimumSize}} {{.Base}} |
- {{ $.Prettify.Decimal64 .BuySide.MaximumSize}} {{.Base}} |
- {{ $.Prettify.Decimal64 .BuySide.MaximumTotal}} {{.Quote}} |
- {{ $.Prettify.Decimal64 .SellSide.MinimumSize}} {{.Base}} |
- {{ $.Prettify.Decimal64 .SellSide.MaximumSize}} {{.Base}} |
- {{ $.Prettify.Decimal64 .SellSide.MaximumTotal}} {{.Quote}} |
- {{ $.Prettify.Decimal64 .MinimumSlippagePercent}}% |
- {{ $.Prettify.Decimal64 .MaximumSlippagePercent}}% |
- {{.TakerFee}} |
- {{.MaximumHoldingsRatio}} |
-
+ {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}}
+ {{ range $asset, $unused := .}}
+ {{ range $pair, $unused := .}}
+
+ {{ $exchange}} |
+ {{ $asset}} |
+ {{ $pair}} |
+ {{ $.Prettify.Decimal8 .InitialHoldings.BaseInitialFunds }} {{.FinalHoldings.Pair.Base}} |
+ {{ $.Prettify.Decimal8 .InitialHoldings.QuoteInitialFunds }} {{.FinalHoldings.Pair.Quote}} |
+ {{ $.Prettify.Decimal8 .InitialHoldings.TotalInitialValue }} {{.FinalHoldings.Pair.Quote}} |
+ {{ $.Prettify.Decimal8 .FinalHoldings.BaseSize }} {{ .FinalHoldings.Pair.Base}} |
+ {{ $.Prettify.Decimal8 .FinalHoldings.QuoteSize }} {{ .FinalHoldings.Pair.Quote}} |
+ {{ $.Prettify.Decimal8 .FinalHoldings.TotalValue }} {{ .FinalHoldings.Pair.Quote}} |
+ {{ .IsStrategyProfitable }} |
+ {{ .DoesPerformanceBeatTheMarket }} |
+ {{ $.Prettify.Decimal8 .StrategyMovement }}% |
+ {{ $.Prettify.Decimal8 .MarketMovement}}% |
+
+ {{end}}
{{end}}
{{end}}
-
-
- {{ if .Statistics.FundingStatistics.Report }}
-
-
-
-
-
-
-
-
- Exchange Name |
- Asset |
- Currency |
- Paired With |
- Initial Funds |
- Transfer Fee |
-
-
-
- {{ range .Statistics.FundingStatistics.Report.Items}}
-
- {{.Exchange}} |
- {{.Asset}} |
- {{.Currency}} |
- {{.PairedWith}} |
- {{ $.Prettify.Decimal8 .InitialFunds}} |
- {{ $.Prettify.Decimal64 .TransferFee}} |
-
- {{end}}
-
-
-
-
- {{ end }}
-
-
-
-
-
+ {{end}}
+ {{ if eq .Config.StrategySettings.DisableUSDTracking false}}
+
USD Totals
-
+
- Can Use Leverage |
- Max Leverage Rate |
- Max Orders With Leverage Ratio |
- Buy side Min Amount |
- Buy side Max Amount |
- Buy side Max Total |
- Sell side Min Amount |
- Sell side Max Amount |
- Sell side Max Total |
+ Initial Total Funds in USD |
+ ${{ $.Prettify.Decimal2 .Statistics.FundingStatistics.Report.InitialFunds}} |
+
+
+ Final Total Funds in USD |
+ ${{ $.Prettify.Decimal2 .Statistics.FundingStatistics.Report.FinalFunds}} |
-
-
- {{.Config.PortfolioSettings.Leverage.CanUseLeverage}} |
- {{.Config.PortfolioSettings.Leverage.MaximumLeverageRate}} |
- {{.Config.PortfolioSettings.Leverage.MaximumOrdersWithLeverageRatio}} |
- {{ $.Prettify.Decimal64 .Config.PortfolioSettings.BuySide.MinimumSize}} |
- {{ $.Prettify.Decimal64 .Config.PortfolioSettings.BuySide.MaximumSize}} |
- {{ $.Prettify.Decimal64 .Config.PortfolioSettings.BuySide.MaximumTotal}} |
- {{ $.Prettify.Decimal64 .Config.PortfolioSettings.SellSide.MinimumSize}} |
- {{ $.Prettify.Decimal64 .Config.PortfolioSettings.SellSide.MaximumSize}} |
- {{ $.Prettify.Decimal64 .Config.PortfolioSettings.SellSide.MaximumTotal}} |
+ Difference |
+ {{ $.Prettify.Decimal8 .Statistics.FundingStatistics.TotalUSDStatistics.HoldingValueDifference}}% |
-
+ {{end}}
+
+
+
+
+
+
+
+
+
+ Strategy name |
+ Is multi currency |
+ Custom settings |
+
+
+
+ {{.Config.StrategySettings.Name}} |
+ {{.Config.StrategySettings.SimultaneousSignalProcessing}} |
+ {{.Config.StrategySettings.CustomSettings}} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Exchange Name |
+ Asset |
+ Currency |
+ Buy side Min Amount |
+ Buy side Max Amount |
+ Buy side Max Total |
+ Sell side Min Amount |
+ Sell side Max Amount |
+ Sell side Max Total |
+ Min Slippage Percent |
+ Max Slippage Percent |
+ Taker Fee |
+ Maximum Holdings Ratio |
+
+
+
+ {{ range .Config.CurrencySettings}}
+ {{if .USDTrackingPair}}
+ {{else}}
+
+ {{.ExchangeName}} |
+ {{.Asset}} |
+ {{.Base}}-{{.Quote}} |
+ {{ $.Prettify.Decimal64 .BuySide.MinimumSize}} {{if .Asset.IsFutures}} {{.Base}}-{{.Quote}} {{else}}{{.Base}} {{end}} |
+ {{ $.Prettify.Decimal64 .BuySide.MaximumSize}} {{if .Asset.IsFutures}} {{.Base}}-{{.Quote}} {{else}}{{.Base}} {{end}} |
+ {{ $.Prettify.Decimal64 .BuySide.MaximumTotal}} {{if .Asset.IsFutures}}{{else}}{{.Quote}} {{end}} |
+ {{ $.Prettify.Decimal64 .SellSide.MinimumSize}} {{if .Asset.IsFutures}} {{.Base}}-{{.Quote}} {{else}}{{.Base}} {{end}} |
+ {{ $.Prettify.Decimal64 .SellSide.MaximumSize}} {{if .Asset.IsFutures}} {{.Base}}-{{.Quote}} {{else}}{{.Base}} {{end}} |
+ {{ $.Prettify.Decimal64 .SellSide.MaximumTotal}} {{if .Asset.IsFutures}}{{else}}{{.Quote}} {{end}} |
+ {{ $.Prettify.Decimal64 .MinimumSlippagePercent}}% |
+ {{ $.Prettify.Decimal64 .MaximumSlippagePercent}}% |
+ {{.TakerFee}} |
+ {{.MaximumHoldingsRatio}} |
+
+ {{end}}
+ {{end}}
+
+
+
+
+ {{ if .Statistics.FundingStatistics.Report }}
-
+
- Risk-Free Rate |
+ Exchange Name |
+ Asset |
+ Currency |
+ Paired With |
+ Initial Funds |
+ Final Funds |
+ Transfer Fee |
+ Is Collateral |
-
- {{ .Config.StatisticSettings.RiskFreeRate}} |
-
+ {{ range .Statistics.FundingStatistics.Report.Items}}
+
+ {{.Exchange}} |
+ {{.Asset}} |
+ {{.Currency}} |
+ {{.PairedWith}} |
+ {{ $.Prettify.Decimal8 .InitialFunds}} {{.Currency}} |
+ {{ $.Prettify.Decimal8 .FinalFunds}} {{.Currency}} |
+ {{ $.Prettify.Decimal64 .TransferFee}} |
+ {{ .IsCollateral }} |
+
+ {{end}}
- {{ if .Warnings }}
+
+ {{ end }}
+
+
+
+
+
+
+
+
+ Buy side Min Amount |
+ Buy side Max Amount |
+ Buy side Max Total |
+ Sell side Min Amount |
+ Sell side Max Amount |
+ Sell side Max Total |
+
+
+
+
+ {{ $.Prettify.Decimal64 .Config.PortfolioSettings.BuySide.MinimumSize}} |
+ {{ $.Prettify.Decimal64 .Config.PortfolioSettings.BuySide.MaximumSize}} |
+ {{ $.Prettify.Decimal64 .Config.PortfolioSettings.BuySide.MaximumTotal}} |
+ {{ $.Prettify.Decimal64 .Config.PortfolioSettings.SellSide.MinimumSize}} |
+ {{ $.Prettify.Decimal64 .Config.PortfolioSettings.SellSide.MaximumSize}} |
+ {{ $.Prettify.Decimal64 .Config.PortfolioSettings.SellSide.MaximumTotal}} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Risk-Free Rate |
+
+
+
+
+ {{ .Config.StatisticSettings.RiskFreeRate}} |
+
+
+
+
+ {{ if .Warnings }}
@@ -544,7 +572,7 @@
- {{end}}
+ {{end}}
@@ -746,9 +774,146 @@
// Apply the theme
Highcharts.setOptions(Highcharts.theme);
+ {{ if .PNLOverTimeChart }}
+
PNL Over Time
+
+
+
+ {{end}}
+ {{if .FuturesSpotDiffChart }}
+
Futures Spot Diff %
+
+
+
+ {{end}}
{{ if eq $.Config.StrategySettings.DisableUSDTracking false }}
USD Totals
+ {{ if .USDTotalsChart.ShowZeroDisclaimer}}
+
Note: zero values are not rendered on chart. If line abruptly ends, it is because its value is zero
+ {{end}}