From 91d699be9d3b22c51fa820ac901fd90cb64e0ee7 Mon Sep 17 00:00:00 2001 From: Scott Date: Wed, 4 Oct 2023 09:19:41 +1000 Subject: [PATCH] maps: expansion of Key concept (#1349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * moves everything to use single map keys, also breaks * full rollout * tests * fix a little bug * minor test fixups * Fix Key use * rm ๐Ÿ”‘ from ๐Ÿ”‘ struct name --- backtester/data/data.go | 51 +- backtester/data/data_test.go | 8 +- backtester/data/data_types.go | 3 +- backtester/engine/backtest.go | 3 + backtester/engine/backtest_test.go | 12 +- backtester/engine/setup.go | 23 +- .../eventhandlers/portfolio/portfolio.go | 213 ++--- .../eventhandlers/portfolio/portfolio_test.go | 156 ++-- .../portfolio/portfolio_types.go | 3 +- .../eventhandlers/portfolio/risk/risk.go | 8 +- .../eventhandlers/portfolio/risk/risk_test.go | 41 +- .../portfolio/risk/risk_types.go | 5 +- backtester/eventhandlers/portfolio/setup.go | 29 +- .../statistics/currencystatistics.go | 5 + .../statistics/currencystatistics_test.go | 7 +- .../statistics/fundingstatistics.go | 40 +- .../statistics/fundingstatistics_test.go | 26 +- .../eventhandlers/statistics/printresults.go | 69 +- .../eventhandlers/statistics/statistics.go | 166 ++-- .../statistics/statistics_test.go | 38 +- .../statistics/statistics_types.go | 45 +- backtester/report/chart.go | 104 +-- backtester/report/chart_test.go | 34 +- backtester/report/report.go | 9 +- backtester/report/report_test.go | 67 +- backtester/report/tpl.gohtml | 861 +++++++++--------- common/key/key.go | 47 + common/key/key_test.go | 72 ++ engine/engine_test.go | 2 +- engine/event_manager_test.go | 2 +- exchanges/account/account.go | 127 ++- exchanges/account/account_test.go | 16 +- exchanges/account/account_types.go | 3 +- exchanges/asset/asset.go | 2 + exchanges/bithumb/bithumb_test.go | 2 +- exchanges/exchange.go | 3 + exchanges/futures/futures.go | 103 ++- exchanges/futures/futures_test.go | 172 ++-- exchanges/futures/futures_types.go | 3 +- exchanges/orderbook/linked_list_test.go | 2 +- exchanges/orderbook/orderbook.go | 87 +- exchanges/orderbook/orderbook_types.go | 3 +- exchanges/stream/buffer/buffer.go | 15 +- exchanges/stream/buffer/buffer_test.go | 31 +- exchanges/stream/buffer/buffer_types.go | 12 +- exchanges/ticker/ticker.go | 84 +- exchanges/ticker/ticker_types.go | 3 +- 47 files changed, 1478 insertions(+), 1339 deletions(-) create mode 100644 common/key/key.go create mode 100644 common/key/key_test.go diff --git a/backtester/data/data.go b/backtester/data/data.go index bf506ca8..5f94ea4e 100644 --- a/backtester/data/data.go +++ b/backtester/data/data.go @@ -7,6 +7,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/common" gctcommon "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) @@ -14,7 +15,7 @@ import ( // NewHandlerHolder returns a new HandlerHolder func NewHandlerHolder() *HandlerHolder { return &HandlerHolder{ - data: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler), + data: make(map[key.ExchangePairAsset]Handler), } } @@ -26,28 +27,15 @@ func (h *HandlerHolder) SetDataForCurrency(e string, a asset.Item, p currency.Pa h.m.Lock() defer h.m.Unlock() if h.data == nil { - h.data = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler) + h.data = make(map[key.ExchangePairAsset]Handler) } e = strings.ToLower(e) - m1, ok := h.data[e] - if !ok { - m1 = make(map[asset.Item]map[*currency.Item]map[*currency.Item]Handler) - h.data[e] = m1 - } - - m2, ok := m1[a] - if !ok { - m2 = make(map[*currency.Item]map[*currency.Item]Handler) - m1[a] = m2 - } - - m3, ok := m2[p.Base.Item] - if !ok { - m3 = make(map[*currency.Item]Handler) - m2[p.Base.Item] = m3 - } - - m3[p.Quote.Item] = k + h.data[key.ExchangePairAsset{ + Exchange: e, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] = k return nil } @@ -58,15 +46,9 @@ func (h *HandlerHolder) GetAllData() ([]Handler, error) { } h.m.Lock() defer h.m.Unlock() - var resp []Handler - for _, exchMap := range h.data { - for _, assetMap := range exchMap { - for _, baseMap := range assetMap { - for _, handler := range baseMap { - resp = append(resp, handler) - } - } - } + resp := make([]Handler, 0, len(h.data)) + for _, handler := range h.data { + resp = append(resp, handler) } return resp, nil } @@ -84,7 +66,12 @@ func (h *HandlerHolder) GetDataForCurrency(ev common.Event) (Handler, error) { exch := ev.GetExchange() a := ev.GetAssetType() p := ev.Pair() - handler, ok := h.data[exch][a][p.Base.Item][p.Quote.Item] + handler, ok := h.data[key.ExchangePairAsset{ + Exchange: exch, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] if !ok { return nil, fmt.Errorf("%s %s %s %w", exch, a, p, ErrHandlerNotFound) } @@ -98,7 +85,7 @@ func (h *HandlerHolder) Reset() error { } h.m.Lock() defer h.m.Unlock() - h.data = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler) + h.data = make(map[key.ExchangePairAsset]Handler) return nil } diff --git a/backtester/data/data_test.go b/backtester/data/data_test.go index b0eb7d1c..47467974 100644 --- a/backtester/data/data_test.go +++ b/backtester/data/data_test.go @@ -10,6 +10,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/common" "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event" gctcommon "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -37,7 +38,12 @@ func TestSetDataForCurrency(t *testing.T) { if d.data == nil { t.Error("expected not nil") } - if d.data[exch][a][p.Base.Item][p.Quote.Item] != nil { + if d.data[key.ExchangePairAsset{ + Exchange: exch, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] != nil { t.Error("expected nil") } } diff --git a/backtester/data/data_types.go b/backtester/data/data_types.go index 4a3bac4f..60f42e52 100644 --- a/backtester/data/data_types.go +++ b/backtester/data/data_types.go @@ -7,6 +7,7 @@ import ( "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/backtester/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) @@ -28,7 +29,7 @@ var ( // HandlerHolder stores an event handler per exchange asset pair type HandlerHolder struct { m sync.Mutex - data map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler + data map[key.ExchangePairAsset]Handler } // Holder interface dictates what a Data holder is expected to do diff --git a/backtester/engine/backtest.go b/backtester/engine/backtest.go index ad9a9f71..beefe1b0 100644 --- a/backtester/engine/backtest.go +++ b/backtester/engine/backtest.go @@ -622,6 +622,9 @@ func (bt *BackTest) Stop() error { log.Errorf(common.Backtester, "Could not close all positions on stop: %s", err) } } + if !bt.hasProcessedAnEvent { + return nil + } err := bt.Statistic.CalculateAllResults() if err != nil { return err diff --git a/backtester/engine/backtest_test.go b/backtester/engine/backtest_test.go index d14af7b1..e20c5994 100644 --- a/backtester/engine/backtest_test.go +++ b/backtester/engine/backtest_test.go @@ -35,6 +35,7 @@ import ( "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/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/drivers" @@ -442,11 +443,7 @@ func TestFullCycle(t *testing.T) { tt := time.Now() stats := &statistics.Statistic{} - stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - stats.ExchangeAssetPairStatistics[ex][a] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - stats.ExchangeAssetPairStatistics[ex][a][cp.Base.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic) - + stats.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) port, err := portfolio.Setup(&size.Size{ BuySide: exchange.MinMax{}, SellSide: exchange.MinMax{}, @@ -580,10 +577,7 @@ func TestFullCycleMulti(t *testing.T) { tt := time.Now() stats := &statistics.Statistic{} - stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - stats.ExchangeAssetPairStatistics[ex][a] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - stats.ExchangeAssetPairStatistics[ex][a][cp.Base.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic) + stats.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) port, err := portfolio.Setup(&size.Size{ BuySide: exchange.MinMax{}, diff --git a/backtester/engine/setup.go b/backtester/engine/setup.go index 81dd99a0..d0a3c3d0 100644 --- a/backtester/engine/setup.go +++ b/backtester/engine/setup.go @@ -32,6 +32,7 @@ import ( "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/common/key" gctconfig "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" gctdatabase "github.com/thrasher-corp/gocryptotrader/database" @@ -192,7 +193,7 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str } portfolioRisk := &risk.Risk{ - CurrencySettings: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*risk.CurrencySettings), + CurrencySettings: make(map[key.ExchangePairAsset]*risk.CurrencySettings), } bt.Funding = funds @@ -220,9 +221,6 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str } 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.Item]map[*currency.Item]*risk.CurrencySettings) - } a := cfg.CurrencySettings[i].Asset if !a.IsValid() { return fmt.Errorf( @@ -234,12 +232,10 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str cfg.CurrencySettings[i].Quote, err) } - if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] == nil { - portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] = make(map[*currency.Item]map[*currency.Item]*risk.CurrencySettings) - } - if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][cfg.CurrencySettings[i].Base.Item] == nil { - portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][cfg.CurrencySettings[i].Base.Item] = make(map[*currency.Item]*risk.CurrencySettings) + if portfolioRisk.CurrencySettings == nil { + portfolioRisk.CurrencySettings = make(map[key.ExchangePairAsset]*risk.CurrencySettings) } + var curr currency.Pair var b, q currency.Code b = cfg.CurrencySettings[i].Base @@ -264,7 +260,12 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str portSet.MaximumOrdersWithLeverageRatio = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio portSet.MaxLeverageRate = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate } - portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][curr.Base.Item][curr.Quote.Item] = portSet + portfolioRisk.CurrencySettings[key.ExchangePairAsset{ + Exchange: cfg.CurrencySettings[i].ExchangeName, + Base: cfg.CurrencySettings[i].Base.Item, + Quote: cfg.CurrencySettings[i].Quote.Item, + Asset: a, + }] = portSet if cfg.CurrencySettings[i].MakerFee != nil && cfg.CurrencySettings[i].TakerFee != nil && cfg.CurrencySettings[i].MakerFee.GreaterThan(*cfg.CurrencySettings[i].TakerFee) { @@ -396,7 +397,7 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str StrategyNickname: cfg.Nickname, StrategyDescription: bt.Strategy.Description(), StrategyGoal: cfg.Goal, - ExchangeAssetPairStatistics: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic), + ExchangeAssetPairStatistics: make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic), RiskFreeRate: cfg.StatisticSettings.RiskFreeRate, CandleInterval: cfg.DataSettings.Interval, FundManager: bt.Funding, diff --git a/backtester/eventhandlers/portfolio/portfolio.go b/backtester/eventhandlers/portfolio/portfolio.go index 4a09b5a4..a10e8dfa 100644 --- a/backtester/eventhandlers/portfolio/portfolio.go +++ b/backtester/eventhandlers/portfolio/portfolio.go @@ -18,7 +18,7 @@ import ( "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/config" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" @@ -56,7 +56,12 @@ func (p *Portfolio) OnSignal(ev signal.Event, exchangeSettings *exchange.Setting return o, errInvalidDirection } - lookup := p.exchangeAssetPairPortfolioSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair().Base.Item][ev.Pair().Quote.Item] + lookup := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: ev.GetExchange(), + Base: ev.Pair().Base.Item, + Quote: ev.Pair().Quote.Item, + Asset: ev.GetAssetType(), + }] if lookup == nil { return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, @@ -235,7 +240,12 @@ func (p *Portfolio) OnFill(ev fill.Event, funds funding.IFundReleaser) (fill.Eve if ev == nil { return nil, common.ErrNilEvent } - lookup := p.exchangeAssetPairPortfolioSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair().Base.Item][ev.Pair().Quote.Item] + lookup := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: ev.GetExchange(), + Base: ev.Pair().Base.Item, + Quote: ev.Pair().Quote.Item, + Asset: ev.GetAssetType(), + }] if lookup == nil { return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, ev.GetExchange(), ev.GetAssetType(), ev.Pair()) } @@ -299,25 +309,24 @@ func (p *Portfolio) addComplianceSnapshot(fillEvent fill.Event) error { } // GetLatestOrderSnapshotForEvent gets orders related to the event -func (p *Portfolio) GetLatestOrderSnapshotForEvent(e common.Event) (compliance.Snapshot, error) { - eapSettings, ok := p.exchangeAssetPairPortfolioSettings[e.GetExchange()][e.GetAssetType()][e.Pair().Base.Item][e.Pair().Quote.Item] +func (p *Portfolio) GetLatestOrderSnapshotForEvent(ev common.Event) (compliance.Snapshot, error) { + eapSettings, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: ev.GetExchange(), + Base: ev.Pair().Base.Item, + Quote: ev.Pair().Quote.Item, + Asset: ev.GetAssetType(), + }] if !ok { - return compliance.Snapshot{}, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, e.GetExchange(), e.GetAssetType(), e.Pair()) + return compliance.Snapshot{}, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, ev.GetExchange(), ev.GetAssetType(), ev.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.exchangeAssetPairPortfolioSettings { - for _, assetMap := range exchangeMap { - for _, baseMap := range assetMap { - for _, quoteMap := range baseMap { - resp = append(resp, quoteMap.ComplianceManager.GetLatestSnapshot()) - } - } - } + resp := make([]compliance.Snapshot, 0, len(p.exchangeAssetPairPortfolioSettings)) + for _, d := range p.exchangeAssetPairPortfolioSettings { + resp = append(resp, d.ComplianceManager.GetLatestSnapshot()) } if len(resp) == 0 { return nil, errNoPortfolioSettings @@ -338,7 +347,12 @@ func (p *Portfolio) GetLatestComplianceSnapshot(exchangeName string, a asset.Ite // 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.exchangeAssetPairPortfolioSettings[exchangeName][a][cp.Base.Item][cp.Quote.Item] + lookup := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: exchangeName, + Base: cp.Base.Item, + Quote: cp.Quote.Item, + Asset: a, + }] if lookup == nil { return nil, fmt.Errorf("%w for %v %v %v could not retrieve compliance manager", errNoPortfolioSettings, exchangeName, a, cp) } @@ -504,80 +518,75 @@ func (p *Portfolio) CreateLiquidationOrdersForExchange(ev data.Event, funds fund return nil, fmt.Errorf("%w, requires funding manager", gctcommon.ErrNilPointer) } var closingOrders []order.Event - assetPairSettings, ok := p.exchangeAssetPairPortfolioSettings[ev.GetExchange()] - if !ok { - return nil, config.ErrExchangeNotFound - } - for item, baseMap := range assetPairSettings { - for b, quoteMap := range baseMap { - for q, settings := range quoteMap { - switch { - case item.IsFutures(): - positions := settings.FuturesTracker.GetPositions() - if len(positions) == 0 { - continue - } - pos := positions[len(positions)-1] - if !pos.LatestSize.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.LatestSize, - AllocatedFunds: pos.LatestSize, - OrderType: gctorder.Market, - LiquidatingPosition: true, - }) - case item == asset.Spot: - allFunds, err := funds.GetAllFunding() - if err != nil { - return nil, err - } - 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 - } - cp := currency.NewPair(b.Currency(), q.Currency()) - closingOrders = append(closingOrders, &order.Order{ - Base: &event.Base{ - Offset: ev.GetOffset(), - Exchange: ev.GetExchange(), - Time: ev.GetTime(), - Interval: ev.GetInterval(), - CurrencyPair: cp, - AssetType: item, - Reasons: []string{"LIQUIDATED"}, - }, - Direction: gctorder.Sell, - Status: gctorder.Liquidated, - Amount: allFunds[i].Available, - OrderType: gctorder.Market, - AllocatedFunds: allFunds[i].Available, - LiquidatingPosition: true, - }) - } + for mapKey, settings := range p.exchangeAssetPairPortfolioSettings { + if !mapKey.MatchesExchange(ev.GetExchange()) { + continue + } + switch { + case mapKey.Asset.IsFutures(): + positions := settings.FuturesTracker.GetPositions() + if len(positions) == 0 { + continue + } + pos := positions[len(positions)-1] + if !pos.LatestSize.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.LatestSize, + AllocatedFunds: pos.LatestSize, + OrderType: gctorder.Market, + LiquidatingPosition: true, + }) + case mapKey.Asset == asset.Spot: + allFunds, err := funds.GetAllFunding() + if err != nil { + return nil, err + } + 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 + } + cp := currency.NewPair(mapKey.Base.Currency(), mapKey.Quote.Currency()) + closingOrders = append(closingOrders, &order.Order{ + Base: &event.Base{ + Offset: ev.GetOffset(), + Exchange: ev.GetExchange(), + Time: ev.GetTime(), + Interval: ev.GetInterval(), + CurrencyPair: cp, + AssetType: mapKey.Asset, + Reasons: []string{"LIQUIDATED"}, + }, + Direction: gctorder.Sell, + Status: gctorder.Liquidated, + Amount: allFunds[i].Available, + OrderType: gctorder.Market, + AllocatedFunds: allFunds[i].Available, + LiquidatingPosition: true, + }) } } } @@ -606,7 +615,12 @@ func (p *Portfolio) getFuturesSettingsFromEvent(e common.Event) (*Settings, erro func (p *Portfolio) getSettings(exch string, item asset.Item, pair currency.Pair) (*Settings, error) { exch = strings.ToLower(exch) - settings, ok := p.exchangeAssetPairPortfolioSettings[exch][item][pair.Base.Item][pair.Quote.Item] + settings, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: exch, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: item, + }] if !ok { return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, exch, item, pair) } @@ -658,20 +672,15 @@ func (p *Portfolio) UpdateHoldings(e data.Event, funds funding.IFundReleaser) er // GetLatestHoldingsForAllCurrencies will return the current holdings for all loaded currencies // this is useful to assess the position of your entire portfolio in order to help with risk decisions func (p *Portfolio) GetLatestHoldingsForAllCurrencies() []holdings.Holding { - var resp []holdings.Holding - for _, exchangeMap := range p.exchangeAssetPairPortfolioSettings { - for _, assetMap := range exchangeMap { - for _, baseMap := range assetMap { - for _, quoteMap := range baseMap { - holds, err := quoteMap.GetLatestHoldings() - if err != nil { - continue - } - resp = append(resp, *holds) - } - } + resp := make([]holdings.Holding, 0, len(p.exchangeAssetPairPortfolioSettings)) + for _, d := range p.exchangeAssetPairPortfolioSettings { + holds, err := d.GetLatestHoldings() + if err != nil { + continue } + resp = append(resp, *holds) } + return resp } diff --git a/backtester/eventhandlers/portfolio/portfolio_test.go b/backtester/eventhandlers/portfolio/portfolio_test.go index 66d4256c..35bbc7ff 100644 --- a/backtester/eventhandlers/portfolio/portfolio_test.go +++ b/backtester/eventhandlers/portfolio/portfolio_test.go @@ -19,7 +19,7 @@ import ( "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/config" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/binance" @@ -35,7 +35,7 @@ var leet = decimal.NewFromInt(1337) func TestReset(t *testing.T) { t.Parallel() p := &Portfolio{ - exchangeAssetPairPortfolioSettings: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings), + exchangeAssetPairPortfolioSettings: make(map[key.ExchangePairAsset]*Settings), } err := p.Reset() if !errors.Is(err, nil) { @@ -674,7 +674,12 @@ func TestGetSnapshotAtTime(t *testing.T) { t.Errorf("received: %v, expected: %v", err, nil) } tt := time.Now() - s, ok := p.exchangeAssetPairPortfolioSettings[testExchange][asset.Spot][cp.Base.Item][cp.Quote.Item] + s, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: testExchange, + Base: cp.Base.Item, + Quote: cp.Quote.Item, + Asset: asset.Spot, + }] if !ok { t.Fatal("couldn't get settings") } @@ -728,7 +733,12 @@ func TestGetLatestSnapshot(t *testing.T) { if !errors.Is(err, nil) { t.Errorf("received: %v, expected: %v", err, nil) } - s, ok := p.exchangeAssetPairPortfolioSettings[testExchange][asset.Spot][cp.Base.Item][cp.Quote.Item] + s, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: testExchange, + Base: cp.Base.Item, + Quote: cp.Quote.Item, + Asset: asset.Spot, + }] if !ok { t.Fatal("couldn't get settings") } @@ -843,11 +853,13 @@ func TestCalculatePNL(t *testing.T) { FuturesTracker: mpt, } - p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType] = make(map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][pair.Base.Item] = make(map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][pair.Base.Item][pair.Quote.Item] = s + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) + p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: testExchange, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: ev.AssetType, + }] = s ev.Close = leet err = s.ComplianceManager.AddSnapshot(&compliance.Snapshot{ Timestamp: tt0, @@ -1123,11 +1135,13 @@ func TestGetLatestPNLForEvent(t *testing.T) { FuturesTracker: mpt, } - p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType] = make(map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][ev.Pair().Base.Item] = make(map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][ev.Pair().Base.Item][ev.Pair().Quote.Item] = s + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) + p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: testExchange, + Base: ev.Pair().Base.Item, + Quote: ev.Pair().Quote.Item, + Asset: asset.Futures, + }] = s expectedError = nil err = s.FuturesTracker.TrackNewOrder(&gctorder.Detail{ Exchange: ev.GetExchange(), @@ -1408,10 +1422,9 @@ 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) + if !errors.Is(err, common.ErrNilEvent) { + t.Fatalf("received '%v' expected '%v'", err, common.ErrNilEvent) } b := &event.Base{} @@ -1419,23 +1432,20 @@ func TestCreateLiquidationOrdersForExchange(t *testing.T) { ev := &kline.Kline{ Base: b, } - expectedError = gctcommon.ErrNilPointer _, err = p.CreateLiquidationOrdersForExchange(ev, nil) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v'", err, expectedError) + if !errors.Is(err, gctcommon.ErrNilPointer) { + t.Fatalf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer) } funds := &funding.FundManager{} - expectedError = config.ErrExchangeNotFound _, err = p.CreateLiquidationOrdersForExchange(ev, funds) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v'", err, nil) } ff := &binance.Binance{} ff.Name = testExchange cp := currency.NewPair(currency.BTC, currency.USDT) - expectedError = nil err = p.SetCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Futures, Pair: cp}) if !errors.Is(err, gctcommon.ErrNotYetImplemented) { t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNotYetImplemented) @@ -1446,8 +1456,8 @@ func TestCreateLiquidationOrdersForExchange(t *testing.T) { } ev.Exchange = ff.Name _, err = p.CreateLiquidationOrdersForExchange(ev, funds) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v'", err, nil) } _, err = p.getSettings(ff.Name, asset.Futures, cp) @@ -1482,42 +1492,44 @@ func TestCreateLiquidationOrdersForExchange(t *testing.T) { } err = settings.FuturesTracker.TrackNewOrder(od) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v', expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Errorf("received '%v', expected '%v'", err, nil) } - p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType] = make(map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][ev.Pair().Base.Item] = make(map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][ev.Pair().Base.Item][ev.Pair().Quote.Item] = settings + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) + p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: testExchange, + Base: ev.Pair().Base.Item, + Quote: ev.Pair().Quote.Item, + Asset: asset.Spot, + }] = settings 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) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v'", err, nil) } // 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) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v'", err, nil) } err = funds.AddItem(item) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v'", err, nil) } err = item.IncreaseAvailable(leet) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v'", err, nil) } orders, err := p.CreateLiquidationOrdersForExchange(ev, funds) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v'", err, nil) } - if len(orders) != 0 { - t.Errorf("expected two orders generated, received '%v'", len(orders)) + if len(orders) != 1 { + t.Errorf("expected one order generated, received '%v'", len(orders)) } } @@ -1537,48 +1549,43 @@ func TestGetPositionStatus(t *testing.T) { 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) + if !errors.Is(err, common.ErrNilEvent) { + t.Errorf("received '%v', expected '%v'", err, common.ErrNilEvent) } ev := &kline.Kline{ Base: &event.Base{}, } - expectedError = gctcommon.ErrNilPointer err = p.CheckLiquidationStatus(ev, nil, nil) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v', expected '%v'", err, expectedError) + if !errors.Is(err, gctcommon.ErrNilPointer) { + t.Errorf("received '%v', expected '%v'", err, gctcommon.ErrNilPointer) } item := asset.Futures pair := currency.NewPair(currency.BTC, currency.USDT) - 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) + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v", err, nil) } 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) + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v", err, nil) } collat, err := funding.CreateCollateral(contract, collateral) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v", err, nil) } - expectedError = gctcommon.ErrNilPointer err = p.CheckLiquidationStatus(ev, collat, nil) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v', expected '%v'", err, expectedError) + if !errors.Is(err, gctcommon.ErrNilPointer) { + t.Errorf("received '%v', expected '%v'", err, gctcommon.ErrNilPointer) } pnl := &PNLSummary{} - expectedError = futures.ErrNotFuturesAsset err = p.CheckLiquidationStatus(ev, collat, pnl) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v', expected '%v'", err, expectedError) + if !errors.Is(err, futures.ErrNotFuturesAsset) { + t.Errorf("received '%v', expected '%v'", err, futures.ErrNotFuturesAsset) } pnl.Asset = asset.Futures @@ -1587,7 +1594,6 @@ func TestCheckLiquidationStatus(t *testing.T) { ev.CurrencyPair = pair exch := &binance.Binance{} exch.Name = ev.Exchange - expectedError = nil err = p.SetCurrencySettingsMap(&exchange.Settings{Exchange: exch, Asset: asset.Futures, Pair: pair}) if !errors.Is(err, gctcommon.ErrNotYetImplemented) { t.Errorf("received '%v', expected '%v'", err, gctcommon.ErrNotYetImplemented) @@ -1622,17 +1628,19 @@ func TestCheckLiquidationStatus(t *testing.T) { } err = settings.FuturesTracker.TrackNewOrder(od) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v', expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Errorf("received '%v', expected '%v'", err, nil) } - p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType] = make(map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][pair.Base.Item] = make(map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[testExchange][ev.AssetType][pair.Base.Item][pair.Quote.Item] = settings + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) + p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: testExchange, + Base: ev.Pair().Base.Item, + Quote: ev.Pair().Quote.Item, + Asset: asset.Futures, + }] = settings err = p.CheckLiquidationStatus(ev, collat, pnl) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v', expected '%v'", err, expectedError) + if !errors.Is(err, nil) { + t.Errorf("received '%v', expected '%v'", err, nil) } } diff --git a/backtester/eventhandlers/portfolio/portfolio_types.go b/backtester/eventhandlers/portfolio/portfolio_types.go index ad159afb..394c01a5 100644 --- a/backtester/eventhandlers/portfolio/portfolio_types.go +++ b/backtester/eventhandlers/portfolio/portfolio_types.go @@ -15,6 +15,7 @@ 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/common/key" "github.com/thrasher-corp/gocryptotrader/currency" gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -44,7 +45,7 @@ type Portfolio struct { riskFreeRate decimal.Decimal sizeManager SizeHandler riskManager risk.Handler - exchangeAssetPairPortfolioSettings map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings + exchangeAssetPairPortfolioSettings map[key.ExchangePairAsset]*Settings } // Handler contains all functions expected to operate a portfolio manager diff --git a/backtester/eventhandlers/portfolio/risk/risk.go b/backtester/eventhandlers/portfolio/risk/risk.go index 8b8a362c..fb6e71b3 100644 --- a/backtester/eventhandlers/portfolio/risk/risk.go +++ b/backtester/eventhandlers/portfolio/risk/risk.go @@ -9,6 +9,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings" "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order" gctcommon "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" ) @@ -25,7 +26,12 @@ func (r *Risk) EvaluateOrder(o order.Event, latestHoldings []holdings.Holding, s ex := o.GetExchange() a := o.GetAssetType() p := o.Pair().Format(currency.EMPTYFORMAT) - lookup, ok := r.CurrencySettings[ex][a][p.Base.Item][p.Quote.Item] + lookup, ok := r.CurrencySettings[key.ExchangePairAsset{ + Exchange: ex, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] if !ok { return nil, fmt.Errorf("%v %v %v %w", ex, a, p, errNoCurrencySettings) } diff --git a/backtester/eventhandlers/portfolio/risk/risk_test.go b/backtester/eventhandlers/portfolio/risk/risk_test.go index 118a26d5..9e8e32cb 100644 --- a/backtester/eventhandlers/portfolio/risk/risk_test.go +++ b/backtester/eventhandlers/portfolio/risk/risk_test.go @@ -10,6 +10,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event" "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order" gctcommon "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -68,16 +69,18 @@ func TestEvaluateOrder(t *testing.T) { }, } h := []holdings.Holding{} - r.CurrencySettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencySettings) - r.CurrencySettings[e] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencySettings) - r.CurrencySettings[e][a] = make(map[*currency.Item]map[*currency.Item]*CurrencySettings) - r.CurrencySettings[e][a][p.Base.Item] = make(map[*currency.Item]*CurrencySettings) + r.CurrencySettings = make(map[key.ExchangePairAsset]*CurrencySettings) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) if !errors.Is(err, errNoCurrencySettings) { t.Error(err) } - r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item] = &CurrencySettings{ + r.CurrencySettings[key.ExchangePairAsset{ + Exchange: e, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] = &CurrencySettings{ MaximumOrdersWithLeverageRatio: decimal.NewFromFloat(0.3), MaxLeverageRate: decimal.NewFromFloat(0.3), MaximumHoldingRatio: decimal.NewFromFloat(0.3), @@ -96,7 +99,12 @@ func TestEvaluateOrder(t *testing.T) { Pair: currency.NewPair(currency.DOGE, currency.USDT), }) o.Leverage = decimal.NewFromFloat(1.1) - r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaximumHoldingRatio = decimal.Zero + r.CurrencySettings[key.ExchangePairAsset{ + Exchange: e, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }].MaximumHoldingRatio = decimal.Zero _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) if !errors.Is(err, errLeverageNotAllowed) { t.Error(err) @@ -108,14 +116,24 @@ func TestEvaluateOrder(t *testing.T) { } r.MaximumLeverage = decimal.NewFromInt(33) - r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaxLeverageRate = decimal.NewFromInt(33) + r.CurrencySettings[key.ExchangePairAsset{ + Exchange: e, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }].MaxLeverageRate = decimal.NewFromInt(33) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) if !errors.Is(err, nil) { t.Errorf("received: %v, expected: %v", err, nil) } r.MaximumLeverage = decimal.NewFromInt(33) - r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaxLeverageRate = decimal.NewFromInt(33) + r.CurrencySettings[key.ExchangePairAsset{ + Exchange: e, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }].MaxLeverageRate = decimal.NewFromInt(33) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ @@ -131,7 +149,12 @@ func TestEvaluateOrder(t *testing.T) { } h = append(h, holdings.Holding{Pair: p, BaseValue: decimal.NewFromInt(1337)}, holdings.Holding{Pair: p, BaseValue: decimal.NewFromFloat(1337.42)}) - r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaximumHoldingRatio = decimal.NewFromFloat(0.1) + r.CurrencySettings[key.ExchangePairAsset{ + Exchange: e, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }].MaximumHoldingRatio = decimal.NewFromFloat(0.1) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) if !errors.Is(err, nil) { t.Errorf("received: %v, expected: %v", err, nil) diff --git a/backtester/eventhandlers/portfolio/risk/risk_types.go b/backtester/eventhandlers/portfolio/risk/risk_types.go index fe7538c2..b1019a69 100644 --- a/backtester/eventhandlers/portfolio/risk/risk_types.go +++ b/backtester/eventhandlers/portfolio/risk/risk_types.go @@ -7,8 +7,7 @@ import ( "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/order" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/common/key" ) var ( @@ -24,7 +23,7 @@ type Handler interface { // Risk contains all currency settings in order to evaluate potential orders type Risk struct { - CurrencySettings map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencySettings + CurrencySettings map[key.ExchangePairAsset]*CurrencySettings CanUseLeverage bool MaximumLeverage decimal.Decimal } diff --git a/backtester/eventhandlers/portfolio/setup.go b/backtester/eventhandlers/portfolio/setup.go index ebbe6d34..a40bc648 100644 --- a/backtester/eventhandlers/portfolio/setup.go +++ b/backtester/eventhandlers/portfolio/setup.go @@ -8,7 +8,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk" gctcommon "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" ) @@ -37,7 +37,7 @@ func (p *Portfolio) Reset() error { if p == nil { return gctcommon.ErrNilPointer } - p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) p.riskFreeRate = decimal.Zero p.sizeManager = nil p.riskManager = nil @@ -60,24 +60,10 @@ func (p *Portfolio) SetCurrencySettingsMap(setup *exchange.Settings) error { } if p.exchangeAssetPairPortfolioSettings == nil { - p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) } name := strings.ToLower(setup.Exchange.GetName()) - m, ok := p.exchangeAssetPairPortfolioSettings[name] - if !ok { - m = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings) - p.exchangeAssetPairPortfolioSettings[name] = m - } - m2, ok := m[setup.Asset] - if !ok { - m2 = make(map[*currency.Item]map[*currency.Item]*Settings) - m[setup.Asset] = m2 - } - m3, ok := m2[setup.Pair.Base.Item] - if !ok { - m3 = make(map[*currency.Item]*Settings) - m2[setup.Pair.Base.Item] = m3 - } + settings := &Settings{ Exchange: setup.Exchange, exchangeName: name, @@ -112,6 +98,11 @@ func (p *Portfolio) SetCurrencySettingsMap(setup *exchange.Settings) error { } settings.FuturesTracker = tracker } - m3[setup.Pair.Quote.Item] = settings + p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ + Exchange: name, + Base: setup.Pair.Base.Item, + Quote: setup.Pair.Quote.Item, + Asset: setup.Asset, + }] = settings return nil } diff --git a/backtester/eventhandlers/statistics/currencystatistics.go b/backtester/eventhandlers/statistics/currencystatistics.go index b4a46dfe..5e5fffe3 100644 --- a/backtester/eventhandlers/statistics/currencystatistics.go +++ b/backtester/eventhandlers/statistics/currencystatistics.go @@ -15,6 +15,11 @@ import ( // CalculateResults calculates all statistics for the exchange, asset, currency pair func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) error { first := c.Events[0] + if first.DataEvent == nil { + // you can call stop while a backtester run is running + // if the first data event isn't present, then it hasn't been properly run + return errNoDataAtOffset + } sep := fmt.Sprintf("%v %v %v |\t", first.DataEvent.GetExchange(), first.DataEvent.GetAssetType(), first.DataEvent.Pair()) firstPrice := first.ClosePrice diff --git a/backtester/eventhandlers/statistics/currencystatistics_test.go b/backtester/eventhandlers/statistics/currencystatistics_test.go index cebca665..0169ad2c 100644 --- a/backtester/eventhandlers/statistics/currencystatistics_test.go +++ b/backtester/eventhandlers/statistics/currencystatistics_test.go @@ -157,7 +157,7 @@ func TestCalculateResults(t *testing.T) { } } -func TestPrintResults(_ *testing.T) { +func TestPrintResults(t *testing.T) { cs := CurrencyPairStatistic{} tt1 := time.Now() tt2 := time.Now().Add(gctkline.OneDay.Duration()) @@ -249,7 +249,10 @@ func TestPrintResults(_ *testing.T) { } cs.Events = append(cs.Events, ev, ev2) - cs.PrintResults(exch, a, p, true) + err := cs.PrintResults(exch, a, p, true) + if err != nil { + t.Error(err) + } } func TestCalculateHighestCommittedFunds(t *testing.T) { diff --git a/backtester/eventhandlers/statistics/fundingstatistics.go b/backtester/eventhandlers/statistics/fundingstatistics.go index c4474316..98a00975 100644 --- a/backtester/eventhandlers/statistics/fundingstatistics.go +++ b/backtester/eventhandlers/statistics/fundingstatistics.go @@ -8,15 +8,14 @@ import ( "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/backtester/funding" gctcommon "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" 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" ) // CalculateFundingStatistics calculates funding statistics for total USD strategy results // along with individual funding item statistics -func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic, riskFreeRate decimal.Decimal, interval gctkline.Interval) (*FundingStatistics, error) { +func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[key.ExchangePairAsset]*CurrencyPairStatistic, riskFreeRate decimal.Decimal, interval gctkline.Interval) (*FundingStatistics, error) { if currStats == nil { return nil, gctcommon.ErrNilPointer } @@ -31,28 +30,25 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str Report: report, } for i := range report.Items { - exchangeAssetStats, ok := currStats[report.Items[i].Exchange][report.Items[i].Asset] - if !ok { - if report.Items[i].AppendedViaAPI { - // items added via API may not have been processed along with typical events - // are not relevant to calculating statistics - continue - } - return nil, fmt.Errorf("%w for %v %v", - errNoRelevantStatsFound, - report.Items[i].Exchange, - report.Items[i].Asset) - } var relevantStats []relatedCurrencyPairStatistics - for b, baseMap := range exchangeAssetStats { - for q, v := range baseMap { - if b.Currency().Equal(report.Items[i].Currency) { - relevantStats = append(relevantStats, relatedCurrencyPairStatistics{isBaseCurrency: true, stat: v}) + for k, v := range currStats { + if !k.MatchesExchangeAsset(report.Items[0].Exchange, report.Items[0].Asset) { + if report.Items[i].AppendedViaAPI { + // items added via API may not have been processed along with typical events + // are not relevant to calculating statistics continue } - if q.Currency().Equal(report.Items[i].Currency) { - relevantStats = append(relevantStats, relatedCurrencyPairStatistics{stat: v}) - } + return nil, fmt.Errorf("%w for %v %v", + errNoRelevantStatsFound, + report.Items[i].Exchange, + report.Items[i].Asset) + } + if k.Base.Currency().Equal(report.Items[i].Currency) { + relevantStats = append(relevantStats, relatedCurrencyPairStatistics{isBaseCurrency: true, stat: v}) + continue + } + if k.Quote.Currency().Equal(report.Items[i].Currency) { + relevantStats = append(relevantStats, relatedCurrencyPairStatistics{stat: v}) } } var fundingStat *FundingItemStatistics diff --git a/backtester/eventhandlers/statistics/fundingstatistics_test.go b/backtester/eventhandlers/statistics/fundingstatistics_test.go index 7f702ef3..a10fa666 100644 --- a/backtester/eventhandlers/statistics/fundingstatistics_test.go +++ b/backtester/eventhandlers/statistics/fundingstatistics_test.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/data/kline" "github.com/thrasher-corp/gocryptotrader/backtester/funding" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -77,10 +78,10 @@ func TestCalculateFundingStatistics(t *testing.T) { t.Errorf("received %v expected %v", err, funding.ErrUSDTrackingDisabled) } - cs := make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + cs := make(map[key.ExchangePairAsset]*CurrencyPairStatistic) _, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour) - if !errors.Is(err, errNoRelevantStatsFound) { - t.Errorf("received %v expected %v", err, errNoRelevantStatsFound) + if !errors.Is(err, nil) { + t.Errorf("received %v expected %v", err, nil) } f, err = funding.SetupFundingManager(&engine.ExchangeManager{}, true, false, false) @@ -99,10 +100,12 @@ func TestCalculateFundingStatistics(t *testing.T) { if !errors.Is(err, nil) { t.Errorf("received %v expected %v", err, nil) } - cs["binance"] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) - cs["binance"][asset.Spot] = make(map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) - cs["binance"][asset.Spot][currency.LTC.Item] = make(map[*currency.Item]*CurrencyPairStatistic) - cs["binance"][asset.Spot][currency.LTC.Item][currency.USD.Item] = &CurrencyPairStatistic{} + cs[key.ExchangePairAsset{ + Exchange: "binance", + Base: currency.LTC.Item, + Quote: currency.USD.Item, + Asset: asset.Spot, + }] = &CurrencyPairStatistic{} _, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour) if !errors.Is(err, errMissingSnapshots) { t.Errorf("received %v expected %v", err, errMissingSnapshots) @@ -115,9 +118,12 @@ func TestCalculateFundingStatistics(t *testing.T) { if !errors.Is(err, nil) { t.Errorf("received %v expected %v", err, nil) } - cs["binance"][asset.Spot][currency.BTC.Item] = make(map[*currency.Item]*CurrencyPairStatistic) - cs["binance"][asset.Spot][currency.BTC.Item][currency.USDT.Item] = &CurrencyPairStatistic{} - + cs[key.ExchangePairAsset{ + Exchange: "binance", + Base: currency.LTC.Item, + Quote: currency.USD.Item, + Asset: asset.Spot, + }] = &CurrencyPairStatistic{} _, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour) 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 index f34cbb8b..583fb27c 100644 --- a/backtester/eventhandlers/statistics/printresults.go +++ b/backtester/eventhandlers/statistics/printresults.go @@ -74,41 +74,35 @@ func (s *Statistic) PrintAllEventsChronologically() { log.Infoln(common.Statistics, common.CMDColours.H1+"------------------Events-------------------------------------"+common.CMDColours.Default) var errs error var results []eventOutputHolder - for _, exchangeMap := range s.ExchangeAssetPairStatistics { - for _, assetMap := range exchangeMap { - for _, baseMap := range assetMap { - for _, currencyStatistic := range baseMap { - for i := range currencyStatistic.Events { - var result string - var tt time.Time - var err error - switch { - case currencyStatistic.Events[i].FillEvent != nil: - result, err = s.CreateLog(currencyStatistic.Events[i].FillEvent) - if err != nil { - errs = gctcommon.AppendError(errs, err) - continue - } - tt = currencyStatistic.Events[i].FillEvent.GetTime() - case currencyStatistic.Events[i].SignalEvent != nil: - result, err = s.CreateLog(currencyStatistic.Events[i].SignalEvent) - if err != nil { - errs = gctcommon.AppendError(errs, err) - continue - } - tt = currencyStatistic.Events[i].SignalEvent.GetTime() - case currencyStatistic.Events[i].DataEvent != nil: - result, err = s.CreateLog(currencyStatistic.Events[i].DataEvent) - if err != nil { - errs = gctcommon.AppendError(errs, err) - continue - } - tt = currencyStatistic.Events[i].DataEvent.GetTime() - } - results = addEventOutputToTime(results, tt, result) - } + for _, currencyStatistic := range s.ExchangeAssetPairStatistics { + for i := range currencyStatistic.Events { + var result string + var tt time.Time + var err error + switch { + case currencyStatistic.Events[i].FillEvent != nil: + result, err = s.CreateLog(currencyStatistic.Events[i].FillEvent) + if err != nil { + errs = gctcommon.AppendError(errs, err) + continue } + tt = currencyStatistic.Events[i].FillEvent.GetTime() + case currencyStatistic.Events[i].SignalEvent != nil: + result, err = s.CreateLog(currencyStatistic.Events[i].SignalEvent) + if err != nil { + errs = gctcommon.AppendError(errs, err) + continue + } + tt = currencyStatistic.Events[i].SignalEvent.GetTime() + case currencyStatistic.Events[i].DataEvent != nil: + result, err = s.CreateLog(currencyStatistic.Events[i].DataEvent) + if err != nil { + errs = gctcommon.AppendError(errs, err) + continue + } + tt = currencyStatistic.Events[i].DataEvent.GetTime() } + results = addEventOutputToTime(results, tt, result) } } @@ -206,12 +200,18 @@ func (s *Statistic) CreateLog(data common.Event) (string, error) { } // PrintResults outputs all calculated statistics to the command line -func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.Pair, usingExchangeLevelFunding bool) { +func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.Pair, usingExchangeLevelFunding bool) error { + if len(c.Events) == 0 { + return errCurrencyStatisticsUnset + } 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] + if first.DataEvent == nil { + return errNoDataAtOffset + } c.StartingClosePrice.Value = first.DataEvent.GetClosePrice() c.StartingClosePrice.Time = first.Time c.EndingClosePrice.Value = last.DataEvent.GetClosePrice() @@ -302,6 +302,7 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency. 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, ".", ",")) } + return nil } // PrintResults outputs all calculated funding statistics to the command line diff --git a/backtester/eventhandlers/statistics/statistics.go b/backtester/eventhandlers/statistics/statistics.go index 9e70b0da..9319bdb0 100644 --- a/backtester/eventhandlers/statistics/statistics.go +++ b/backtester/eventhandlers/statistics/statistics.go @@ -15,8 +15,8 @@ import ( "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/key" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -33,7 +33,7 @@ func (s *Statistic) Reset() error { s.EndDate = time.Time{} s.CandleInterval = 0 s.RiskFreeRate = decimal.Zero - s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) s.CurrencyStatistics = nil s.TotalBuyOrders = 0 s.TotalLongOrders = 0 @@ -62,46 +62,38 @@ func (s *Statistic) SetEventForOffset(ev common.Event) error { a := ev.GetAssetType() p := ev.Pair() if s.ExchangeAssetPairStatistics == nil { - s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) } - m, ok := s.ExchangeAssetPairStatistics[ex] - if !ok { - m = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) - s.ExchangeAssetPairStatistics[ex] = m + mapKey := key.ExchangePairAsset{ + Exchange: ex, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, } - m2, ok := m[a] + stats, ok := s.ExchangeAssetPairStatistics[mapKey] if !ok { - m2 = make(map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) - m[a] = m2 - } - m3, ok := m2[p.Base.Item] - if !ok { - m3 = make(map[*currency.Item]*CurrencyPairStatistic) - m2[p.Base.Item] = m3 - } - lookup, ok := m3[p.Quote.Item] - if !ok { - lookup = &CurrencyPairStatistic{ - Exchange: ev.GetExchange(), - Asset: ev.GetAssetType(), - Currency: ev.Pair(), + stats = &CurrencyPairStatistic{ + Exchange: ex, + Asset: a, + Currency: p, UnderlyingPair: ev.GetUnderlyingPair(), } - m3[p.Quote.Item] = lookup + s.ExchangeAssetPairStatistics[mapKey] = stats } - for i := range lookup.Events { - if lookup.Events[i].Offset != ev.GetOffset() { + + for i := range stats.Events { + if stats.Events[i].Offset != ev.GetOffset() { continue } - return applyEventAtOffset(ev, &lookup.Events[i]) + return applyEventAtOffset(ev, &stats.Events[i]) } // add to events and then apply the supplied event to it - lookup.Events = append(lookup.Events, DataAtOffset{ + stats.Events = append(stats.Events, DataAtOffset{ Offset: ev.GetOffset(), Time: ev.GetTime(), }) - err := applyEventAtOffset(ev, &lookup.Events[len(lookup.Events)-1]) + err := applyEventAtOffset(ev, &stats.Events[len(stats.Events)-1]) if err != nil { return err } @@ -146,7 +138,12 @@ func (s *Statistic) AddHoldingsForTime(h *holdings.Holding) error { if s.ExchangeAssetPairStatistics == nil { return errExchangeAssetPairStatsUnset } - lookup := s.ExchangeAssetPairStatistics[h.Exchange][h.Asset][h.Pair.Base.Item][h.Pair.Quote.Item] + lookup := s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: h.Exchange, + Base: h.Pair.Base.Item, + Quote: h.Pair.Quote.Item, + Asset: h.Asset, + }] if lookup == nil { return fmt.Errorf("%w for %v %v %v to set holding event", errCurrencyStatisticsUnset, h.Exchange, h.Asset, h.Pair) } @@ -167,7 +164,12 @@ func (s *Statistic) AddPNLForTime(pnl *portfolio.PNLSummary) error { if s.ExchangeAssetPairStatistics == nil { return errExchangeAssetPairStatsUnset } - lookup := s.ExchangeAssetPairStatistics[pnl.Exchange][pnl.Asset][pnl.Pair.Base.Item][pnl.Pair.Quote.Item] + lookup := s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: pnl.Exchange, + Base: pnl.Pair.Base.Item, + Quote: pnl.Pair.Quote.Item, + Asset: pnl.Asset, + }] if lookup == nil { return fmt.Errorf("%w for %v %v %v to set pnl", errCurrencyStatisticsUnset, pnl.Exchange, pnl.Asset, pnl.Pair) } @@ -195,7 +197,12 @@ func (s *Statistic) AddComplianceSnapshotForTime(c *compliance.Snapshot, e commo exch := e.GetExchange() a := e.GetAssetType() p := e.Pair() - lookup := s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item] + lookup := s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: exch, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] if lookup == nil { return fmt.Errorf("%w for %v %v %v to set compliance snapshot", errCurrencyStatisticsUnset, exch, a, p) } @@ -214,53 +221,50 @@ func (s *Statistic) CalculateAllResults() error { log.Infoln(common.Statistics, "Calculating backtesting results") s.PrintAllEventsChronologically() currCount := 0 - var finalResults []FinalResultsHolder + finalResults := make([]FinalResultsHolder, 0, len(s.ExchangeAssetPairStatistics)) var err error - for exchangeName, exchangeMap := range s.ExchangeAssetPairStatistics { - for assetItem, assetMap := range exchangeMap { - for b, baseMap := range assetMap { - for q, stats := range baseMap { - currCount++ - last := stats.Events[len(stats.Events)-1] - if last.PNL != nil { - s.HasCollateral = true - } - err = stats.CalculateResults(s.RiskFreeRate) - if err != nil { - log.Errorln(common.Statistics, err) - } - stats.FinalHoldings = last.Holdings - stats.InitialHoldings = stats.Events[0].Holdings - if last.ComplianceSnapshot == nil { - return errMissingSnapshots - } - stats.FinalOrders = *last.ComplianceSnapshot - s.StartDate = stats.Events[0].Time - s.EndDate = last.Time - cp := currency.NewPair(b.Currency(), q.Currency()) - stats.PrintResults(exchangeName, assetItem, cp, s.FundManager.IsUsingExchangeLevelFunding()) + for mapKey, stats := range s.ExchangeAssetPairStatistics { + currCount++ + last := stats.Events[len(stats.Events)-1] + if last.PNL != nil { + s.HasCollateral = true + } + err = stats.CalculateResults(s.RiskFreeRate) + if err != nil { + log.Errorln(common.Statistics, err) + } + stats.FinalHoldings = last.Holdings + stats.InitialHoldings = stats.Events[0].Holdings + if last.ComplianceSnapshot == nil { + return errMissingSnapshots + } + stats.FinalOrders = *last.ComplianceSnapshot + s.StartDate = stats.Events[0].Time + s.EndDate = last.Time + cp := currency.NewPair(mapKey.Base.Currency(), mapKey.Quote.Currency()) + err = stats.PrintResults(mapKey.Exchange, mapKey.Asset, cp, s.FundManager.IsUsingExchangeLevelFunding()) + if err != nil { + return err + } - finalResults = append(finalResults, FinalResultsHolder{ - Exchange: exchangeName, - Asset: assetItem, - Pair: cp, - MaxDrawdown: stats.MaxDrawdown, - MarketMovement: stats.MarketMovement, - StrategyMovement: stats.StrategyMovement, - }) - if assetItem.IsFutures() { - s.TotalLongOrders += stats.BuyOrders - s.TotalShortOrders += stats.SellOrders - } else { - s.TotalBuyOrders += stats.BuyOrders - s.TotalSellOrders += stats.SellOrders - } - s.TotalOrders += stats.TotalOrders - if stats.ShowMissingDataWarning { - s.WasAnyDataMissing = true - } - } - } + finalResults = append(finalResults, FinalResultsHolder{ + Exchange: mapKey.Exchange, + Asset: mapKey.Asset, + Pair: cp, + MaxDrawdown: stats.MaxDrawdown, + MarketMovement: stats.MarketMovement, + StrategyMovement: stats.StrategyMovement, + }) + if mapKey.Asset.IsFutures() { + s.TotalLongOrders += stats.BuyOrders + s.TotalShortOrders += stats.SellOrders + } else { + s.TotalBuyOrders += stats.BuyOrders + s.TotalSellOrders += stats.SellOrders + } + s.TotalOrders += stats.TotalOrders + if stats.ShowMissingDataWarning { + s.WasAnyDataMissing = true } } s.FundingStatistics, err = CalculateFundingStatistics(s.FundManager, s.ExchangeAssetPairStatistics, s.RiskFreeRate, s.CandleInterval) @@ -336,14 +340,8 @@ func (s *Statistic) SetStrategyName(name string) { // Serialise outputs the Statistic struct in json func (s *Statistic) Serialise() (string, error) { s.CurrencyStatistics = nil - for _, exchangeMap := range s.ExchangeAssetPairStatistics { - for _, assetMap := range exchangeMap { - for _, baseMap := range assetMap { - for _, stats := range baseMap { - s.CurrencyStatistics = append(s.CurrencyStatistics, stats) - } - } - } + for _, stats := range s.ExchangeAssetPairStatistics { + s.CurrencyStatistics = append(s.CurrencyStatistics, stats) } resp, err := json.MarshalIndent(s, "", " ") diff --git a/backtester/eventhandlers/statistics/statistics_test.go b/backtester/eventhandlers/statistics/statistics_test.go index 2b7b8142..02759b99 100644 --- a/backtester/eventhandlers/statistics/statistics_test.go +++ b/backtester/eventhandlers/statistics/statistics_test.go @@ -18,6 +18,7 @@ import ( "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/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -85,7 +86,12 @@ func TestAddDataEventForTime(t *testing.T) { if s.ExchangeAssetPairStatistics == nil { t.Error("expected not nil") } - if len(s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item].Events) != 1 { + if len(s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: exch, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }].Events) != 1 { t.Error("expected 1 event") } } @@ -105,7 +111,7 @@ func TestAddSignalEventForTime(t *testing.T) { if !errors.Is(err, common.ErrNilEvent) { t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent) } - s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) b := &event.Base{} err = s.SetEventForOffset(&signal.Signal{ Base: b, @@ -154,7 +160,7 @@ func TestAddExchangeEventForTime(t *testing.T) { if !errors.Is(err, common.ErrNilEvent) { t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent) } - s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) b := &event.Base{} b.Exchange = exch @@ -203,7 +209,7 @@ func TestAddFillEventForTime(t *testing.T) { if !errors.Is(err, common.ErrNilEvent) { t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent) } - s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) b := &event.Base{} err = s.SetEventForOffset(&fill.Fill{ Base: b, @@ -255,7 +261,7 @@ func TestAddHoldingsForTime(t *testing.T) { if !errors.Is(err, errExchangeAssetPairStatsUnset) { t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset) } - s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) err = s.AddHoldingsForTime(&holdings.Holding{}) if !errors.Is(err, errCurrencyStatisticsUnset) { t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset) @@ -324,7 +330,7 @@ func TestAddComplianceSnapshotForTime(t *testing.T) { if !errors.Is(err, errExchangeAssetPairStatsUnset) { t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset) } - s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) b := &event.Base{} err = s.AddComplianceSnapshotForTime(&compliance.Snapshot{}, &fill.Fill{Base: b}) if !errors.Is(err, errCurrencyStatisticsUnset) { @@ -726,10 +732,22 @@ func TestCalculateTheResults(t *testing.T) { t.Errorf("received: %v, expected: %v", err, nil) } - s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item].Events[1].Holdings.QuoteInitialFunds = eleet - s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item].Events[1].Holdings.TotalValue = eleeet - s.ExchangeAssetPairStatistics[exch][a][p2.Base.Item][p2.Quote.Item].Events[1].Holdings.QuoteInitialFunds = eleet - s.ExchangeAssetPairStatistics[exch][a][p2.Base.Item][p2.Quote.Item].Events[1].Holdings.TotalValue = eleeet + mapKey1 := key.ExchangePairAsset{ + Exchange: exch, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + } + mapKey2 := key.ExchangePairAsset{ + Exchange: exch, + Base: p2.Base.Item, + Quote: p2.Quote.Item, + Asset: a, + } + s.ExchangeAssetPairStatistics[mapKey1].Events[1].Holdings.QuoteInitialFunds = eleet + s.ExchangeAssetPairStatistics[mapKey1].Events[1].Holdings.TotalValue = eleeet + s.ExchangeAssetPairStatistics[mapKey2].Events[1].Holdings.QuoteInitialFunds = eleet + s.ExchangeAssetPairStatistics[mapKey2].Events[1].Holdings.TotalValue = eleeet funds, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, false, false) if !errors.Is(err, nil) { diff --git a/backtester/eventhandlers/statistics/statistics_types.go b/backtester/eventhandlers/statistics/statistics_types.go index 1464ee48..f1724a74 100644 --- a/backtester/eventhandlers/statistics/statistics_types.go +++ b/backtester/eventhandlers/statistics/statistics_types.go @@ -14,6 +14,7 @@ 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/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -34,28 +35,28 @@ var ( // Statistic holds all statistical information for a backtester run, from drawdowns to ratios. // Any currency specific information is handled in currencystatistics type Statistic struct { - StrategyName string `json:"strategy-name"` - StrategyDescription string `json:"strategy-description"` - StrategyNickname string `json:"strategy-nickname"` - StrategyGoal string `json:"strategy-goal"` - StartDate time.Time `json:"start-date"` - EndDate time.Time `json:"end-date"` - CandleInterval gctkline.Interval `json:"candle-interval"` - RiskFreeRate decimal.Decimal `json:"risk-free-rate"` - ExchangeAssetPairStatistics map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic `json:"exchange-asset-pair-statistics"` - CurrencyStatistics []*CurrencyPairStatistic `json:"currency-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"` - WasAnyDataMissing bool `json:"was-any-data-missing"` - FundingStatistics *FundingStatistics `json:"funding-statistics"` - FundManager funding.IFundingManager `json:"-"` - HasCollateral bool `json:"has-collateral"` + StrategyName string `json:"strategy-name"` + StrategyDescription string `json:"strategy-description"` + StrategyNickname string `json:"strategy-nickname"` + StrategyGoal string `json:"strategy-goal"` + StartDate time.Time `json:"start-date"` + EndDate time.Time `json:"end-date"` + CandleInterval gctkline.Interval `json:"candle-interval"` + RiskFreeRate decimal.Decimal `json:"risk-free-rate"` + ExchangeAssetPairStatistics map[key.ExchangePairAsset]*CurrencyPairStatistic `json:"-"` + CurrencyStatistics []*CurrencyPairStatistic `json:"currency-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"` + 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 diff --git a/backtester/report/chart.go b/backtester/report/chart.go index 35540857..ceefe29b 100644 --- a/backtester/report/chart.go +++ b/backtester/report/chart.go @@ -6,8 +6,8 @@ import ( "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics" gctcommon "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) // createUSDTotalsChart used for creating a chart in the HTML report @@ -91,53 +91,47 @@ func createHoldingsOverTimeChart(stats []statistics.FundingItemStatistics) (*Cha // createPNLCharts shows a running history of all realised and unrealised PNL values // over time -func createPNLCharts(items map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) (*Chart, error) { +func createPNLCharts(items map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) (*Chart, error) { if items == nil { return nil, fmt.Errorf("%w missing currency pair statistics", gctcommon.ErrNilPointer) } response := &Chart{ AxisType: "linear", } - for exch, assetMap := range items { - for item, baseMap := range assetMap { - for b, quoteMap := range baseMap { - for q, result := range quoteMap { - id := fmt.Sprintf("%v %v %v%v", - exch, - item, - b, - q) - uPNLName := fmt.Sprintf("%v Unrealised PNL", id) - rPNLName := fmt.Sprintf("%v Realised PNL", id) + for mapKey, result := range items { + id := fmt.Sprintf("%v %v %v%v", + mapKey.Exchange, + mapKey.Asset, + mapKey.Base, + mapKey.Quote) + 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) - } + 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.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) (*Chart, error) { +func createFuturesSpotDiffChart(items map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) (*Chart, error) { if items == nil { return nil, fmt.Errorf("%w missing currency pair statistics", gctcommon.ErrNilPointer) } @@ -146,32 +140,26 @@ func createFuturesSpotDiffChart(items map[string]map[asset.Item]map[*currency.It AxisType: "linear", } - for _, assetMap := range items { - for item, baseMap := range assetMap { - for b, quoteMap := range baseMap { - for q, result := range quoteMap { - cp := currency.NewPair(b.Currency(), q.Currency()) - if item.IsFutures() { - p := result.UnderlyingPair.Format(currency.EMPTYFORMAT) - diff, ok := currs[p] - if !ok { - diff = linkCurrencyDiff{} - } - diff.FuturesPair = cp - diff.SpotPair = p - diff.FuturesEvents = result.Events - currs[p] = diff - } else { - p := cp.Format(currency.EMPTYFORMAT) - diff, ok := currs[p] - if !ok { - diff = linkCurrencyDiff{} - } - diff.SpotEvents = result.Events - currs[p] = diff - } - } + for mapKey, result := range items { + cp := currency.NewPair(mapKey.Base.Currency(), mapKey.Quote.Currency()) + if mapKey.Asset.IsFutures() { + p := result.UnderlyingPair.Format(currency.EMPTYFORMAT) + diff, ok := currs[p] + if !ok { + diff = linkCurrencyDiff{} } + diff.FuturesPair = cp + diff.SpotPair = p + diff.FuturesEvents = result.Events + currs[p] = diff + } else { + p := cp.Format(currency.EMPTYFORMAT) + diff, ok := currs[p] + if !ok { + diff = linkCurrencyDiff{} + } + diff.SpotEvents = result.Events + currs[p] = diff } } diff --git a/backtester/report/chart_test.go b/backtester/report/chart_test.go index 33c37a26..f2491b4a 100644 --- a/backtester/report/chart_test.go +++ b/backtester/report/chart_test.go @@ -11,6 +11,7 @@ import ( evkline "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline" "github.com/thrasher-corp/gocryptotrader/backtester/funding" gctcommon "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" @@ -108,11 +109,13 @@ func TestCreatePNLCharts(t *testing.T) { tt := time.Now() var d Data d.Statistics = &statistics.Statistic{} - d.Statistics.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item][currency.USDT.Item] = &statistics.CurrencyPairStatistic{ + d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) + d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: testExchange, + Base: currency.BTC.Item, + Quote: currency.USDT.Item, + Asset: asset.Spot, + }] = &statistics.CurrencyPairStatistic{ Events: []statistics.DataAtOffset{ { PNL: &portfolio.PNLSummary{ @@ -172,11 +175,13 @@ func TestCreateFuturesSpotDiffChart(t *testing.T) { 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.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item][currency.USD.Item] = &statistics.CurrencyPairStatistic{ + d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) + d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: testExchange, + Base: currency.BTC.Item, + Quote: currency.USD.Item, + Asset: asset.Spot, + }] = &statistics.CurrencyPairStatistic{ Currency: cp, Events: []statistics.DataAtOffset{ { @@ -196,9 +201,12 @@ func TestCreateFuturesSpotDiffChart(t *testing.T) { }, }, } - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Futures] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Futures][currency.BTC.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Futures][currency.BTC.Item][currency.DOGE.Item] = &statistics.CurrencyPairStatistic{ + d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: testExchange, + Base: currency.BTC.Item, + Quote: currency.DOGE.Item, + Asset: asset.Futures, + }] = &statistics.CurrencyPairStatistic{ UnderlyingPair: cp, Currency: cp2, Events: []statistics.DataAtOffset{ diff --git a/backtester/report/report.go b/backtester/report/report.go index 927f57f8..2fe2fdc7 100644 --- a/backtester/report/report.go +++ b/backtester/report/report.go @@ -9,6 +9,7 @@ import ( "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/backtester/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/log" @@ -148,8 +149,12 @@ func (d *Data) enhanceCandles() error { Watermark: fmt.Sprintf("%s - %s - %s", cases.Title(language.English).String(lookup.Exchange), lookup.Asset.String(), lookup.Pair.Upper()), } - statsForCandles := - d.Statistics.ExchangeAssetPairStatistics[lookup.Exchange][lookup.Asset][lookup.Pair.Base.Item][lookup.Pair.Quote.Item] + statsForCandles := d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: lookup.Exchange, + Base: lookup.Pair.Base.Item, + Quote: lookup.Pair.Quote.Item, + Asset: lookup.Asset, + }] if statsForCandles == nil { continue } diff --git a/backtester/report/report_test.go b/backtester/report/report_test.go index 4e807a46..68da9d40 100644 --- a/backtester/report/report_test.go +++ b/backtester/report/report_test.go @@ -10,6 +10,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics" "github.com/thrasher-corp/gocryptotrader/backtester/funding" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -234,23 +235,22 @@ func TestGenerateReport(t *testing.T) { }, StrategyName: "testStrat", RiskFreeRate: decimal.NewFromFloat(0.03), - ExchangeAssetPairStatistics: map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic{ - e: { - a: { - p.Base.Item: { - p.Quote.Item: &statistics.CurrencyPairStatistic{ - 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, - ArithmeticRatios: &statistics.Ratios{}, - GeometricRatios: &statistics.Ratios{}, - }, - }, - }, + ExchangeAssetPairStatistics: map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic{ + { + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + Exchange: e, + }: { + 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, + ArithmeticRatios: &statistics.Ratios{}, + GeometricRatios: &statistics.Ratios{}, }, }, TotalBuyOrders: 1337, @@ -339,11 +339,13 @@ func TestEnhanceCandles(t *testing.T) { t.Errorf("received: %v, expected: %v", err, nil) } - d.Statistics.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot] = make(map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item] = make(map[*currency.Item]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item][currency.USDT.Item] = &statistics.CurrencyPairStatistic{} + d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) + d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: testExchange, + Base: currency.BTC.Item, + Quote: currency.USDT.Item, + Asset: asset.Spot, + }] = &statistics.CurrencyPairStatistic{} err = d.SetKlineData(&gctkline.Item{ Exchange: testExchange, @@ -402,7 +404,12 @@ func TestEnhanceCandles(t *testing.T) { t.Errorf("received: %v, expected: %v", err, nil) } - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item][currency.USDT.Item].FinalOrders = compliance.Snapshot{ + d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: testExchange, + Base: currency.BTC.Item, + Quote: currency.USDT.Item, + Asset: asset.Spot, + }].FinalOrders = compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ { ClosePrice: decimal.NewFromInt(1335), @@ -419,7 +426,12 @@ func TestEnhanceCandles(t *testing.T) { t.Errorf("received: %v, expected: %v", err, nil) } - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item][currency.USDT.Item].FinalOrders = compliance.Snapshot{ + d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: testExchange, + Base: currency.BTC.Item, + Quote: currency.USDT.Item, + Asset: asset.Spot, + }].FinalOrders = compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ { ClosePrice: decimal.NewFromInt(1335), @@ -439,7 +451,12 @@ func TestEnhanceCandles(t *testing.T) { t.Errorf("received: %v, expected: %v", err, nil) } - d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.BTC.Item][currency.USDT.Item].FinalOrders = compliance.Snapshot{ + d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ + Exchange: testExchange, + Base: currency.BTC.Item, + Quote: currency.USDT.Item, + Asset: asset.Spot, + }].FinalOrders = compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ { ClosePrice: decimal.NewFromInt(1335), diff --git a/backtester/report/tpl.gohtml b/backtester/report/tpl.gohtml index e0e5398a..e96b482a 100644 --- a/backtester/report/tpl.gohtml +++ b/backtester/report/tpl.gohtml @@ -36,7 +36,7 @@ - + @@ -329,28 +329,22 @@ - {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}} - {{ range $asset, $unused := .}} - {{ range $base, $unused := .}} - {{ range $quote, $unused := .}} - - {{ $exchange}} - {{ $asset}} - {{ $base}}-{{$quote}} - {{ $.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}} + {{ range $mapKey, $stats := .Statistics.ExchangeAssetPairStatistics}} + + {{ $mapKey.Exchange}} + {{ $mapKey.Asset}} + {{ $mapKey.Base.Currency}}-{{$mapKey.Quote.Currency}} + {{ $.Prettify.Decimal8 $stats.InitialHoldings.BaseInitialFunds }} {{$stats.FinalHoldings.Pair.Base}} + {{ $.Prettify.Decimal8 $stats.InitialHoldings.QuoteInitialFunds }} {{$stats.FinalHoldings.Pair.Quote}} + {{ $.Prettify.Decimal8 $stats.InitialHoldings.TotalInitialValue }} {{$stats.FinalHoldings.Pair.Quote}} + {{ $.Prettify.Decimal8 $stats.FinalHoldings.BaseSize }} {{ $stats.FinalHoldings.Pair.Base}} + {{ $.Prettify.Decimal8 $stats.FinalHoldings.QuoteSize }} {{ $stats.FinalHoldings.Pair.Quote}} + {{ $.Prettify.Decimal8 $stats.FinalHoldings.TotalValue }} {{ $stats.FinalHoldings.Pair.Quote}} + {{ $stats.IsStrategyProfitable }} + {{ $stats.DoesPerformanceBeatTheMarket }} + {{ $.Prettify.Decimal8 $stats.StrategyMovement }}% + {{ $.Prettify.Decimal8 $stats.MarketMovement}}% + {{end}} @@ -472,18 +466,18 @@ {{ range .Statistics.FundingStatistics.Report.Items}} {{ if .AppendedViaAPI}} - {{else}} - - {{.Exchange}} - {{.Asset}} - {{.Currency}} - {{.PairedWith}} - {{ $.Prettify.Decimal8 .InitialFunds}} {{.Currency}} - {{ $.Prettify.Decimal8 .FinalFunds}} {{.Currency}} - {{ $.Prettify.Decimal64 .TransferFee}} - {{ .IsCollateral }} - - {{end}} + {{else}} + + {{.Exchange}} + {{.Asset}} + {{.Currency}} + {{.PairedWith}} + {{ $.Prettify.Decimal8 .InitialFunds}} {{.Currency}} + {{ $.Prettify.Decimal8 .FinalFunds}} {{.Currency}} + {{ $.Prettify.Decimal64 .TransferFee}} + {{ .IsCollateral }} + + {{end}} {{end}} @@ -1229,240 +1223,235 @@ - {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}} - {{ range $asset, $unused := .}} - {{ range $base, $val := .}} - {{ range $quote, $val := .}} -
-
-

Pair Statistics for {{$exchange}} {{ $asset}} {{ $base}}-{{$quote}}

-
-
+ {{ range $mapKey, $stats := .Statistics.ExchangeAssetPairStatistics}} + +
+
+

Pair Statistics for {{$mapKey.Exchange}} {{ $mapKey.Asset}} {{ $mapKey.Base.Currency}}-{{$mapKey.Quote.Currency}}

+
+
+ + + {{ if $stats.Asset.IsFutures }} + + + + + + + + + + + + + + + + + + + + + + + + + {{ else }} + + + + + + + + + + + + + + + + + + + + + + + + + {{end}} + + + + + {{ if $stats.MaxDrawdown.Highest.Value.IsZero }} + {{else}} + + + + + {{ end }} + + + + + + + + + + + + + + + + + {{ if $stats.Asset.IsFutures }} + {{else}} + + + + + {{end}} + + + + + + {{ if $stats.Asset.IsFutures }} + {{else}} + {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} + + + + + + + + + {{ end }} + {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} + + + + + + + + + {{else}} + + + + + + + + + + + + + + + + + + + + + + + + + {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} + + + + + + + + + {{end }} + {{end}} + {{end}} + +
Long Orders{{ $.Prettify.Int $stats.BuyOrders}}
Short Orders{{ $.Prettify.Int $stats.SellOrders}}
Lowest Unrealised PNL{{ $.Prettify.Decimal8 $stats.LowestUnrealisedPNL.Value}} at {{ $stats.LowestUnrealisedPNL.Time}}
Highest Unrealised PNL{{ $.Prettify.Decimal8 $stats.HighestUnrealisedPNL.Value}} at {{ $stats.HighestUnrealisedPNL.Time}}
Lowest Realised PNL{{ $.Prettify.Decimal8 $stats.LowestRealisedPNL.Value}} at {{ $stats.LowestRealisedPNL.Time}}
Highest Realised PNL{{ $.Prettify.Decimal8 $stats.HighestRealisedPNL.Value}} at {{ $stats.HighestRealisedPNL.Time}}
Base Initial Funds{{ $.Prettify.Decimal8 $stats.FinalHoldings.BaseInitialFunds}} {{$stats.FinalHoldings.Pair.Base}}
Quote Initial Funds{{ $.Prettify.Decimal8 $stats.FinalHoldings.QuoteInitialFunds}} {{$stats.FinalHoldings.Pair.Quote}}
Buy Orders{{ $.Prettify.Int $stats.BuyOrders}}
Buy Amount{{ $.Prettify.Decimal8 $stats.FinalHoldings.BoughtAmount}} {{$stats.FinalHoldings.Pair.Base}}
Sell Orders{{ $.Prettify.Int $stats.SellOrders}}
Sell Amount{{ $.Prettify.Decimal8 $stats.FinalHoldings.SoldAmount}} {{$stats.FinalHoldings.Pair.Base}}
Total Orders{{ $.Prettify.Int $stats.TotalOrders}}
Biggest DrawdownStart: {{ $stats.MaxDrawdown.Highest.Time }} End: {{ $stats.MaxDrawdown.Lowest.Time }} Drop: {{ $.Prettify.Decimal8 $stats.MaxDrawdown.DrawdownPercent}}%
Starting Close Price{{ $.Prettify.Decimal8 $stats.StartingClosePrice.Value}} {{$stats.FinalHoldings.Pair.Quote}}
Ending Close Price{{ $.Prettify.Decimal8 $stats.EndingClosePrice.Value}} {{ $stats.FinalHoldings.Pair.Quote }}
Lowest Close Price{{ $.Prettify.Decimal8 $stats.LowestClosePrice.Value}} {{$stats.FinalHoldings.Pair.Quote}}
Highest Close Price{{ $.Prettify.Decimal8 $stats.HighestClosePrice.Value}} {{ $stats.FinalHoldings.Pair.Quote}}
Highest Committed Funds{{ $.Prettify.Decimal8 $stats.HighestCommittedFunds.Value}} at {{ $stats.HighestCommittedFunds.Time}}
Market Movement{{ $.Prettify.Decimal8 $stats.MarketMovement}}%
Strategy Movement{{ $.Prettify.Decimal8 $stats.StrategyMovement}}%
Did it beat the market?{{ .DoesPerformanceBeatTheMarket }}
Final Holdings Value{{ $.Prettify.Decimal8 $stats.FinalHoldings.BaseValue}} {{ $stats.FinalHoldings.Pair.Quote }}
Total Value{{ $.Prettify.Decimal8 $stats.FinalHoldings.TotalValue}} {{ $stats.FinalHoldings.Pair.Quote}}
Total Value Lost to Volume Sizing{{ $.Prettify.Decimal8 $stats.FinalHoldings.TotalValueLostToVolumeSizing}} {{$stats.FinalHoldings.Pair.Quote}}
Total Value Lost to Slippage{{ $.Prettify.Decimal8 $stats.FinalHoldings.TotalValueLostToSlippage}} {{ $stats.FinalHoldings.Pair.Quote }}
Total Value Lost{{ $.Prettify.Decimal8 $stats.FinalHoldings.TotalValueLost}} {{$stats.FinalHoldings.Pair.Quote}}
Total Fees{{ $.Prettify.Decimal8 $stats.FinalHoldings.TotalFees}} {{ $stats.FinalHoldings.Pair.Quote }}
Final Funds{{ $.Prettify.Decimal8 $stats.FinalHoldings.QuoteSize}} {{ $stats.FinalHoldings.Pair.Quote}}
Final Holdings{{ $.Prettify.Decimal8 $stats.FinalHoldings.BaseSize}} {{$stats.FinalHoldings.Pair.Base}}
Final Holdings Value{{ $.Prettify.Decimal8 $stats.FinalHoldings.BaseValue}} {{ $stats.FinalHoldings.Pair.Quote }}
Total Value{{ $.Prettify.Decimal8 $stats.FinalHoldings.TotalValue}} {{ $stats.FinalHoldings.Pair.Quote}}
+ {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} + Rates + + + + + + + + + {{ if $stats.CompoundAnnualGrowthRate.IsZero}} + + {{else}} + + {{end}} + + +
Risk Free Rate{{$.Statistics.RiskFreeRate}}%
Compound Annual Growth RateN/A{{ $.Prettify.Decimal8 $stats.CompoundAnnualGrowthRate}}%
+ {{ if gt $stats.TotalOrders 1}} + {{if $stats.ShowMissingDataWarning}} +

Missing data was detected during this backtesting run
+ Ratio calculations will be skewed

+ {{end}} + Arithmetic Ratios - {{ if $val.Asset.IsFutures }} - - - - - - - - - - - - - - - - - - - - - - - - - {{ else }} - - - - - - - - - - - - - - - - - - - - - - - - - {{end}} - - - - {{ if $val.MaxDrawdown.Highest.Value.IsZero }} - {{else}} - - - - - {{ end }} - - - + + - - + + - - + + - - + + - {{ if $val.Asset.IsFutures }} - {{else}} - - - - - {{end}} - - - - - - {{ if $val.Asset.IsFutures }} - {{else}} - {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} - - - - - - - - - {{ end }} - {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} - - - - - - - - - {{else}} - - - - - - - - - - - - - - - - - - - - - - - - - {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} - - - - - - - - - {{end }} - {{end}} - {{end}}
Long Orders{{ $.Prettify.Int $val.BuyOrders}}
Short Orders{{ $.Prettify.Int $val.SellOrders}}
Lowest Unrealised PNL{{ $.Prettify.Decimal8 $val.LowestUnrealisedPNL.Value}} at {{ $val.LowestUnrealisedPNL.Time}}
Highest Unrealised PNL{{ $.Prettify.Decimal8 $val.HighestUnrealisedPNL.Value}} at {{ $val.HighestUnrealisedPNL.Time}}
Lowest Realised PNL{{ $.Prettify.Decimal8 $val.LowestRealisedPNL.Value}} at {{ $val.LowestRealisedPNL.Time}}
Highest Realised PNL{{ $.Prettify.Decimal8 $val.HighestRealisedPNL.Value}} at {{ $val.HighestRealisedPNL.Time}}
Base Initial Funds{{ $.Prettify.Decimal8 $val.FinalHoldings.BaseInitialFunds}} {{$val.FinalHoldings.Pair.Base}}
Quote Initial Funds{{ $.Prettify.Decimal8 $val.FinalHoldings.QuoteInitialFunds}} {{$val.FinalHoldings.Pair.Quote}}
Buy Orders{{ $.Prettify.Int $val.BuyOrders}}
Buy Amount{{ $.Prettify.Decimal8 $val.FinalHoldings.BoughtAmount}} {{$val.FinalHoldings.Pair.Base}}
Sell Orders{{ $.Prettify.Int $val.SellOrders}}
Sell Amount{{ $.Prettify.Decimal8 $val.FinalHoldings.SoldAmount}} {{$val.FinalHoldings.Pair.Base}}
Total Orders{{ $.Prettify.Int $val.TotalOrders}}
Biggest DrawdownStart: {{ $val.MaxDrawdown.Highest.Time }} End: {{ $val.MaxDrawdown.Lowest.Time }} Drop: {{ $.Prettify.Decimal8 $val.MaxDrawdown.DrawdownPercent}}%
Starting Close Price{{ $.Prettify.Decimal8 $val.StartingClosePrice.Value}} {{$val.FinalHoldings.Pair.Quote}}Sharpe Ratio{{$stats.ArithmeticRatios.SharpeRatio}}
Ending Close Price{{ $.Prettify.Decimal8 $val.EndingClosePrice.Value}} {{ $val.FinalHoldings.Pair.Quote }}Sortino Ratio{{$stats.ArithmeticRatios.SortinoRatio}}
Lowest Close Price{{ $.Prettify.Decimal8 $val.LowestClosePrice.Value}} {{$val.FinalHoldings.Pair.Quote}}Information Ratio{{$stats.ArithmeticRatios.InformationRatio}}
Highest Close Price{{ $.Prettify.Decimal8 $val.HighestClosePrice.Value}} {{ $val.FinalHoldings.Pair.Quote}}Calmar Ratio{{$stats.ArithmeticRatios.CalmarRatio}}
Highest Committed Funds{{ $.Prettify.Decimal8 $val.HighestCommittedFunds.Value}} at {{ $val.HighestCommittedFunds.Time}}
Market Movement{{ $.Prettify.Decimal8 $val.MarketMovement}}%
Strategy Movement{{ $.Prettify.Decimal8 $val.StrategyMovement}}%
Did it beat the market?{{ .DoesPerformanceBeatTheMarket }}
Final Holdings Value{{ $.Prettify.Decimal8 $val.FinalHoldings.BaseValue}} {{ $val.FinalHoldings.Pair.Quote }}
Total Value{{ $.Prettify.Decimal8 $val.FinalHoldings.TotalValue}} {{ $val.FinalHoldings.Pair.Quote}}
Total Value Lost to Volume Sizing{{ $.Prettify.Decimal8 $val.FinalHoldings.TotalValueLostToVolumeSizing}} {{$val.FinalHoldings.Pair.Quote}}
Total Value Lost to Slippage{{ $.Prettify.Decimal8 $val.FinalHoldings.TotalValueLostToSlippage}} {{ $val.FinalHoldings.Pair.Quote }}
Total Value Lost{{ $.Prettify.Decimal8 $val.FinalHoldings.TotalValueLost}} {{$val.FinalHoldings.Pair.Quote}}
Total Fees{{ $.Prettify.Decimal8 $val.FinalHoldings.TotalFees}} {{ $val.FinalHoldings.Pair.Quote }}
Final Funds{{ $.Prettify.Decimal8 $val.FinalHoldings.QuoteSize}} {{ $val.FinalHoldings.Pair.Quote}}
Final Holdings{{ $.Prettify.Decimal8 $val.FinalHoldings.BaseSize}} {{$val.FinalHoldings.Pair.Base}}
Final Holdings Value{{ $.Prettify.Decimal8 $val.FinalHoldings.BaseValue}} {{ $val.FinalHoldings.Pair.Quote }}
Total Value{{ $.Prettify.Decimal8 $val.FinalHoldings.TotalValue}} {{ $val.FinalHoldings.Pair.Quote}}
- {{ if eq $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding false }} - Rates - - - - - - - - - {{ if $val.CompoundAnnualGrowthRate.IsZero}} - - {{else}} - - {{end}} - - -
Risk Free Rate{{$.Statistics.RiskFreeRate}}%
Compound Annual Growth RateN/A{{ $.Prettify.Decimal8 $val.CompoundAnnualGrowthRate}}%
- {{ if gt $val.TotalOrders 1}} - {{if $val.ShowMissingDataWarning}} -

Missing data was detected during this backtesting run
- Ratio calculations will be skewed

- {{end}} - Arithmetic Ratios - - - - - - - - - - - - - - - - - - - -
Sharpe Ratio{{$val.ArithmeticRatios.SharpeRatio}}
Sortino Ratio{{$val.ArithmeticRatios.SortinoRatio}}
Information Ratio{{$val.ArithmeticRatios.InformationRatio}}
Calmar Ratio{{$val.ArithmeticRatios.CalmarRatio}}
- Geometric Ratios - - - - - - - - - - - - - - - - - - - -
Sharpe Ratio{{$val.GeometricRatios.SharpeRatio}}
Sortino Ratio{{$val.GeometricRatios.SortinoRatio}}
Information Ratio{{$val.GeometricRatios.InformationRatio}}
Calmar Ratio{{$val.GeometricRatios.CalmarRatio}}
- {{end}} - {{end }} - {{end }} -
-
- {{end}} - {{end}} - {{end}} + Geometric Ratios + + + + + + + + + + + + + + + + + + + +
Sharpe Ratio{{$stats.GeometricRatios.SharpeRatio}}
Sortino Ratio{{$stats.GeometricRatios.SortinoRatio}}
Information Ratio{{$stats.GeometricRatios.InformationRatio}}
Calmar Ratio{{$stats.GeometricRatios.CalmarRatio}}
+ {{end}} + {{end }} + {{end }} +
+
{{ if $.Config.StrategySettings.DisableUSDTracking }} {{ if $.Statistics.FundingStatistics.Report.UsingExchangeLevelFunding }}
@@ -1474,86 +1463,86 @@ {{end}} {{ range .Statistics.FundingStatistics.Items }} {{ if .ReportItem.AppendedViaAPI}} - {{else}} -
-
-

Funding Statistics for {{.ReportItem.Exchange}} {{.ReportItem.Asset}} {{.ReportItem.Currency}}

-
-
- - - {{ if .ReportItem.IsCollateral}} - - - - - - - - - {{ else }} - - - - - - - - - {{end }} - - - {{ if .ReportItem.ShowInfinite}} - - {{else}} - - {{end}} - - {{ if eq $.Config.StrategySettings.DisableUSDTracking false }} + {{else}} +
+
+

Funding Statistics for {{.ReportItem.Exchange}} {{.ReportItem.Asset}} {{.ReportItem.Currency}}

+
+
+
Initial Collateral{{ $.Prettify.Decimal8 .ReportItem.InitialFunds}}
Final Collateral{{ $.Prettify.Decimal8 .ReportItem.FinalFunds}}
Initial Funds{{ $.Prettify.Decimal8 .ReportItem.InitialFunds}}
Final Funds{{ $.Prettify.Decimal8 .ReportItem.FinalFunds}}
DifferenceInfinity%{{ $.Prettify.Decimal8 .ReportItem.Difference}}%
+ {{ if .ReportItem.IsCollateral}} - {{ else }} - {{ if .ReportItem.Currency.IsFiatCurrency}} - {{else}} - - - - - - - - - - - - - - - - - - - - - {{end }} - - + + - - {{ if .CompoundAnnualGrowthRate.IsZero}} - - {{else}} - - {{end}} + + + + {{ else }} + + + + + + + {{end }} - {{end}} - -
Starting Close Price{{$.Prettify.Decimal8 .StartingClosePrice.Value}} at {{.StartingClosePrice.Time}}
Ending Close Price{{$.Prettify.Decimal8 .EndingClosePrice.Value}} at {{.EndingClosePrice.Time}}
Highest Close Price{{$.Prettify.Decimal8 .HighestClosePrice.Value}} at {{.HighestClosePrice.Time}}
Lowest Close Price{{$.Prettify.Decimal8 .LowestClosePrice.Value}} at {{.LowestClosePrice.Time}}
Market Movement{{$.Prettify.Decimal8 .MarketMovement}}%
Did Strategy Beat The Market?{{.DidStrategyBeatTheMarket}}Initial Collateral{{ $.Prettify.Decimal8 .ReportItem.InitialFunds}}
Compound Annual Growth RateN/A{{$.Prettify.Decimal8 .CompoundAnnualGrowthRate}}%Final Collateral{{ $.Prettify.Decimal8 .ReportItem.FinalFunds}}
Initial Funds{{ $.Prettify.Decimal8 .ReportItem.InitialFunds}}
Final Funds{{ $.Prettify.Decimal8 .ReportItem.FinalFunds}}
+ + Difference + {{ if .ReportItem.ShowInfinite}} + Infinity% + {{else}} + {{ $.Prettify.Decimal8 .ReportItem.Difference}}% + {{end}} + + {{ if eq $.Config.StrategySettings.DisableUSDTracking false }} + {{ if .ReportItem.IsCollateral}} + {{ else }} + {{ if .ReportItem.Currency.IsFiatCurrency}} + {{else}} + + Starting Close Price + {{$.Prettify.Decimal8 .StartingClosePrice.Value}} at {{.StartingClosePrice.Time}} + + + Ending Close Price + {{$.Prettify.Decimal8 .EndingClosePrice.Value}} at {{.EndingClosePrice.Time}} + + + Highest Close Price + {{$.Prettify.Decimal8 .HighestClosePrice.Value}} at {{.HighestClosePrice.Time}} + + + Lowest Close Price + {{$.Prettify.Decimal8 .LowestClosePrice.Value}} at {{.LowestClosePrice.Time}} + + + Market Movement + {{$.Prettify.Decimal8 .MarketMovement}}% + + {{end }} + + Did Strategy Beat The Market? + {{.DidStrategyBeatTheMarket}} + + + Compound Annual Growth Rate + {{ if .CompoundAnnualGrowthRate.IsZero}} + N/A + {{else}} + {{$.Prettify.Decimal8 .CompoundAnnualGrowthRate}}% + {{end}} + + {{end }} + {{end}} + + +
-
- {{end}} + {{end}} {{end}} {{ if eq $.Config.StrategySettings.DisableUSDTracking false }}
@@ -1669,44 +1658,38 @@

Orders

- {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}} - {{ range $asset, $unused := .}} - {{ range $base, $unused := .}} - {{ range $quote, $val := .}} -
-

{{$exchange}} {{$asset}} {{ $base }}-{{$quote}}

-
-
- - - - - - - - - - - - - {{range $val.FinalOrders.Orders}} - - - - - - - - - - - {{end}} - -
DateClose PriceSidePriceAmountFeeTotalSlippage Rate
{{ .Order.Date }}{{ $.Prettify.Decimal8 .ClosePrice}} {{$quote}}{{ .Order.Side }}{{$.Prettify.Float8 .Order.Price }} {{$quote}}{{$.Prettify.Float8 .Order.Amount }} {{$base}}{{$.Prettify.Float8 .Order.Fee }} {{$quote}}{{ $.Prettify.Decimal8 .CostBasis }} {{.Order.FeeAsset}}{{ $.Prettify.Decimal8 .SlippageRate }}%
-
+ {{ range $mapKey, $val := .Statistics.ExchangeAssetPairStatistics}} +
+

{{$mapKey.Exchange}} {{$mapKey.Asset}} {{ $mapKey.Base }}-{{$mapKey.Quote}}

+
+
+ + + + + + + + + + + + + {{range $val.FinalOrders.Orders}} + + + + + + + + + + {{end}} - {{end}} - {{end}} + +
DateClose PriceSidePriceAmountFeeTotalSlippage Rate
{{ .Order.Date }}{{ $.Prettify.Decimal8 .ClosePrice}} {{$mapKey.Quote}}{{ .Order.Side }}{{$.Prettify.Float8 .Order.Price }} {{$mapKey.Quote}}{{$.Prettify.Float8 .Order.Amount }} {{$mapKey.Base}}{{$.Prettify.Float8 .Order.Fee }} {{$mapKey.Quote}}{{ $.Prettify.Decimal8 .CostBasis }} {{.Order.FeeAsset}}{{ $.Prettify.Decimal8 .SlippageRate }}%
+
{{end}}
@@ -1716,85 +1699,79 @@

Events

- {{ range $exchange, $unused := .Statistics.ExchangeAssetPairStatistics}} - {{ range $asset, $unused :=.}} - {{ range $base, $unused := .}} - {{ range $quote, $data := .}} -
-

{{$exchange}} {{$asset}} {{ $base }}-{{$quote}}

-
-
- - - - - - - {{if $asset.IsFutures}} - - - - - {{ else }} - - - - - {{ end }} + {{ range $mapKey, $val := .Statistics.ExchangeAssetPairStatistics}} +
+

{{$mapKey.Exchange}} {{$mapKey.Asset}} {{ $mapKey.Base }}-{{$mapKey.Quote}}

+
+
+
DatePriceActionEvent DetailsHoldingsPosition DirectionUnrealised PNLRealised PNL{{$base}} Funds{{$quote}} FundsTotal value in {{$quote}}Committed funds in {{$quote}}
+ + + + + + {{if $mapKey.Asset.IsFutures}} + + + + + {{ else }} + + + + + {{ end }} - - - {{range $ev := $data.Events}} - - {{ if ne $ev.FillEvent nil }} - - - - - {{ else if ne $ev.SignalEvent nil}} - - - - - {{ end }} - {{if $asset.IsFutures}} - {{if ne $ev.PNL nil }} - - - - - {{else}} - - - - - {{end}} - {{else }} - - - - + + + {{range $ev := $val.Events}} + + {{ if ne $ev.FillEvent nil }} + + + + + + + {{ else if ne $ev.SignalEvent nil}} + + + + + {{ end }} + {{if $mapKey.Asset.IsFutures}} + {{if ne $ev.PNL nil }} + + + + + {{else}} + + + + {{end}} - -
DatePriceActionEvent DetailsHoldingsPosition DirectionUnrealised PNLRealised PNL{{$mapKey.Base}} Funds{{$mapKey.Quote}} FundsTotal value in {{$mapKey.Quote}}Committed funds in {{$mapKey.Quote}}
{{$ev.FillEvent.GetTime}}{{ $.Prettify.Decimal8 $ev.FillEvent.GetClosePrice}} {{if $asset.IsFutures}}{{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}{{else}}{{$quote}}{{end}}{{$ev.FillEvent.GetDirection}} -
    - {{ range $ev.FillEvent.GetReasons }} -
  • {{.}}
  • - {{end}} -
-
{{$ev.SignalEvent.GetTime}}{{ $.Prettify.Decimal8 $ev.SignalEvent.GetClosePrice}} {{if $asset.IsFutures}}{{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}{{else}}{{$quote}}{{end}}{{$ev.SignalEvent.GetDirection}} -
    - {{ range $ev.SignalEvent.GetReasons }} -
  • {{.}}
  • - {{end}} -
-
{{ $.Prettify.Decimal8 $ev.PNL.GetExposure}} {{$base}}-{{$quote}}{{$ev.PNL.GetDirection}}{{$.Prettify.Decimal8 $ev.PNL.GetUnrealisedPNL.PNL}} {{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}{{$.Prettify.Decimal8 $ev.PNL.GetRealisedPNL.PNL}} {{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}0 {{$base}}-{{$quote}}N/A00{{ $.Prettify.Decimal8 $ev.Holdings.BaseSize}} {{$base}}{{ $.Prettify.Decimal8 $ev.Holdings.QuoteSize}} {{$quote}}{{ $.Prettify.Decimal8 $ev.Holdings.TotalValue}} {{$quote}}{{ $.Prettify.Decimal8 $ev.Holdings.CommittedFunds}} {{$quote}}
{{$ev.FillEvent.GetTime}}{{ $.Prettify.Decimal8 $ev.FillEvent.GetClosePrice}} {{if $mapKey.Asset.IsFutures}}{{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}{{else}}{{$mapKey.Quote}}{{end}}{{$ev.FillEvent.GetDirection}} +
    + {{ range $ev.FillEvent.GetReasons }} +
  • {{.}}
  • {{end}} -
{{$ev.SignalEvent.GetTime}}{{ $.Prettify.Decimal8 $ev.SignalEvent.GetClosePrice}} {{if $mapKey.Asset.IsFutures}}{{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}{{else}}{{$mapKey.Quote}}{{end}}{{$ev.SignalEvent.GetDirection}} +
    + {{ range $ev.SignalEvent.GetReasons }} +
  • {{.}}
  • + {{end}} +
+
{{ $.Prettify.Decimal8 $ev.PNL.GetExposure}} {{$mapKey.Base}}-{{$mapKey.Quote}}{{$ev.PNL.GetDirection}}{{$.Prettify.Decimal8 $ev.PNL.GetUnrealisedPNL.PNL}} {{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}{{$.Prettify.Decimal8 $ev.PNL.GetRealisedPNL.PNL}} {{if ne $ev.PNL nil }}{{$ev.PNL.GetCollateralCurrency}}{{end}}0 {{$mapKey.Base}}-{{$mapKey.Quote}}N/A00
-
+ {{else }} + {{ $.Prettify.Decimal8 $ev.Holdings.BaseSize}} {{$mapKey.Base}} + {{ $.Prettify.Decimal8 $ev.Holdings.QuoteSize}} {{$mapKey.Quote}} + {{ $.Prettify.Decimal8 $ev.Holdings.TotalValue}} {{$mapKey.Quote}} + {{ $.Prettify.Decimal8 $ev.Holdings.CommittedFunds}} {{$mapKey.Quote}} + {{end}} + {{end}} - {{end}} - {{end}} + + +
{{end}} diff --git a/common/key/key.go b/common/key/key.go new file mode 100644 index 00000000..96d2210b --- /dev/null +++ b/common/key/key.go @@ -0,0 +1,47 @@ +package key + +import ( + "strings" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +// ExchangePairAsset is a unique map key signature for exchange, currency pair and asset +type ExchangePairAsset struct { + Exchange string + Base *currency.Item + Quote *currency.Item + Asset asset.Item +} + +// PairAsset is a unique map key signature for currency pair and asset +type PairAsset struct { + Base *currency.Item + Quote *currency.Item + Asset asset.Item +} + +// SubAccountCurrencyAsset is a unique map key signature for subaccount, currency code and asset +type SubAccountCurrencyAsset struct { + SubAccount string + Currency *currency.Item + Asset asset.Item +} + +// MatchesExchangeAsset checks if the key matches the exchange and asset +func (k *ExchangePairAsset) MatchesExchangeAsset(exch string, item asset.Item) bool { + return strings.EqualFold(k.Exchange, exch) && k.Asset == item +} + +// MatchesPairAsset checks if the key matches the pair and asset +func (k *ExchangePairAsset) MatchesPairAsset(pair currency.Pair, item asset.Item) bool { + return k.Base == pair.Base.Item && + k.Quote == pair.Quote.Item && + k.Asset == item +} + +// MatchesExchange checks if the exchange matches +func (k *ExchangePairAsset) MatchesExchange(exch string) bool { + return strings.EqualFold(k.Exchange, exch) +} diff --git a/common/key/key_test.go b/common/key/key_test.go new file mode 100644 index 00000000..6ab6a650 --- /dev/null +++ b/common/key/key_test.go @@ -0,0 +1,72 @@ +package key + +import ( + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +func TestMatchesExchangeAsset(t *testing.T) { + t.Parallel() + cp := currency.NewPair(currency.BTC, currency.USD) + k := ExchangePairAsset{ + Exchange: "test", + Base: cp.Base.Item, + Quote: cp.Quote.Item, + Asset: asset.Spot, + } + if !k.MatchesExchangeAsset("test", asset.Spot) { + t.Error("expected true") + } + if k.MatchesExchangeAsset("TEST", asset.Futures) { + t.Error("expected false") + } + if k.MatchesExchangeAsset("test", asset.Futures) { + t.Error("expected false") + } + if !k.MatchesExchangeAsset("TEST", asset.Spot) { + t.Error("expected true") + } +} + +func TestMatchesPairAsset(t *testing.T) { + t.Parallel() + cp := currency.NewPair(currency.BTC, currency.USD) + k := ExchangePairAsset{ + Base: cp.Base.Item, + Quote: cp.Quote.Item, + Asset: asset.Spot, + } + if !k.MatchesPairAsset(cp, asset.Spot) { + t.Error("expected true") + } + if k.MatchesPairAsset(cp, asset.Futures) { + t.Error("expected false") + } + if k.MatchesPairAsset(currency.EMPTYPAIR, asset.Futures) { + t.Error("expected false") + } + if k.MatchesPairAsset(currency.NewPair(currency.BTC, currency.USDT), asset.Spot) { + t.Error("expected false") + } +} + +func TestMatchesExchange(t *testing.T) { + t.Parallel() + k := ExchangePairAsset{ + Exchange: "test", + } + if !k.MatchesExchange("test") { + t.Error("expected true") + } + if !k.MatchesExchange("TEST") { + t.Error("expected true") + } + if k.MatchesExchange("tรจst") { + t.Error("expected false") + } + if k.MatchesExchange("") { + t.Error("expected false") + } +} diff --git a/engine/engine_test.go b/engine/engine_test.go index 241e466d..a5030772 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -356,7 +356,7 @@ func TestSettingsPrint(t *testing.T) { var unsupportedDefaultConfigExchanges = []string{ "itbit", // due to unsupported API - "poloniex", // outdated API // TODO remove once updated + "poloniex", // poloniex has dropped support for the API GCT has implemented //TODO: drop this when supported } func TestGetDefaultConfigurations(t *testing.T) { diff --git a/engine/event_manager_test.go b/engine/event_manager_test.go index d7ae6c54..da50dc92 100644 --- a/engine/event_manager_test.go +++ b/engine/event_manager_test.go @@ -299,7 +299,7 @@ func TestCheckEventCondition(t *testing.T) { } m.m.Lock() err = m.checkEventCondition(&m.events[0]) - if err != nil && !strings.Contains(err.Error(), "no tickers for") { + if err != nil && !strings.Contains(err.Error(), "no tickers associated") { t.Error(err) } else if err == nil { t.Error("expected error") diff --git a/exchanges/account/account.go b/exchanges/account/account.go index 5d841b44..043635d9 100644 --- a/exchanges/account/account.go +++ b/exchanges/account/account.go @@ -7,6 +7,7 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -95,7 +96,6 @@ func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holding return Holdings{}, fmt.Errorf("%s %s %w", exch, assetType, errExchangeHoldingsNotFound) } - var accountsHoldings []SubAccount subAccountHoldings, ok := accounts.SubAccounts[*creds] if !ok { return Holdings{}, fmt.Errorf("%s %s %s %w", @@ -105,44 +105,39 @@ func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holding errNoCredentialBalances) } - for subAccount, assetHoldings := range subAccountHoldings { - for ai, currencyHoldings := range assetHoldings { - if ai != assetType { - continue - } - var currencyBalances = make([]Balance, len(currencyHoldings)) - target := 0 - for item, balance := range currencyHoldings { - balance.m.Lock() - currencyBalances[target] = Balance{ - Currency: currency.Code{Item: item, UpperCase: true}, - Total: balance.total, - Hold: balance.hold, - Free: balance.free, - AvailableWithoutBorrow: balance.availableWithoutBorrow, - Borrowed: balance.borrowed, - } - balance.m.Unlock() - target++ - } - - if len(currencyBalances) == 0 { - continue - } - - cpy := *creds - if cpy.SubAccount == "" { - cpy.SubAccount = subAccount - } - - accountsHoldings = append(accountsHoldings, SubAccount{ - Credentials: Protected{creds: cpy}, - ID: subAccount, - AssetType: ai, - Currencies: currencyBalances, - }) - break + var currencyBalances = make([]Balance, 0, len(subAccountHoldings)) + accountsHoldings := make([]SubAccount, 0, len(subAccountHoldings)) + for mapKey, assetHoldings := range subAccountHoldings { + if mapKey.Asset != assetType { + continue } + assetHoldings.m.Lock() + currencyBalances = append(currencyBalances, Balance{ + Currency: currency.Code{Item: mapKey.Currency, UpperCase: true}, + Total: assetHoldings.total, + Hold: assetHoldings.hold, + Free: assetHoldings.free, + AvailableWithoutBorrow: assetHoldings.availableWithoutBorrow, + Borrowed: assetHoldings.borrowed, + }) + assetHoldings.m.Unlock() + + if len(currencyBalances) == 0 { + continue + } + + cpy := *creds + if cpy.SubAccount == "" { + cpy.SubAccount = mapKey.SubAccount + } + + accountsHoldings = append(accountsHoldings, SubAccount{ + Credentials: Protected{creds: cpy}, + ID: mapKey.SubAccount, + AssetType: mapKey.Asset, + Currencies: currencyBalances, + }) + break } if len(accountsHoldings) == 0 { @@ -187,22 +182,14 @@ func GetBalance(exch, subAccount string, creds *Credentials, ai asset.Item, c cu exch, creds, errNoCredentialBalances) } - assetBalances, ok := subAccounts[subAccount] - if !ok { - return nil, fmt.Errorf("%s %s %w", - exch, subAccount, errNoExchangeSubAccountBalances) - } - - currencyBalances, ok := assetBalances[ai] - if !ok { - return nil, fmt.Errorf("%s %s %s %w", - exch, subAccount, ai, errAssetHoldingsNotFound) - } - - bal, ok := currencyBalances[c.Item] + bal, ok := subAccounts[key.SubAccountCurrencyAsset{ + SubAccount: subAccount, + Currency: c.Item, + Asset: ai, + }] if !ok { return nil, fmt.Errorf("%s %s %s %s %w", - exch, subAccount, ai, c, errNoBalanceFound) + exch, subAccount, ai, c, errNoExchangeSubAccountBalances) } return bal, nil } @@ -232,7 +219,7 @@ func (s *Service) Update(incoming *Holdings, creds *Credentials) error { } accounts = &Accounts{ ID: id, - SubAccounts: make(map[Credentials]map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance), + SubAccounts: make(map[Credentials]map[key.SubAccountCurrencyAsset]*ProtectedBalance), } s.exchangeAccounts[exch] = accounts } @@ -257,34 +244,28 @@ func (s *Service) Update(incoming *Holdings, creds *Credentials) error { } incoming.Accounts[x].Credentials.creds = cpy - var subAccounts map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance + var subAccounts map[key.SubAccountCurrencyAsset]*ProtectedBalance subAccounts, ok = accounts.SubAccounts[*creds] if !ok { - subAccounts = make(map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance) + subAccounts = make(map[key.SubAccountCurrencyAsset]*ProtectedBalance) accounts.SubAccounts[*creds] = subAccounts } - var accountAssets map[asset.Item]map[*currency.Item]*ProtectedBalance - accountAssets, ok = subAccounts[incoming.Accounts[x].ID] - if !ok { - accountAssets = make(map[asset.Item]map[*currency.Item]*ProtectedBalance) + for y := range incoming.Accounts[x].Currencies { // Note: Sub accounts are case sensitive and an account "name" is // different to account "naMe". - subAccounts[incoming.Accounts[x].ID] = accountAssets - } - - var currencyBalances map[*currency.Item]*ProtectedBalance - currencyBalances, ok = accountAssets[incoming.Accounts[x].AssetType] - if !ok { - currencyBalances = make(map[*currency.Item]*ProtectedBalance) - accountAssets[incoming.Accounts[x].AssetType] = currencyBalances - } - - for y := range incoming.Accounts[x].Currencies { - bal := currencyBalances[incoming.Accounts[x].Currencies[y].Currency.Item] - if bal == nil { + bal, ok := subAccounts[key.SubAccountCurrencyAsset{ + SubAccount: incoming.Accounts[x].ID, + Currency: incoming.Accounts[x].Currencies[y].Currency.Item, + Asset: incoming.Accounts[x].AssetType, + }] + if !ok || bal == nil { bal = &ProtectedBalance{} - currencyBalances[incoming.Accounts[x].Currencies[y].Currency.Item] = bal + subAccounts[key.SubAccountCurrencyAsset{ + SubAccount: incoming.Accounts[x].ID, + Currency: incoming.Accounts[x].Currencies[y].Currency.Item, + Asset: incoming.Accounts[x].AssetType, + }] = bal } bal.load(incoming.Accounts[x].Currencies[y]) } diff --git a/exchanges/account/account_test.go b/exchanges/account/account_test.go index b82c6146..a3b80f81 100644 --- a/exchanges/account/account_test.go +++ b/exchanges/account/account_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -292,13 +293,8 @@ func TestGetBalance(t *testing.T) { } _, err = GetBalance("bruh", "1337", happyCredentials, asset.Futures, currency.BTC) - if !errors.Is(err, errAssetHoldingsNotFound) { - t.Fatalf("received: '%v' but expected: '%v'", err, errAssetHoldingsNotFound) - } - - _, err = GetBalance("bruh", "1337", happyCredentials, asset.Spot, currency.BTC) - if !errors.Is(err, errNoBalanceFound) { - t.Fatalf("received: '%v' but expected: '%v'", err, errNoBalanceFound) + if !errors.Is(err, errNoExchangeSubAccountBalances) { + t.Fatalf("received: '%v' but expected: '%v'", err, errNoExchangeSubAccountBalances) } err = Process(&Holdings{ @@ -473,7 +469,11 @@ func TestUpdate(t *testing.T) { t.Fatal("account should be loaded") } - b, ok := acc.SubAccounts[Credentials{Key: "AAAAA"}]["1337"][asset.Spot][currency.BTC.Item] + b, ok := acc.SubAccounts[Credentials{Key: "AAAAA"}][key.SubAccountCurrencyAsset{ + SubAccount: "1337", + Currency: currency.BTC.Item, + Asset: asset.Spot, + }] if !ok { t.Fatal("account should be loaded") } diff --git a/exchanges/account/account_types.go b/exchanges/account/account_types.go index 8e8c1280..509e07e2 100644 --- a/exchanges/account/account_types.go +++ b/exchanges/account/account_types.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/alert" @@ -32,7 +33,7 @@ type Accounts struct { // TODO: Credential tracker to match to keys that are managed and return // pointer. // TODO: Have different cred struct for centralized verse DEFI exchanges. - SubAccounts map[Credentials]map[string]map[asset.Item]map[*currency.Item]*ProtectedBalance + SubAccounts map[Credentials]map[key.SubAccountCurrencyAsset]*ProtectedBalance } // Holdings is a generic type to hold each exchange's holdings for all enabled diff --git a/exchanges/asset/asset.go b/exchanges/asset/asset.go index 3256695d..43442f74 100644 --- a/exchanges/asset/asset.go +++ b/exchanges/asset/asset.go @@ -12,6 +12,8 @@ var ( ErrNotSupported = errors.New("unsupported asset type") // ErrNotEnabled is an error for an asset not enabled ErrNotEnabled = errors.New("asset type not enabled") + // ErrInvalidAsset is returned when the assist isn't valid + ErrInvalidAsset = errors.New("asset is invalid") ) // Item stores the asset type diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index b8a3ffbd..eb9082be 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -619,7 +619,7 @@ func TestGetHistoricCandles(t *testing.T) { if err != nil { t.Fatal(err) } - startTime := time.Now().AddDate(0, -2, 0) + startTime := time.Now().AddDate(0, -1, 0) _, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, startTime, time.Now()) if err != nil { t.Fatal(err) diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 38c7de4e..98f7f6ba 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -46,6 +46,9 @@ const ( ) var ( + // ErrExchangeNameIsEmpty is returned when the exchange name is empty + ErrExchangeNameIsEmpty = errors.New("exchange name is empty") + errEndpointStringNotFound = errors.New("endpoint string not found") errConfigPairFormatRequiresDelimiter = errors.New("config pair format requires delimiter") errSymbolCannotBeMatched = errors.New("symbol cannot be matched") diff --git a/exchanges/futures/futures.go b/exchanges/futures/futures.go index f9ebd23c..e8e71eef 100644 --- a/exchanges/futures/futures.go +++ b/exchanges/futures/futures.go @@ -10,6 +10,7 @@ import ( "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -20,7 +21,7 @@ import ( // to track futures orders func SetupPositionController() PositionController { return PositionController{ - multiPositionTrackers: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker), + multiPositionTrackers: make(map[key.ExchangePairAsset]*MultiPositionTracker), } } @@ -41,24 +42,14 @@ func (c *PositionController) TrackNewOrder(d *order.Detail) error { } c.m.Lock() defer c.m.Unlock() - exchMap, ok := c.multiPositionTrackers[d.Exchange] + exchMap, ok := c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: d.Exchange, + Base: d.Pair.Base.Item, + Quote: d.Pair.Quote.Item, + Asset: d.AssetType, + }] if !ok { - exchMap = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[d.Exchange] = exchMap - } - itemMap, ok := exchMap[d.AssetType] - if !ok { - itemMap = make(map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - exchMap[d.AssetType] = itemMap - } - baseMap, ok := itemMap[d.Pair.Base.Item] - if !ok { - baseMap = make(map[*currency.Item]*MultiPositionTracker) - itemMap[d.Pair.Base.Item] = baseMap - } - quoteMap, ok := baseMap[d.Pair.Quote.Item] - if !ok { - quoteMap, err = SetupMultiPositionTracker(&MultiPositionTrackerSetup{ + exchMap, err = SetupMultiPositionTracker(&MultiPositionTrackerSetup{ Exchange: d.Exchange, Asset: d.AssetType, Pair: d.Pair, @@ -67,9 +58,14 @@ func (c *PositionController) TrackNewOrder(d *order.Detail) error { if err != nil { return err } - baseMap[d.Pair.Quote.Item] = quoteMap + c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: d.Exchange, + Base: d.Pair.Base.Item, + Quote: d.Pair.Quote.Item, + Asset: d.AssetType, + }] = exchMap } - err = quoteMap.TrackNewOrder(d) + err = exchMap.TrackNewOrder(d) if err != nil { return err } @@ -91,7 +87,12 @@ func (c *PositionController) SetCollateralCurrency(exch string, item asset.Item, c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item] + tracker := c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: exch, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: item, + }] if tracker == nil { return fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair) } @@ -119,7 +120,12 @@ func (c *PositionController) GetPositionsForExchange(exch string, item asset.Ite } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item] + tracker := c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: exch, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: item, + }] if tracker == nil { return nil, fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair) } @@ -142,7 +148,12 @@ func (c *PositionController) TrackFundingDetails(d *fundingrate.Rates) error { } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[d.Exchange][d.Asset][d.Pair.Base.Item][d.Pair.Quote.Item] + tracker := c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: d.Exchange, + Base: d.Pair.Base.Item, + Quote: d.Pair.Quote.Item, + Asset: d.Asset, + }] if tracker == nil { return fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, d.Exchange, d.Asset, d.Pair) } @@ -177,7 +188,12 @@ func (c *PositionController) GetOpenPosition(exch string, item asset.Item, pair } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item] + tracker := c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: exch, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: item, + }] if tracker == nil { return nil, fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair) } @@ -200,19 +216,13 @@ func (c *PositionController) GetAllOpenPositions() ([]Position, error) { c.m.Lock() defer c.m.Unlock() var openPositions []Position - for _, exchMap := range c.multiPositionTrackers { - for _, itemMap := range exchMap { - for _, baseMap := range itemMap { - for _, multiPositionTracker := range baseMap { - positions := multiPositionTracker.GetPositions() - for i := range positions { - if positions[i].Status.IsInactive() { - continue - } - openPositions = append(openPositions, positions[i]) - } - } + for _, multiPositionTracker := range c.multiPositionTrackers { + positions := multiPositionTracker.GetPositions() + for i := range positions { + if positions[i].Status.IsInactive() { + continue } + openPositions = append(openPositions, positions[i]) } } if len(openPositions) == 0 { @@ -235,7 +245,12 @@ func (c *PositionController) UpdateOpenPositionUnrealisedPNL(exch string, item a } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item] + tracker := c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: exch, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: item, + }] if tracker == nil { return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionNotFound) } @@ -327,7 +342,12 @@ func (c *PositionController) ClearPositionsForExchange(exch string, item asset.I c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item] + tracker := c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: exch, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: item, + }] if tracker == nil { return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionNotFound) } @@ -345,7 +365,12 @@ func (c *PositionController) ClearPositionsForExchange(exch string, item asset.I if err != nil { return err } - c.multiPositionTrackers[exch][item][pair.Base.Item][pair.Quote.Item] = newMPT + c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: exch, + Base: pair.Base.Item, + Quote: pair.Quote.Item, + Asset: item, + }] = newMPT return nil } diff --git a/exchanges/futures/futures_test.go b/exchanges/futures/futures_test.go index 0f68dbc5..4a598c55 100644 --- a/exchanges/futures/futures_test.go +++ b/exchanges/futures/futures_test.go @@ -8,6 +8,7 @@ import ( "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -601,14 +602,23 @@ func TestGetPositionsForExchange(t *testing.T) { if len(pos) != 0 { t.Error("expected zero") } - c.multiPositionTrackers = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange] = nil + c.multiPositionTrackers = make(map[key.ExchangePairAsset]*MultiPositionTracker) + c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: testExchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + }] = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) if !errors.Is(err, ErrPositionNotFound) { t.Errorf("received '%v' expected '%v", err, ErrPositionNotFound) } - c.multiPositionTrackers[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange][asset.Futures] = nil + c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: testExchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + }] = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) if !errors.Is(err, ErrPositionNotFound) { t.Errorf("received '%v' expected '%v", err, ErrPositionNotFound) @@ -618,9 +628,12 @@ func TestGetPositionsForExchange(t *testing.T) { t.Errorf("received '%v' expected '%v", err, ErrNotFuturesAsset) } - c.multiPositionTrackers[testExchange][asset.Futures] = make(map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item] = make(map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item][p.Quote.Item] = &MultiPositionTracker{ + c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: testExchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + }] = &MultiPositionTracker{ exchange: testExchange, } @@ -631,7 +644,12 @@ func TestGetPositionsForExchange(t *testing.T) { if len(pos) != 0 { t.Fatal("expected zero") } - c.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item][p.Quote.Item] = &MultiPositionTracker{ + c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: testExchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + }] = &MultiPositionTracker{ exchange: testExchange, positions: []*PositionTracker{ { @@ -669,29 +687,23 @@ func TestClearPositionsForExchange(t *testing.T) { if !errors.Is(err, ErrPositionNotFound) { t.Errorf("received '%v' expected '%v", err, ErrPositionNotFound) } - c.multiPositionTrackers = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange] = nil - err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) - if !errors.Is(err, ErrPositionNotFound) { - t.Errorf("received '%v' expected '%v", err, ErrPositionNotFound) - } - c.multiPositionTrackers[testExchange] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange][asset.Futures] = nil + c.multiPositionTrackers = make(map[key.ExchangePairAsset]*MultiPositionTracker) err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) if !errors.Is(err, ErrPositionNotFound) { t.Errorf("received '%v' expected '%v", err, ErrPositionNotFound) } + err = c.ClearPositionsForExchange(testExchange, asset.Spot, p) if !errors.Is(err, ErrNotFuturesAsset) { t.Errorf("received '%v' expected '%v", err, ErrNotFuturesAsset) } - c.multiPositionTrackers[testExchange][asset.Futures] = make(map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item] = make(map[*currency.Item]*MultiPositionTracker) - c.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item][p.Quote.Item] = &MultiPositionTracker{ - exchange: testExchange, - } - c.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item][p.Quote.Item] = &MultiPositionTracker{ + c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: testExchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + }] = &MultiPositionTracker{ exchange: testExchange, underlying: currency.DOGE, positions: []*PositionTracker{ @@ -704,7 +716,12 @@ func TestClearPositionsForExchange(t *testing.T) { if !errors.Is(err, nil) { t.Errorf("received '%v' expected '%v", err, nil) } - if len(c.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item][p.Quote.Item].positions) != 0 { + if len(c.multiPositionTrackers[key.ExchangePairAsset{ + Exchange: testExchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + }].positions) != 0 { t.Fatal("expected 0") } c = nil @@ -940,46 +957,36 @@ func TestUpdateOpenPositionUnrealisedPNL(t *testing.T) { func TestSetCollateralCurrency(t *testing.T) { t.Parallel() - var expectedError = errExchangeNameEmpty pc := SetupPositionController() err := pc.SetCollateralCurrency("", asset.Spot, currency.EMPTYPAIR, currency.Code{}) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, errExchangeNameEmpty) { + t.Errorf("received '%v' expected '%v", err, errExchangeNameEmpty) } - expectedError = ErrNotFuturesAsset err = pc.SetCollateralCurrency("hi", asset.Spot, currency.EMPTYPAIR, currency.Code{}) - if !errors.Is(err, expectedError) { - t.Errorf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, ErrNotFuturesAsset) { + t.Errorf("received '%v' expected '%v", err, ErrNotFuturesAsset) } p := currency.NewPair(currency.BTC, currency.USDT) - pc.multiPositionTrackers = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker) + pc.multiPositionTrackers = make(map[key.ExchangePairAsset]*MultiPositionTracker) err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) - expectedError = ErrPositionNotFound - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) - } - pc.multiPositionTrackers["hi"] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker) - err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, ErrPositionNotFound) { + t.Fatalf("received '%v' expected '%v", err, ErrPositionNotFound) } - pc.multiPositionTrackers["hi"][asset.Futures] = make(map[*currency.Item]map[*currency.Item]*MultiPositionTracker) err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, ErrPositionNotFound) { + t.Fatalf("received '%v' expected '%v", err, ErrPositionNotFound) } - expectedError = ErrPositionNotFound - pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item] = make(map[*currency.Item]*MultiPositionTracker) - pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item] = nil - err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + mapKey := key.ExchangePairAsset{ + Exchange: "hi", + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, } - pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item] = &MultiPositionTracker{ + pc.multiPositionTrackers[mapKey] = &MultiPositionTracker{ exchange: "hi", asset: asset.Futures, pair: p, @@ -1000,34 +1007,30 @@ func TestSetCollateralCurrency(t *testing.T) { } err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) - expectedError = nil - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v", err, nil) } - if !pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].collateralCurrency.Equal(currency.DOGE) { - t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].collateralCurrency, currency.DOGE) + if !pc.multiPositionTrackers[mapKey].collateralCurrency.Equal(currency.DOGE) { + t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers[mapKey].collateralCurrency, currency.DOGE) } - if !pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].positions[0].collateralCurrency.Equal(currency.DOGE) { - t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].positions[0].collateralCurrency, currency.DOGE) + if !pc.multiPositionTrackers[mapKey].positions[0].collateralCurrency.Equal(currency.DOGE) { + t.Errorf("received '%v' expected '%v'", pc.multiPositionTrackers[mapKey].positions[0].collateralCurrency, currency.DOGE) } var nilPC *PositionController err = nilPC.SetCollateralCurrency("hi", asset.Spot, currency.EMPTYPAIR, currency.Code{}) - expectedError = common.ErrNilPointer - if !errors.Is(err, expectedError) { - t.Errorf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, common.ErrNilPointer) { + t.Errorf("received '%v' expected '%v", err, common.ErrNilPointer) } } func TestMPTUpdateOpenPositionUnrealisedPNL(t *testing.T) { t.Parallel() - var err, expectedError error - expectedError = nil p := currency.NewPair(currency.BTC, currency.USDT) pc := SetupPositionController() - err = pc.TrackNewOrder(&order.Detail{ + err := pc.TrackNewOrder(&order.Detail{ Date: time.Now(), Exchange: "hi", Pair: p, @@ -1037,30 +1040,35 @@ func TestMPTUpdateOpenPositionUnrealisedPNL(t *testing.T) { Price: 1, Amount: 1, }) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v", err, nil) } - result, err := pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + mapKey := key.ExchangePairAsset{ + Exchange: "hi", + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + } + + result, err := pc.multiPositionTrackers[mapKey].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) + if !errors.Is(err, nil) { + t.Fatalf("received '%v' expected '%v", err, nil) } if result.Equal(decimal.NewFromInt(1337)) { t.Error("") } - expectedError = ErrPositionClosed - pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].positions[0].status = order.Closed - _, err = pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + pc.multiPositionTrackers[mapKey].positions[0].status = order.Closed + _, err = pc.multiPositionTrackers[mapKey].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) + if !errors.Is(err, ErrPositionClosed) { + t.Fatalf("received '%v' expected '%v", err, ErrPositionClosed) } - expectedError = ErrPositionNotFound - pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].positions = nil - _, err = pc.multiPositionTrackers["hi"][asset.Futures][p.Base.Item][p.Quote.Item].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) - if !errors.Is(err, expectedError) { - t.Fatalf("received '%v' expected '%v", err, expectedError) + pc.multiPositionTrackers[mapKey].positions = nil + _, err = pc.multiPositionTrackers[mapKey].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) + if !errors.Is(err, ErrPositionNotFound) { + t.Fatalf("received '%v' expected '%v", err, ErrPositionNotFound) } } @@ -1305,8 +1313,16 @@ func TestPCTrackFundingDetails(t *testing.T) { Payment: decimal.NewFromInt(1337), }, } - pc.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item][p.Quote.Item].orderPositions["lol"].openingDate = tn.Add(-time.Hour) - pc.multiPositionTrackers[testExchange][asset.Futures][p.Base.Item][p.Quote.Item].orderPositions["lol"].lastUpdated = tn + + mapKey := key.ExchangePairAsset{ + Exchange: testExchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: asset.Futures, + } + + pc.multiPositionTrackers[mapKey].orderPositions["lol"].openingDate = tn.Add(-time.Hour) + pc.multiPositionTrackers[mapKey].orderPositions["lol"].lastUpdated = tn err = pc.TrackFundingDetails(rates) if !errors.Is(err, nil) { t.Errorf("received '%v' expected '%v", err, nil) diff --git a/exchanges/futures/futures_types.go b/exchanges/futures/futures_types.go index 31177058..291472d8 100644 --- a/exchanges/futures/futures_types.go +++ b/exchanges/futures/futures_types.go @@ -7,6 +7,7 @@ import ( "time" "github.com/shopspring/decimal" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" @@ -85,7 +86,7 @@ type TotalCollateralResponse struct { // the position controller and its all tracked happily type PositionController struct { m sync.Mutex - multiPositionTrackers map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*MultiPositionTracker + multiPositionTrackers map[key.ExchangePairAsset]*MultiPositionTracker updated time.Time } diff --git a/exchanges/orderbook/linked_list_test.go b/exchanges/orderbook/linked_list_test.go index d5804429..c5d66312 100644 --- a/exchanges/orderbook/linked_list_test.go +++ b/exchanges/orderbook/linked_list_test.go @@ -2009,7 +2009,7 @@ func TestGetQuoteAmountFromNominalSlippage(t *testing.T) { t.Fatalf("%s received: '%v' but expected: '%v'", tt.Name, err, tt.ExpectedError) } if !quote.IsEqual(tt.ExpectedShift) { - t.Fatalf("%s quote received: '%+v' but expected: '%+v'", + t.Fatalf("%s quote received: \n'%+v' \nbut expected: \n'%+v'", tt.Name, quote, tt.ExpectedShift) } }) diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index d4c3c2a3..93a93666 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -44,6 +45,12 @@ func SubscribeToExchangeOrderbooks(exchange string) (dispatch.Pipe, error) { // Update stores orderbook data func (s *Service) Update(b *Base) error { name := strings.ToLower(b.Exchange) + mapKey := key.PairAsset{ + Base: b.Pair.Base.Item, + Quote: b.Pair.Quote.Item, + Asset: b.Asset, + } + s.mu.Lock() m1, ok := s.books[name] if !ok { @@ -53,29 +60,16 @@ func (s *Service) Update(b *Base) error { return err } m1 = Exchange{ - m: make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Depth), + m: make(map[key.PairAsset]*Depth), ID: id, } s.books[name] = m1 } - - m2, ok := m1.m[b.Asset] - if !ok { - m2 = make(map[*currency.Item]map[*currency.Item]*Depth) - m1.m[b.Asset] = m2 - } - - m3, ok := m2[b.Pair.Base.Item] - if !ok { - m3 = make(map[*currency.Item]*Depth) - m2[b.Pair.Base.Item] = m3 - } - - book, ok := m3[b.Pair.Quote.Item] + book, ok := m1.m[mapKey] if !ok { book = NewDepth(m1.ID) book.AssignOptions(b) - m3[b.Pair.Quote.Item] = book + m1.m[mapKey] = book } err := book.LoadSnapshot(b.Bids, b.Asks, b.LastUpdateID, b.LastUpdated, true) s.mu.Unlock() @@ -97,6 +91,12 @@ func (s *Service) DeployDepth(exchange string, p currency.Pair, a asset.Item) (* if !a.IsValid() { return nil, errAssetTypeNotSet } + mapKey := key.PairAsset{ + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + } + s.mu.Lock() defer s.mu.Unlock() m1, ok := s.books[strings.ToLower(exchange)] @@ -106,28 +106,18 @@ func (s *Service) DeployDepth(exchange string, p currency.Pair, a asset.Item) (* return nil, err } m1 = Exchange{ - m: make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Depth), + m: make(map[key.PairAsset]*Depth), ID: id, } s.books[strings.ToLower(exchange)] = m1 } - m2, ok := m1.m[a] - if !ok { - m2 = make(map[*currency.Item]map[*currency.Item]*Depth) - m1.m[a] = m2 - } - m3, ok := m2[p.Base.Item] - if !ok { - m3 = make(map[*currency.Item]*Depth) - m2[p.Base.Item] = m3 - } - book, ok := m3[p.Quote.Item] + book, ok := m1.m[mapKey] if !ok { book = NewDepth(m1.ID) book.exchange = exchange book.pair = p book.asset = a - m3[p.Quote.Item] = book + m1.m[mapKey] = book } return book, nil } @@ -143,21 +133,11 @@ func (s *Service) GetDepth(exchange string, p currency.Pair, a asset.Item) (*Dep errCannotFindOrderbook, exchange) } - m2, ok := m1.m[a] - if !ok { - return nil, fmt.Errorf("%w associated with asset type %s", - errCannotFindOrderbook, - a) - } - - m3, ok := m2[p.Base.Item] - if !ok { - return nil, fmt.Errorf("%w associated with base currency %s", - errCannotFindOrderbook, - p.Base) - } - - book, ok := m3[p.Quote.Item] + book, ok := m1.m[key.PairAsset{ + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] if !ok { return nil, fmt.Errorf("%w associated with base currency %s", errCannotFindOrderbook, @@ -175,6 +155,7 @@ func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Bas if !a.IsValid() { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) } + s.mu.Lock() defer s.mu.Unlock() m1, ok := s.books[strings.ToLower(exchange)] @@ -183,19 +164,11 @@ func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Bas errCannotFindOrderbook, exchange) } - m2, ok := m1.m[a] - if !ok { - return nil, fmt.Errorf("%w associated with asset type %s", - errCannotFindOrderbook, - a) - } - m3, ok := m2[p.Base.Item] - if !ok { - return nil, fmt.Errorf("%w associated with base currency %s", - errCannotFindOrderbook, - p.Base) - } - book, ok := m3[p.Quote.Item] + book, ok := m1.m[key.PairAsset{ + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] if !ok { return nil, fmt.Errorf("%w associated with base currency %s", errCannotFindOrderbook, diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index ceee0ecd..3235f9a1 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -49,7 +50,7 @@ type Service struct { // Exchange defines a holder for the exchange specific depth items with a // specific ID associated with that exchange type Exchange struct { - m map[asset.Item]map[*currency.Item]map[*currency.Item]*Depth + m map[key.PairAsset]*Depth ID uuid.UUID } diff --git a/exchanges/stream/buffer/buffer.go b/exchanges/stream/buffer/buffer.go index f2ef36e9..57c2b5fd 100644 --- a/exchanges/stream/buffer/buffer.go +++ b/exchanges/stream/buffer/buffer.go @@ -6,6 +6,7 @@ import ( "sort" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -60,7 +61,7 @@ func (w *Orderbook) Setup(exchangeConfig *config.Exchange, c *Config, dataHandle w.updateEntriesByID = c.UpdateEntriesByID w.exchangeName = exchangeConfig.Name w.dataHandler = dataHandler - w.ob = make(map[Key]*orderbookHolder) + w.ob = make(map[key.PairAsset]*orderbookHolder) w.verbose = exchangeConfig.Verbose // set default publish period if missing @@ -93,7 +94,7 @@ func (w *Orderbook) Update(u *orderbook.Update) error { } w.mtx.Lock() defer w.mtx.Unlock() - book, ok := w.ob[Key{Base: u.Pair.Base.Item, Quote: u.Pair.Quote.Item, Asset: u.Asset}] + book, ok := w.ob[key.PairAsset{Base: u.Pair.Base.Item, Quote: u.Pair.Quote.Item, Asset: u.Asset}] if !ok { return fmt.Errorf("%w for Exchange %s CurrencyPair: %s AssetType: %s", errDepthNotFound, @@ -311,7 +312,7 @@ func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error { w.mtx.Lock() defer w.mtx.Unlock() - holder, ok := w.ob[Key{Base: book.Pair.Base.Item, Quote: book.Pair.Quote.Item, Asset: book.Asset}] + holder, ok := w.ob[key.PairAsset{Base: book.Pair.Base.Item, Quote: book.Pair.Quote.Item, Asset: book.Asset}] if !ok { // Associate orderbook pointer with local exchange depth map var depth *orderbook.Depth @@ -327,7 +328,7 @@ func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error { ticker = time.NewTicker(w.publishPeriod) } holder = &orderbookHolder{ob: depth, buffer: &buffer, ticker: ticker} - w.ob[Key{Base: book.Pair.Base.Item, Quote: book.Pair.Quote.Item, Asset: book.Asset}] = holder + w.ob[key.PairAsset{Base: book.Pair.Base.Item, Quote: book.Pair.Quote.Item, Asset: book.Asset}] = holder } holder.updateID = book.LastUpdateID @@ -364,7 +365,7 @@ func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error { func (w *Orderbook) GetOrderbook(p currency.Pair, a asset.Item) (*orderbook.Base, error) { w.mtx.Lock() defer w.mtx.Unlock() - book, ok := w.ob[Key{Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] + book, ok := w.ob[key.PairAsset{Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] if !ok { return nil, fmt.Errorf("%s %s %s %w", w.exchangeName, p, a, errDepthNotFound) } @@ -375,7 +376,7 @@ func (w *Orderbook) GetOrderbook(p currency.Pair, a asset.Item) (*orderbook.Base // connection is lost and reconnected func (w *Orderbook) FlushBuffer() { w.mtx.Lock() - w.ob = make(map[Key]*orderbookHolder) + w.ob = make(map[key.PairAsset]*orderbookHolder) w.mtx.Unlock() } @@ -383,7 +384,7 @@ func (w *Orderbook) FlushBuffer() { func (w *Orderbook) FlushOrderbook(p currency.Pair, a asset.Item) error { w.mtx.Lock() defer w.mtx.Unlock() - book, ok := w.ob[Key{Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] + book, ok := w.ob[key.PairAsset{Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] if !ok { return fmt.Errorf("cannot flush orderbook %s %s %s %w", w.exchangeName, diff --git a/exchanges/stream/buffer/buffer_test.go b/exchanges/stream/buffer/buffer_test.go index 7f2ecfb3..3378ebe3 100644 --- a/exchanges/stream/buffer/buffer_test.go +++ b/exchanges/stream/buffer/buffer_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -44,7 +45,7 @@ func createSnapshot() (holder *Orderbook, asks, bids orderbook.Items, err error) LastUpdated: time.Now(), } - newBook := make(map[Key]*orderbookHolder) + newBook := make(map[key.PairAsset]*orderbookHolder) ch := make(chan interface{}) go func(<-chan interface{}) { // reader @@ -93,7 +94,7 @@ func BenchmarkUpdateBidsByPrice(b *testing.B) { UpdateTime: time.Now(), Asset: asset.Spot, } - holder := ob.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + holder := ob.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] err = holder.updateByPrice(update) if err != nil { b.Fatal(err) @@ -116,7 +117,7 @@ func BenchmarkUpdateAsksByPrice(b *testing.B) { UpdateTime: time.Now(), Asset: asset.Spot, } - holder := ob.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + holder := ob.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] err = holder.updateByPrice(update) if err != nil { b.Fatal(err) @@ -246,7 +247,7 @@ func TestUpdates(t *testing.T) { t.Error(err) } - book := holder.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + book := holder.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] err = book.updateByPrice(&orderbook.Update{ Bids: itemArray[5], Asks: itemArray[5], @@ -302,7 +303,7 @@ func TestHittingTheBuffer(t *testing.T) { } } - book := holder.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + book := holder.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] askLen, err := book.ob.GetAskLength() if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) @@ -350,7 +351,7 @@ func TestInsertWithIDs(t *testing.T) { } } - book := holder.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + book := holder.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] askLen, err := book.ob.GetAskLength() if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) @@ -393,7 +394,7 @@ func TestSortIDs(t *testing.T) { t.Fatal(err) } } - book := holder.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + book := holder.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] askLen, err := book.ob.GetAskLength() if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) @@ -438,7 +439,7 @@ func TestOutOfOrderIDs(t *testing.T) { t.Fatal(err) } } - book := holder.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + book := holder.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] cpy, err := book.ob.Retrieve() if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) @@ -570,7 +571,7 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) { func TestRunSnapshotWithNoData(t *testing.T) { t.Parallel() var obl Orderbook - obl.ob = make(map[Key]*orderbookHolder) + obl.ob = make(map[key.PairAsset]*orderbookHolder) obl.dataHandler = make(chan interface{}, 1) var snapShot1 orderbook.Base snapShot1.Asset = asset.Spot @@ -589,7 +590,7 @@ func TestLoadSnapshot(t *testing.T) { t.Parallel() var obl Orderbook obl.dataHandler = make(chan interface{}, 100) - obl.ob = make(map[Key]*orderbookHolder) + obl.ob = make(map[key.PairAsset]*orderbookHolder) var snapShot1 orderbook.Base snapShot1.Exchange = "SnapshotWithOverride" asks := []orderbook.Item{ @@ -615,11 +616,11 @@ func TestFlushBuffer(t *testing.T) { if err != nil { t.Fatal(err) } - if obl.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] == nil { + if obl.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] == nil { t.Error("expected ob to have ask entries") } obl.FlushBuffer() - if obl.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] != nil { + if obl.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] != nil { t.Error("expected ob be flushed") } } @@ -629,7 +630,7 @@ func TestInsertingSnapShots(t *testing.T) { t.Parallel() var holder Orderbook holder.dataHandler = make(chan interface{}, 100) - holder.ob = make(map[Key]*orderbookHolder) + holder.ob = make(map[key.PairAsset]*orderbookHolder) var snapShot1 orderbook.Base snapShot1.Exchange = "WSORDERBOOKTEST1" asks := []orderbook.Item{ @@ -795,7 +796,7 @@ func TestGetOrderbook(t *testing.T) { if err != nil { t.Fatal(err) } - bufferOb := holder.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + bufferOb := holder.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] b, err := bufferOb.ob.Retrieve() if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) @@ -888,7 +889,7 @@ func TestEnsureMultipleUpdatesViaPrice(t *testing.T) { } asks := bidAskGenerator() - book := holder.ob[Key{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] + book := holder.ob[key.PairAsset{Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot}] err = book.updateByPrice(&orderbook.Update{ Bids: asks, Asks: asks, diff --git a/exchanges/stream/buffer/buffer_types.go b/exchanges/stream/buffer/buffer_types.go index bc66df5c..9383e361 100644 --- a/exchanges/stream/buffer/buffer_types.go +++ b/exchanges/stream/buffer/buffer_types.go @@ -4,8 +4,7 @@ import ( "sync" "time" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" ) @@ -30,7 +29,7 @@ type Config struct { // Orderbook defines a local cache of orderbooks for amending, appending // and deleting changes and updates the main store for a stream type Orderbook struct { - ob map[Key]*orderbookHolder + ob map[key.PairAsset]*orderbookHolder obBufferLimit int bufferEnabled bool sortBuffer bool @@ -67,10 +66,3 @@ type orderbookHolder struct { ticker *time.Ticker updateID int64 } - -// Key defines a unique orderbook key for a specific pair and asset -type Key struct { - Base *currency.Item - Quote *currency.Item - Asset asset.Item -} diff --git a/exchanges/ticker/ticker.go b/exchanges/ticker/ticker.go index ddf03923..c0979160 100644 --- a/exchanges/ticker/ticker.go +++ b/exchanges/ticker/ticker.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -22,7 +23,7 @@ var ( func init() { service = new(Service) - service.Tickers = make(map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker) + service.Tickers = make(map[key.ExchangePairAsset]*Ticker) service.Exchange = make(map[string]uuid.UUID) service.mux = dispatch.GetNewMux(nil) } @@ -33,8 +34,12 @@ func SubscribeTicker(exchange string, p currency.Pair, a asset.Item) (dispatch.P exchange = strings.ToLower(exchange) service.mu.Lock() defer service.mu.Unlock() - - tick, ok := service.Tickers[exchange][p.Base.Item][p.Quote.Item][a] + tick, ok := service.Tickers[key.ExchangePairAsset{ + Exchange: exchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] if !ok { return dispatch.Pipe{}, fmt.Errorf("ticker item not found for %s %s %s", exchange, @@ -72,30 +77,18 @@ func GetTicker(exchange string, p currency.Pair, a asset.Item) (*Price, error) { exchange = strings.ToLower(exchange) service.mu.Lock() defer service.mu.Unlock() - m1, ok := service.Tickers[exchange] + tick, ok := service.Tickers[key.ExchangePairAsset{ + Exchange: exchange, + Base: p.Base.Item, + Quote: p.Quote.Item, + Asset: a, + }] if !ok { - return nil, fmt.Errorf("no tickers for %s exchange", exchange) + return nil, fmt.Errorf("no tickers associated with asset type %s %s %s", + exchange, p, a) } - m2, ok := m1[p.Base.Item] - if !ok { - return nil, fmt.Errorf("no tickers associated with base currency %s", - p.Base) - } - - m3, ok := m2[p.Quote.Item] - if !ok { - return nil, fmt.Errorf("no tickers associated with quote currency %s", - p.Quote) - } - - t, ok := m3[a] - if !ok { - return nil, fmt.Errorf("no tickers associated with asset type %s", - a) - } - - cpy := t.Price // Don't let external functions have access to underlying + cpy := tick.Price // Don't let external functions have access to underlying return &cpy, nil } @@ -103,20 +96,10 @@ func GetTicker(exchange string, p currency.Pair, a asset.Item) (*Price, error) { func FindLast(p currency.Pair, a asset.Item) (float64, error) { service.mu.Lock() defer service.mu.Unlock() - for _, m1 := range service.Tickers { - m2, ok := m1[p.Base.Item] - if !ok { + for mapKey, t := range service.Tickers { + if !mapKey.MatchesPairAsset(p, a) { continue } - m3, ok := m2[p.Quote.Item] - if !ok { - continue - } - t, ok := m3[a] - if !ok { - continue - } - if t.Last == 0 { return 0, errInvalidTicker } @@ -178,27 +161,14 @@ func ProcessTicker(p *Price) error { // update updates ticker price func (s *Service) update(p *Price) error { name := strings.ToLower(p.ExchangeName) + mapKey := key.ExchangePairAsset{ + Exchange: name, + Base: p.Pair.Base.Item, + Quote: p.Pair.Quote.Item, + Asset: p.AssetType, + } s.mu.Lock() - - m1, ok := service.Tickers[name] - if !ok { - m1 = make(map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker) - service.Tickers[name] = m1 - } - - m2, ok := m1[p.Pair.Base.Item] - if !ok { - m2 = make(map[*currency.Item]map[asset.Item]*Ticker) - m1[p.Pair.Base.Item] = m2 - } - - m3, ok := m2[p.Pair.Quote.Item] - if !ok { - m3 = make(map[asset.Item]*Ticker) - m2[p.Pair.Quote.Item] = m3 - } - - t, ok := m3[p.AssetType] + t, ok := service.Tickers[mapKey] if !ok || t == nil { newTicker := &Ticker{} err := s.setItemID(newTicker, p, name) @@ -206,7 +176,7 @@ func (s *Service) update(p *Price) error { s.mu.Unlock() return err } - m3[p.AssetType] = newTicker + service.Tickers[mapKey] = newTicker s.mu.Unlock() return nil } diff --git a/exchanges/ticker/ticker_types.go b/exchanges/ticker/ticker_types.go index 4d094824..6525a394 100644 --- a/exchanges/ticker/ticker_types.go +++ b/exchanges/ticker/ticker_types.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/dispatch" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -25,7 +26,7 @@ var ( // Service holds ticker information for each individual exchange type Service struct { - Tickers map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker + Tickers map[key.ExchangePairAsset]*Ticker Exchange map[string]uuid.UUID mux *dispatch.Mux mu sync.Mutex