mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
Backtester: USD tracking (#818)
* Initial concept for creating price tracking pairs * Completes coverage, even with a slow test * I dont know what point to hook this stuff up * Bit of a broken way of handling tracking pairs * Correctly calculates USD rates against all currencies * Removes dependency on GCT config * Failed currency statistics redesign * initial Update chart to use highcharts * Minor changes to stats * Creats funding stats to handle the stat calculations. Needs more work * tracks USD snapshots and BREAKS THINGS FURTHER * Fixed! * Adds ratio calculations and such, but its WRONG. do it at totals level dummy * End of day basic lint * Remaining lints * USD totals statistics * Minor panic fixes * Printing of funding stats, but its bad * Properly calculates overall benchmark, moves funding stat output * Adds some template charge, removes duplicate fields * New charts! * Darkcharts. funding protection when disabled * Now works with usd tracking/funding disabled! * Attempting to only show working stats based on settings. * Spruces up the goose/reporting * Completes report HTML rendering * lint and test fixes * funding statistics testing * slightly more test coverage * Test coverage * Initial documentation * Fixes tests * Database testing and rendering improvements and breakages * report and cmd rendering, linting. fix comma output. rm gct cfg * PR mode 🎉 Path field, config builder support,testing,linting,docs * minor calculation improvement * Secret lint that did not show up locally * Disable USD tracking for example configs * ShazNitNoScope * Forgotten errors * "" * literally Logarithmically logically renders the date 👀 * Fixes typos, fixes parallel test, fixes chart gui and exporting
This commit is contained in:
@@ -6,17 +6,18 @@ gloriousCode | https://github.com/gloriousCode
|
||||
dependabot-preview[bot] | https://github.com/apps/dependabot-preview
|
||||
xtda | https://github.com/xtda
|
||||
dependabot[bot] | https://github.com/apps/dependabot
|
||||
lrascao | https://github.com/lrascao
|
||||
Rots | https://github.com/Rots
|
||||
vazha | https://github.com/vazha
|
||||
ermalguni | https://github.com/ermalguni
|
||||
MadCozBadd | https://github.com/MadCozBadd
|
||||
ydm | https://github.com/ydm
|
||||
vadimzhukck | https://github.com/vadimzhukck
|
||||
lrascao | https://github.com/lrascao
|
||||
140am | https://github.com/140am
|
||||
marcofranssen | https://github.com/marcofranssen
|
||||
dackroyd | https://github.com/dackroyd
|
||||
cranktakular | https://github.com/cranktakular
|
||||
khcchiu | https://github.com/khcchiu
|
||||
woshidama323 | https://github.com/woshidama323
|
||||
yangrq1018 | https://github.com/yangrq1018
|
||||
TaltaM | https://github.com/TaltaM
|
||||
@@ -29,7 +30,6 @@ MarkDzulko | https://github.com/MarkDzulko
|
||||
gam-phon | https://github.com/gam-phon
|
||||
cornelk | https://github.com/cornelk
|
||||
if1live | https://github.com/if1live
|
||||
khcchiu | https://github.com/khcchiu
|
||||
herenow | https://github.com/herenow
|
||||
mshogin | https://github.com/mshogin
|
||||
soxipy | https://github.com/soxipy
|
||||
|
||||
12
README.md
12
README.md
@@ -144,23 +144,24 @@ Binaries will be published once the codebase reaches a stable condition.
|
||||
|
||||
|User|Contribution Amount|
|
||||
|--|--|
|
||||
| [thrasher-](https://github.com/thrasher-) | 658 |
|
||||
| [shazbert](https://github.com/shazbert) | 223 |
|
||||
| [thrasher-](https://github.com/thrasher-) | 660 |
|
||||
| [shazbert](https://github.com/shazbert) | 226 |
|
||||
| [gloriousCode](https://github.com/gloriousCode) | 191 |
|
||||
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 |
|
||||
| [xtda](https://github.com/xtda) | 47 |
|
||||
| [dependabot[bot]](https://github.com/apps/dependabot) | 24 |
|
||||
| [dependabot[bot]](https://github.com/apps/dependabot) | 29 |
|
||||
| [lrascao](https://github.com/lrascao) | 15 |
|
||||
| [Rots](https://github.com/Rots) | 15 |
|
||||
| [vazha](https://github.com/vazha) | 15 |
|
||||
| [ermalguni](https://github.com/ermalguni) | 14 |
|
||||
| [MadCozBadd](https://github.com/MadCozBadd) | 13 |
|
||||
| [ydm](https://github.com/ydm) | 11 |
|
||||
| [ydm](https://github.com/ydm) | 13 |
|
||||
| [vadimzhukck](https://github.com/vadimzhukck) | 10 |
|
||||
| [lrascao](https://github.com/lrascao) | 8 |
|
||||
| [140am](https://github.com/140am) | 8 |
|
||||
| [marcofranssen](https://github.com/marcofranssen) | 8 |
|
||||
| [dackroyd](https://github.com/dackroyd) | 5 |
|
||||
| [cranktakular](https://github.com/cranktakular) | 5 |
|
||||
| [khcchiu](https://github.com/khcchiu) | 4 |
|
||||
| [woshidama323](https://github.com/woshidama323) | 3 |
|
||||
| [yangrq1018](https://github.com/yangrq1018) | 3 |
|
||||
| [TaltaM](https://github.com/TaltaM) | 3 |
|
||||
@@ -173,7 +174,6 @@ Binaries will be published once the codebase reaches a stable condition.
|
||||
| [gam-phon](https://github.com/gam-phon) | 2 |
|
||||
| [cornelk](https://github.com/cornelk) | 2 |
|
||||
| [if1live](https://github.com/if1live) | 2 |
|
||||
| [khcchiu](https://github.com/khcchiu) | 2 |
|
||||
| [herenow](https://github.com/herenow) | 2 |
|
||||
| [mshogin](https://github.com/mshogin) | 2 |
|
||||
| [soxipy](https://github.com/soxipy) | 2 |
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
@@ -25,18 +26,18 @@ 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/eventhandlers/portfolio/risk"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/settings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics/currencystatistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding/trackingcurrencies"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/report"
|
||||
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
gctdatabase "github.com/thrasher-corp/gocryptotrader/database"
|
||||
"github.com/thrasher-corp/gocryptotrader/engine"
|
||||
@@ -50,7 +51,9 @@ import (
|
||||
// New returns a new BackTest instance
|
||||
func New() *BackTest {
|
||||
return &BackTest{
|
||||
shutdown: make(chan struct{}),
|
||||
shutdown: make(chan struct{}),
|
||||
Datas: &data.HandlerPerCurrency{},
|
||||
EventQueue: &eventholder.Holder{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,21 +65,35 @@ func (bt *BackTest) Reset() {
|
||||
bt.Statistic.Reset()
|
||||
bt.Exchange.Reset()
|
||||
bt.Funding.Reset()
|
||||
bt.Bot = nil
|
||||
bt.exchangeManager = nil
|
||||
bt.orderManager = nil
|
||||
bt.databaseManager = nil
|
||||
}
|
||||
|
||||
// NewFromConfig takes a strategy config and configures a backtester variable to run
|
||||
func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.Engine) (*BackTest, error) {
|
||||
func NewFromConfig(cfg *config.Config, templatePath, output string) (*BackTest, error) {
|
||||
log.Infoln(log.BackTester, "loading config...")
|
||||
if cfg == nil {
|
||||
return nil, errNilConfig
|
||||
}
|
||||
if bot == nil {
|
||||
return nil, errNilBot
|
||||
}
|
||||
var err error
|
||||
bt := New()
|
||||
bt.Datas = &data.HandlerPerCurrency{}
|
||||
bt.EventQueue = &eventholder.Holder{}
|
||||
bt.exchangeManager = engine.SetupExchangeManager()
|
||||
bt.orderManager, err = engine.SetupOrderManager(bt.exchangeManager, &engine.CommunicationManager{}, &sync.WaitGroup{}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = bt.orderManager.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.DataSettings.DatabaseData != nil {
|
||||
bt.databaseManager, err = engine.SetupDatabaseConnectionManager(&cfg.DataSettings.DatabaseData.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reports := &report.Data{
|
||||
Config: cfg,
|
||||
TemplatePath: templatePath,
|
||||
@@ -84,17 +101,12 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
}
|
||||
bt.Reports = reports
|
||||
|
||||
err := bt.setupBot(cfg, bot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buyRule := config.MinMax{
|
||||
buyRule := exchange.MinMax{
|
||||
MinimumSize: cfg.PortfolioSettings.BuySide.MinimumSize,
|
||||
MaximumSize: cfg.PortfolioSettings.BuySide.MaximumSize,
|
||||
MaximumTotal: cfg.PortfolioSettings.BuySide.MaximumTotal,
|
||||
}
|
||||
sellRule := config.MinMax{
|
||||
sellRule := exchange.MinMax{
|
||||
MinimumSize: cfg.PortfolioSettings.SellSide.MinimumSize,
|
||||
MaximumSize: cfg.PortfolioSettings.SellSide.MaximumSize,
|
||||
MaximumTotal: cfg.PortfolioSettings.SellSide.MaximumTotal,
|
||||
@@ -104,9 +116,11 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
SellSide: sellRule,
|
||||
}
|
||||
|
||||
useExchangeLevelFunding := cfg.StrategySettings.UseExchangeLevelFunding
|
||||
funds := funding.SetupFundingManager(useExchangeLevelFunding)
|
||||
if useExchangeLevelFunding {
|
||||
funds := funding.SetupFundingManager(
|
||||
cfg.StrategySettings.UseExchangeLevelFunding,
|
||||
cfg.StrategySettings.DisableUSDTracking,
|
||||
)
|
||||
if cfg.StrategySettings.UseExchangeLevelFunding {
|
||||
for i := range cfg.StrategySettings.ExchangeLevelFunding {
|
||||
var a asset.Item
|
||||
a, err = asset.New(cfg.StrategySettings.ExchangeLevelFunding[i].Asset)
|
||||
@@ -130,9 +144,43 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
}
|
||||
}
|
||||
|
||||
var emm = make(map[string]gctexchange.IBotExchange)
|
||||
for i := range cfg.CurrencySettings {
|
||||
_, ok := emm[cfg.CurrencySettings[i].ExchangeName]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
var exch gctexchange.IBotExchange
|
||||
exch, err = bt.exchangeManager.NewExchangeByName(cfg.CurrencySettings[i].ExchangeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = exch.GetDefaultConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exchBase := exch.GetBase()
|
||||
err = exch.UpdateTradablePairs(context.Background(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assets := exchBase.CurrencyPairs.GetAssetTypes(false)
|
||||
for i := range assets {
|
||||
exchBase.CurrencyPairs.Pairs[assets[i]].AssetEnabled = convert.BoolPtr(true)
|
||||
err = exch.SetPairs(exchBase.CurrencyPairs.Pairs[assets[i]].Available, assets[i], true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
bt.exchangeManager.Add(exch)
|
||||
emm[cfg.CurrencySettings[i].ExchangeName] = exch
|
||||
}
|
||||
|
||||
portfolioRisk := &risk.Risk{
|
||||
CurrencySettings: make(map[string]map[asset.Item]map[currency.Pair]*risk.CurrencySettings),
|
||||
}
|
||||
|
||||
for i := range cfg.CurrencySettings {
|
||||
if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName] == nil {
|
||||
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName] = make(map[asset.Item]map[currency.Pair]*risk.CurrencySettings)
|
||||
@@ -157,7 +205,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
q = currency.NewCode(cfg.CurrencySettings[i].Quote)
|
||||
curr = currency.NewPair(b, q)
|
||||
var exch gctexchange.IBotExchange
|
||||
exch, err = bot.ExchangeManager.GetExchangeByName(cfg.CurrencySettings[i].ExchangeName)
|
||||
exch, err = bt.exchangeManager.GetExchangeByName(cfg.CurrencySettings[i].ExchangeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -189,7 +237,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
}
|
||||
|
||||
var baseItem, quoteItem *funding.Item
|
||||
if useExchangeLevelFunding {
|
||||
if cfg.StrategySettings.UseExchangeLevelFunding {
|
||||
// add any remaining currency items that have no funding data in the strategy config
|
||||
baseItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
|
||||
a,
|
||||
@@ -252,6 +300,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bt.Funding = funds
|
||||
var p *portfolio.Portfolio
|
||||
p, err = portfolio.Setup(sizeManager, portfolioRisk, cfg.StatisticSettings.RiskFreeRate)
|
||||
@@ -275,12 +324,48 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
StrategyNickname: cfg.Nickname,
|
||||
StrategyDescription: bt.Strategy.Description(),
|
||||
StrategyGoal: cfg.Goal,
|
||||
ExchangeAssetPairStatistics: make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic),
|
||||
ExchangeAssetPairStatistics: make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic),
|
||||
RiskFreeRate: cfg.StatisticSettings.RiskFreeRate,
|
||||
CandleInterval: gctkline.Interval(cfg.DataSettings.Interval),
|
||||
FundManager: bt.Funding,
|
||||
}
|
||||
bt.Statistic = stats
|
||||
reports.Statistics = stats
|
||||
|
||||
if !cfg.StrategySettings.DisableUSDTracking {
|
||||
var trackingPairs []trackingcurrencies.TrackingPair
|
||||
for i := range cfg.CurrencySettings {
|
||||
trackingPairs = append(trackingPairs, trackingcurrencies.TrackingPair{
|
||||
Exchange: cfg.CurrencySettings[i].ExchangeName,
|
||||
Asset: cfg.CurrencySettings[i].Asset,
|
||||
Base: cfg.CurrencySettings[i].Base,
|
||||
Quote: cfg.CurrencySettings[i].Quote,
|
||||
})
|
||||
}
|
||||
trackingPairs, err = trackingcurrencies.CreateUSDTrackingPairs(trackingPairs, bt.exchangeManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trackingPairCheck:
|
||||
for i := range trackingPairs {
|
||||
for j := range cfg.CurrencySettings {
|
||||
if cfg.CurrencySettings[j].ExchangeName == trackingPairs[i].Exchange &&
|
||||
cfg.CurrencySettings[j].Asset == trackingPairs[i].Asset &&
|
||||
cfg.CurrencySettings[j].Base == trackingPairs[i].Base &&
|
||||
cfg.CurrencySettings[j].Quote == trackingPairs[i].Quote {
|
||||
continue trackingPairCheck
|
||||
}
|
||||
}
|
||||
cfg.CurrencySettings = append(cfg.CurrencySettings, config.CurrencySettings{
|
||||
ExchangeName: trackingPairs[i].Exchange,
|
||||
Asset: trackingPairs[i].Asset,
|
||||
Base: trackingPairs[i].Base,
|
||||
Quote: trackingPairs[i].Quote,
|
||||
USDTrackingPair: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
e, err := bt.setupExchangeSettings(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -288,8 +373,9 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
|
||||
bt.Exchange = &e
|
||||
for i := range e.CurrencySettings {
|
||||
var lookup *settings.Settings
|
||||
lookup, err = p.SetupCurrencySettingsMap(e.CurrencySettings[i].ExchangeName, e.CurrencySettings[i].AssetType, e.CurrencySettings[i].CurrencyPair)
|
||||
var lookup *portfolio.Settings
|
||||
|
||||
lookup, err = p.SetupCurrencySettingsMap(&e.CurrencySettings[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -324,109 +410,120 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
|
||||
|
||||
exchangeName := strings.ToLower(exch.GetName())
|
||||
bt.Datas.Setup()
|
||||
klineData, err := bt.loadData(cfg, exch, pair, a)
|
||||
klineData, err := bt.loadData(cfg, exch, pair, a, cfg.CurrencySettings[i].USDTrackingPair)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
bt.Datas.SetDataForCurrency(exchangeName, a, pair, klineData)
|
||||
var makerFee, takerFee decimal.Decimal
|
||||
if cfg.CurrencySettings[i].MakerFee.GreaterThan(decimal.Zero) {
|
||||
makerFee = cfg.CurrencySettings[i].MakerFee
|
||||
}
|
||||
if cfg.CurrencySettings[i].TakerFee.GreaterThan(decimal.Zero) {
|
||||
takerFee = cfg.CurrencySettings[i].TakerFee
|
||||
}
|
||||
if makerFee.IsZero() || takerFee.IsZero() {
|
||||
var apiMakerFee, apiTakerFee decimal.Decimal
|
||||
apiMakerFee, apiTakerFee = getFees(context.TODO(), exch, pair)
|
||||
if makerFee.IsZero() {
|
||||
makerFee = apiMakerFee
|
||||
}
|
||||
if takerFee.IsZero() {
|
||||
takerFee = apiTakerFee
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(decimal.Zero) {
|
||||
log.Warnf(log.BackTester, "invalid maximum slippage percent '%v'. Slippage percent is defined as a number, eg '100.00', defaulting to '%v'",
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent,
|
||||
slippage.DefaultMaximumSlippagePercent)
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.IsZero() {
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MinimumSlippagePercent.LessThan(decimal.Zero) {
|
||||
log.Warnf(log.BackTester, "invalid minimum slippage percent '%v'. Slippage percent is defined as a number, eg '80.00', defaulting to '%v'",
|
||||
cfg.CurrencySettings[i].MinimumSlippagePercent,
|
||||
slippage.DefaultMinimumSlippagePercent)
|
||||
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MinimumSlippagePercent.IsZero() {
|
||||
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(cfg.CurrencySettings[i].MinimumSlippagePercent) {
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
|
||||
realOrders := false
|
||||
if cfg.DataSettings.LiveData != nil {
|
||||
realOrders = cfg.DataSettings.LiveData.RealOrders
|
||||
}
|
||||
|
||||
buyRule := config.MinMax{
|
||||
MinimumSize: cfg.CurrencySettings[i].BuySide.MinimumSize,
|
||||
MaximumSize: cfg.CurrencySettings[i].BuySide.MaximumSize,
|
||||
MaximumTotal: cfg.CurrencySettings[i].BuySide.MaximumTotal,
|
||||
}
|
||||
sellRule := config.MinMax{
|
||||
MinimumSize: cfg.CurrencySettings[i].SellSide.MinimumSize,
|
||||
MaximumSize: cfg.CurrencySettings[i].SellSide.MaximumSize,
|
||||
MaximumTotal: cfg.CurrencySettings[i].SellSide.MaximumTotal,
|
||||
}
|
||||
|
||||
limits, err := exch.GetOrderExecutionLimits(a, pair)
|
||||
if err != nil && !errors.Is(err, gctorder.ErrExchangeLimitNotLoaded) {
|
||||
err = bt.Funding.AddUSDTrackingData(klineData)
|
||||
if err != nil &&
|
||||
!errors.Is(err, trackingcurrencies.ErrCurrencyDoesNotContainsUSD) &&
|
||||
!errors.Is(err, funding.ErrUSDTrackingDisabled) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if limits != nil {
|
||||
if !cfg.CurrencySettings[i].CanUseExchangeLimits {
|
||||
log.Warnf(log.BackTester, "exchange %s order execution limits supported but disabled for %s %s, live results may differ",
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
pair,
|
||||
a)
|
||||
cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true
|
||||
if !cfg.CurrencySettings[i].USDTrackingPair {
|
||||
bt.Datas.SetDataForCurrency(exchangeName, a, pair, klineData)
|
||||
var makerFee, takerFee decimal.Decimal
|
||||
if cfg.CurrencySettings[i].MakerFee.GreaterThan(decimal.Zero) {
|
||||
makerFee = cfg.CurrencySettings[i].MakerFee
|
||||
}
|
||||
if cfg.CurrencySettings[i].TakerFee.GreaterThan(decimal.Zero) {
|
||||
takerFee = cfg.CurrencySettings[i].TakerFee
|
||||
}
|
||||
if makerFee.IsZero() || takerFee.IsZero() {
|
||||
var apiMakerFee, apiTakerFee decimal.Decimal
|
||||
apiMakerFee, apiTakerFee = getFees(context.TODO(), exch, pair)
|
||||
if makerFee.IsZero() {
|
||||
makerFee = apiMakerFee
|
||||
}
|
||||
if takerFee.IsZero() {
|
||||
takerFee = apiTakerFee
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(decimal.Zero) {
|
||||
log.Warnf(log.BackTester, "invalid maximum slippage percent '%v'. Slippage percent is defined as a number, eg '100.00', defaulting to '%v'",
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent,
|
||||
slippage.DefaultMaximumSlippagePercent)
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.IsZero() {
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MinimumSlippagePercent.LessThan(decimal.Zero) {
|
||||
log.Warnf(log.BackTester, "invalid minimum slippage percent '%v'. Slippage percent is defined as a number, eg '80.00', defaulting to '%v'",
|
||||
cfg.CurrencySettings[i].MinimumSlippagePercent,
|
||||
slippage.DefaultMinimumSlippagePercent)
|
||||
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MinimumSlippagePercent.IsZero() {
|
||||
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(cfg.CurrencySettings[i].MinimumSlippagePercent) {
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
|
||||
realOrders := false
|
||||
if cfg.DataSettings.LiveData != nil {
|
||||
realOrders = cfg.DataSettings.LiveData.RealOrders
|
||||
}
|
||||
|
||||
buyRule := exchange.MinMax{
|
||||
MinimumSize: cfg.CurrencySettings[i].BuySide.MinimumSize,
|
||||
MaximumSize: cfg.CurrencySettings[i].BuySide.MaximumSize,
|
||||
MaximumTotal: cfg.CurrencySettings[i].BuySide.MaximumTotal,
|
||||
}
|
||||
sellRule := exchange.MinMax{
|
||||
MinimumSize: cfg.CurrencySettings[i].SellSide.MinimumSize,
|
||||
MaximumSize: cfg.CurrencySettings[i].SellSide.MaximumSize,
|
||||
MaximumTotal: cfg.CurrencySettings[i].SellSide.MaximumTotal,
|
||||
}
|
||||
|
||||
limits, err := exch.GetOrderExecutionLimits(a, pair)
|
||||
if err != nil && !errors.Is(err, gctorder.ErrExchangeLimitNotLoaded) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if limits != nil {
|
||||
if !cfg.CurrencySettings[i].CanUseExchangeLimits {
|
||||
log.Warnf(log.BackTester, "exchange %s order execution limits supported but disabled for %s %s, live results may differ",
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
pair,
|
||||
a)
|
||||
cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true
|
||||
}
|
||||
}
|
||||
|
||||
resp.CurrencySettings = append(resp.CurrencySettings, exchange.Settings{
|
||||
Exchange: cfg.CurrencySettings[i].ExchangeName,
|
||||
MinimumSlippageRate: cfg.CurrencySettings[i].MinimumSlippagePercent,
|
||||
MaximumSlippageRate: cfg.CurrencySettings[i].MaximumSlippagePercent,
|
||||
Pair: pair,
|
||||
Asset: a,
|
||||
ExchangeFee: takerFee,
|
||||
MakerFee: takerFee,
|
||||
TakerFee: makerFee,
|
||||
UseRealOrders: realOrders,
|
||||
BuySide: buyRule,
|
||||
SellSide: sellRule,
|
||||
Leverage: exchange.Leverage{
|
||||
CanUseLeverage: cfg.CurrencySettings[i].Leverage.CanUseLeverage,
|
||||
MaximumLeverageRate: cfg.CurrencySettings[i].Leverage.MaximumLeverageRate,
|
||||
MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].Leverage.MaximumOrdersWithLeverageRatio,
|
||||
},
|
||||
Limits: limits,
|
||||
SkipCandleVolumeFitting: cfg.CurrencySettings[i].SkipCandleVolumeFitting,
|
||||
CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits,
|
||||
})
|
||||
}
|
||||
resp.CurrencySettings = append(resp.CurrencySettings, exchange.Settings{
|
||||
ExchangeName: cfg.CurrencySettings[i].ExchangeName,
|
||||
MinimumSlippageRate: cfg.CurrencySettings[i].MinimumSlippagePercent,
|
||||
MaximumSlippageRate: cfg.CurrencySettings[i].MaximumSlippagePercent,
|
||||
CurrencyPair: pair,
|
||||
AssetType: a,
|
||||
ExchangeFee: takerFee,
|
||||
MakerFee: takerFee,
|
||||
TakerFee: makerFee,
|
||||
UseRealOrders: realOrders,
|
||||
BuySide: buyRule,
|
||||
SellSide: sellRule,
|
||||
Leverage: config.Leverage{
|
||||
CanUseLeverage: cfg.CurrencySettings[i].Leverage.CanUseLeverage,
|
||||
MaximumLeverageRate: cfg.CurrencySettings[i].Leverage.MaximumLeverageRate,
|
||||
MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].Leverage.MaximumOrdersWithLeverageRatio,
|
||||
},
|
||||
Limits: limits,
|
||||
SkipCandleVolumeFitting: cfg.CurrencySettings[i].SkipCandleVolumeFitting,
|
||||
CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (bt *BackTest) loadExchangePairAssetBase(exch, base, quote, ass string) (gctexchange.IBotExchange, currency.Pair, asset.Item, error) {
|
||||
e, err := bt.Bot.GetExchangeByName(exch)
|
||||
e, err := bt.exchangeManager.GetExchangeByName(exch)
|
||||
if err != nil {
|
||||
return nil, currency.Pair{}, "", err
|
||||
}
|
||||
@@ -455,36 +552,6 @@ func (bt *BackTest) loadExchangePairAssetBase(exch, base, quote, ass string) (gc
|
||||
return e, fPair, a, nil
|
||||
}
|
||||
|
||||
// setupBot sets up a basic bot to retrieve exchange data
|
||||
// as well as process orders
|
||||
func (bt *BackTest) setupBot(cfg *config.Config, bot *engine.Engine) error {
|
||||
var err error
|
||||
bt.Bot = bot
|
||||
bt.Bot.ExchangeManager = engine.SetupExchangeManager()
|
||||
for i := range cfg.CurrencySettings {
|
||||
err = bt.Bot.LoadExchange(cfg.CurrencySettings[i].ExchangeName, nil)
|
||||
if err != nil && !errors.Is(err, engine.ErrExchangeAlreadyLoaded) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !bt.Bot.OrderManager.IsRunning() {
|
||||
bt.Bot.OrderManager, err = engine.SetupOrderManager(
|
||||
bt.Bot.ExchangeManager,
|
||||
bt.Bot.CommunicationsManager,
|
||||
&bt.Bot.ServicesWG,
|
||||
bot.Settings.Verbose)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bt.Bot.OrderManager.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFees will return an exchange's fee rate from GCT's wrapper function
|
||||
func getFees(ctx context.Context, exch gctexchange.IBotExchange, fPair currency.Pair) (makerFee, takerFee decimal.Decimal) {
|
||||
fTakerFee, err := exch.GetFeeByType(ctx,
|
||||
@@ -515,7 +582,7 @@ func getFees(ctx context.Context, exch gctexchange.IBotExchange, fPair currency.
|
||||
|
||||
// loadData will create kline data from the sources defined in start config files. It can exist from databases, csv or API endpoints
|
||||
// it can also be generated from trade data which will be converted into kline data
|
||||
func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item) (*kline.DataFromKline, error) {
|
||||
func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
|
||||
if exch == nil {
|
||||
return nil, engine.ErrExchangeNotFound
|
||||
}
|
||||
@@ -553,7 +620,8 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
|
||||
strings.ToLower(exch.GetName()),
|
||||
cfg.DataSettings.Interval,
|
||||
fPair,
|
||||
a)
|
||||
a,
|
||||
isUSDTrackingPair)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v. Please check your GoCryptoTrader configuration", err)
|
||||
}
|
||||
@@ -577,30 +645,25 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
|
||||
if cfg.DataSettings.DatabaseData.InclusiveEndDate {
|
||||
cfg.DataSettings.DatabaseData.EndDate = cfg.DataSettings.DatabaseData.EndDate.Add(cfg.DataSettings.Interval)
|
||||
}
|
||||
if cfg.DataSettings.DatabaseData.ConfigOverride != nil {
|
||||
bt.Bot.Config.Database = *cfg.DataSettings.DatabaseData.ConfigOverride
|
||||
gctdatabase.DB.DataPath = filepath.Join(gctcommon.GetDefaultDataDir(runtime.GOOS), "database")
|
||||
err = gctdatabase.DB.SetConfig(cfg.DataSettings.DatabaseData.ConfigOverride)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.DataSettings.DatabaseData.Path == "" {
|
||||
cfg.DataSettings.DatabaseData.Path = filepath.Join(gctcommon.GetDefaultDataDir(runtime.GOOS), "database")
|
||||
}
|
||||
bt.Bot.DatabaseManager, err = engine.SetupDatabaseConnectionManager(gctdatabase.DB.GetConfig())
|
||||
gctdatabase.DB.DataPath = filepath.Join(cfg.DataSettings.DatabaseData.Path)
|
||||
err = gctdatabase.DB.SetConfig(&cfg.DataSettings.DatabaseData.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = bt.Bot.DatabaseManager.Start(&bt.Bot.ServicesWG)
|
||||
err = bt.databaseManager.Start(&sync.WaitGroup{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
stopErr := bt.Bot.DatabaseManager.Stop()
|
||||
stopErr := bt.databaseManager.Stop()
|
||||
if stopErr != nil {
|
||||
log.Error(log.BackTester, stopErr)
|
||||
}
|
||||
}()
|
||||
resp, err = loadDatabaseData(cfg, exch.GetName(), fPair, a, dataType)
|
||||
resp, err = loadDatabaseData(cfg, exch.GetName(), fPair, a, dataType, isUSDTrackingPair)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve data from GoCryptoTrader database. Error: %v. Please ensure the database is setup correctly and has data before use", err)
|
||||
}
|
||||
@@ -636,6 +699,9 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
|
||||
return resp, err
|
||||
}
|
||||
case cfg.DataSettings.LiveData != nil:
|
||||
if isUSDTrackingPair {
|
||||
return nil, errLiveUSDTrackingNotSupported
|
||||
}
|
||||
if len(cfg.CurrencySettings) > 1 {
|
||||
return nil, errors.New("live data simulation only supports one currency")
|
||||
}
|
||||
@@ -671,7 +737,7 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func loadDatabaseData(cfg *config.Config, name string, fPair currency.Pair, a asset.Item, dataType int64) (*kline.DataFromKline, error) {
|
||||
func loadDatabaseData(cfg *config.Config, name string, fPair currency.Pair, a asset.Item, dataType int64, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
|
||||
if cfg == nil || cfg.DataSettings.DatabaseData == nil {
|
||||
return nil, errors.New("nil config data received")
|
||||
}
|
||||
@@ -686,7 +752,8 @@ func loadDatabaseData(cfg *config.Config, name string, fPair currency.Pair, a as
|
||||
strings.ToLower(name),
|
||||
dataType,
|
||||
fPair,
|
||||
a)
|
||||
a,
|
||||
isUSDTrackingPair)
|
||||
}
|
||||
|
||||
func loadAPIData(cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, resultLimit uint32, dataType int64) (*kline.DataFromKline, error) {
|
||||
@@ -811,9 +878,19 @@ func (bt *BackTest) handleEvent(ev common.EventHandler) error {
|
||||
switch eType := ev.(type) {
|
||||
case common.DataEventHandler:
|
||||
if bt.Strategy.UsingSimultaneousProcessing() {
|
||||
return bt.processSimultaneousDataEvents()
|
||||
err = bt.processSimultaneousDataEvents()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bt.Funding.CreateSnapshot(ev.GetTime())
|
||||
return nil
|
||||
}
|
||||
return bt.processSingleDataEvent(eType, funds)
|
||||
err = bt.processSingleDataEvent(eType, funds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bt.Funding.CreateSnapshot(ev.GetTime())
|
||||
return nil
|
||||
case signal.Event:
|
||||
bt.processSignalEvent(eType, funds)
|
||||
case order.Event:
|
||||
@@ -835,7 +912,7 @@ func (bt *BackTest) processSingleDataEvent(ev common.DataEventHandler, funds fun
|
||||
return err
|
||||
}
|
||||
d := bt.Datas.GetDataForCurrency(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
||||
s, err := bt.Strategy.OnSignal(d, bt.Funding)
|
||||
s, err := bt.Strategy.OnSignal(d, bt.Funding, bt.Portfolio)
|
||||
if err != nil {
|
||||
if errors.Is(err, base.ErrTooMuchBadData) {
|
||||
// too much bad data is a severe error and backtesting must cease
|
||||
@@ -880,7 +957,7 @@ func (bt *BackTest) processSimultaneousDataEvents() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
signals, err := bt.Strategy.OnSimultaneousSignals(dataEvents, bt.Funding)
|
||||
signals, err := bt.Strategy.OnSimultaneousSignals(dataEvents, bt.Funding, bt.Portfolio)
|
||||
if err != nil {
|
||||
if errors.Is(err, base.ErrTooMuchBadData) {
|
||||
// too much bad data is a severe error and backtesting must cease
|
||||
@@ -941,7 +1018,7 @@ func (bt *BackTest) processSignalEvent(ev signal.Event, funds funding.IPairReser
|
||||
|
||||
func (bt *BackTest) processOrderEvent(ev order.Event, funds funding.IPairReleaser) {
|
||||
d := bt.Datas.GetDataForCurrency(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
||||
f, err := bt.Exchange.ExecuteOrder(ev, d, bt.Bot, funds)
|
||||
f, err := bt.Exchange.ExecuteOrder(ev, d, bt.orderManager, funds)
|
||||
if err != nil {
|
||||
if f == nil {
|
||||
log.Errorf(log.BackTester, "fill event should always be returned, please fix, %v", err)
|
||||
|
||||
@@ -2,9 +2,7 @@ package backtest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -20,7 +18,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics/currencystatistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage"
|
||||
@@ -28,7 +25,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/report"
|
||||
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
||||
gctconfig "github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/database"
|
||||
"github.com/thrasher-corp/gocryptotrader/database/drivers"
|
||||
@@ -48,55 +44,15 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func newBotWithExchange() *engine.Engine {
|
||||
bot := &engine.Engine{
|
||||
Config: &gctconfig.Config{
|
||||
Exchanges: []gctconfig.Exchange{
|
||||
{
|
||||
Name: testExchange,
|
||||
Enabled: true,
|
||||
WebsocketTrafficTimeout: time.Second,
|
||||
CurrencyPairs: ¤cy.PairsManager{
|
||||
Pairs: map[asset.Item]*currency.PairStore{
|
||||
asset.Spot: {
|
||||
AssetEnabled: convert.BoolPtr(true),
|
||||
Available: []currency.Pair{currency.NewPair(currency.BTC, currency.USD)},
|
||||
Enabled: []currency.Pair{currency.NewPair(currency.BTC, currency.USD)},
|
||||
ConfigFormat: ¤cy.PairFormat{},
|
||||
RequestFormat: ¤cy.PairFormat{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
em := engine.SetupExchangeManager()
|
||||
exch, err := em.NewExchangeByName(testExchange)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
exch.SetDefaults()
|
||||
em.Add(exch)
|
||||
bot.ExchangeManager = em
|
||||
return bot
|
||||
}
|
||||
|
||||
func TestNewFromConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := NewFromConfig(nil, "", "", nil)
|
||||
_, err := NewFromConfig(nil, "", "")
|
||||
if !errors.Is(err, errNilConfig) {
|
||||
t.Errorf("received %v, expected %v", err, errNilConfig)
|
||||
}
|
||||
|
||||
cfg := &config.Config{}
|
||||
_, err = NewFromConfig(cfg, "", "", nil)
|
||||
if !errors.Is(err, errNilBot) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNilBot)
|
||||
}
|
||||
|
||||
bot := newBotWithExchange()
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, base.ErrStrategyNotFound) {
|
||||
t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
|
||||
}
|
||||
@@ -108,24 +64,24 @@ func TestNewFromConfig(t *testing.T) {
|
||||
Quote: "test",
|
||||
},
|
||||
}
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, engine.ErrExchangeNotFound) {
|
||||
t.Errorf("received: %v, expected: %v", err, engine.ErrExchangeNotFound)
|
||||
}
|
||||
cfg.CurrencySettings[0].ExchangeName = testExchange
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, errInvalidConfigAsset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errInvalidConfigAsset)
|
||||
}
|
||||
cfg.CurrencySettings[0].Asset = asset.Spot.String()
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, currency.ErrPairNotFound) {
|
||||
t.Errorf("received: %v, expected: %v", err, currency.ErrPairNotFound)
|
||||
}
|
||||
|
||||
cfg.CurrencySettings[0].Base = "btc"
|
||||
cfg.CurrencySettings[0].Quote = "usd"
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, base.ErrStrategyNotFound) {
|
||||
t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
|
||||
}
|
||||
@@ -143,19 +99,19 @@ func TestNewFromConfig(t *testing.T) {
|
||||
EndDate: time.Time{},
|
||||
}
|
||||
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if err != nil && !strings.Contains(err.Error(), "unrecognised dataType") {
|
||||
t.Error(err)
|
||||
}
|
||||
cfg.DataSettings.DataType = common.CandleStr
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, errIntervalUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errIntervalUnset)
|
||||
}
|
||||
cfg.DataSettings.Interval = gctkline.OneMin.Duration()
|
||||
cfg.CurrencySettings[0].MakerFee = decimal.Zero
|
||||
cfg.CurrencySettings[0].TakerFee = decimal.Zero
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, gctcommon.ErrDateUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrDateUnset)
|
||||
}
|
||||
@@ -163,7 +119,7 @@ func TestNewFromConfig(t *testing.T) {
|
||||
cfg.DataSettings.APIData.StartDate = time.Now().Add(-time.Minute)
|
||||
cfg.DataSettings.APIData.EndDate = time.Now()
|
||||
cfg.DataSettings.APIData.InclusiveEndDate = true
|
||||
_, err = NewFromConfig(cfg, "", "", bot)
|
||||
_, err = NewFromConfig(cfg, "", "")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
@@ -173,7 +129,6 @@ func TestLoadDataAPI(t *testing.T) {
|
||||
t.Parallel()
|
||||
bt := BackTest{
|
||||
Reports: &report.Data{},
|
||||
Bot: &engine.Engine{},
|
||||
}
|
||||
cp := currency.NewPair(currency.BTC, currency.USDT)
|
||||
cfg := &config.Config{
|
||||
@@ -220,7 +175,7 @@ func TestLoadDataAPI(t *testing.T) {
|
||||
ConfigFormat: ¤cy.PairFormat{Uppercase: true},
|
||||
RequestFormat: ¤cy.PairFormat{Uppercase: true}}
|
||||
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot)
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -230,9 +185,6 @@ func TestLoadDataDatabase(t *testing.T) {
|
||||
t.Parallel()
|
||||
bt := BackTest{
|
||||
Reports: &report.Data{},
|
||||
Bot: &engine.Engine{
|
||||
Config: &gctconfig.Config{Database: database.Config{}},
|
||||
},
|
||||
}
|
||||
cp := currency.NewPair(currency.BTC, currency.USDT)
|
||||
cfg := &config.Config{
|
||||
@@ -254,7 +206,7 @@ func TestLoadDataDatabase(t *testing.T) {
|
||||
DataType: common.CandleStr,
|
||||
Interval: gctkline.OneMin.Duration(),
|
||||
DatabaseData: &config.DatabaseData{
|
||||
ConfigOverride: &database.Config{
|
||||
Config: database.Config{
|
||||
Enabled: true,
|
||||
Driver: "sqlite3",
|
||||
ConnectionDetails: drivers.ConnectionDetails{
|
||||
@@ -286,8 +238,11 @@ func TestLoadDataDatabase(t *testing.T) {
|
||||
AssetEnabled: convert.BoolPtr(true),
|
||||
ConfigFormat: ¤cy.PairFormat{Uppercase: true},
|
||||
RequestFormat: ¤cy.PairFormat{Uppercase: true}}
|
||||
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot)
|
||||
bt.databaseManager, err = engine.SetupDatabaseConnectionManager(&cfg.DataSettings.DatabaseData.Config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
|
||||
if err != nil && !strings.Contains(err.Error(), "unable to retrieve data from GoCryptoTrader database") {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -297,7 +252,6 @@ func TestLoadDataCSV(t *testing.T) {
|
||||
t.Parallel()
|
||||
bt := BackTest{
|
||||
Reports: &report.Data{},
|
||||
Bot: &engine.Engine{},
|
||||
}
|
||||
cp := currency.NewPair(currency.BTC, currency.USDT)
|
||||
cfg := &config.Config{
|
||||
@@ -342,7 +296,7 @@ func TestLoadDataCSV(t *testing.T) {
|
||||
AssetEnabled: convert.BoolPtr(true),
|
||||
ConfigFormat: ¤cy.PairFormat{Uppercase: true},
|
||||
RequestFormat: ¤cy.PairFormat{Uppercase: true}}
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot)
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
|
||||
if err != nil &&
|
||||
!strings.Contains(err.Error(), "The system cannot find the file specified.") &&
|
||||
!strings.Contains(err.Error(), "no such file or directory") {
|
||||
@@ -354,7 +308,6 @@ func TestLoadDataLive(t *testing.T) {
|
||||
t.Parallel()
|
||||
bt := BackTest{
|
||||
Reports: &report.Data{},
|
||||
Bot: &engine.Engine{},
|
||||
shutdown: make(chan struct{}),
|
||||
}
|
||||
cp := currency.NewPair(currency.BTC, currency.USDT)
|
||||
@@ -404,7 +357,7 @@ func TestLoadDataLive(t *testing.T) {
|
||||
AssetEnabled: convert.BoolPtr(true),
|
||||
ConfigFormat: ¤cy.PairFormat{Uppercase: true},
|
||||
RequestFormat: ¤cy.PairFormat{Uppercase: true}}
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot)
|
||||
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -417,9 +370,7 @@ func TestLoadLiveData(t *testing.T) {
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Error(err)
|
||||
}
|
||||
cfg := &config.Config{
|
||||
GoCryptoTraderConfigPath: filepath.Join("..", "..", "testdata", "configtest.json"),
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
err = loadLiveData(cfg, nil)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Error(err)
|
||||
@@ -480,8 +431,8 @@ func TestLoadLiveData(t *testing.T) {
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := funding.SetupFundingManager(true, false)
|
||||
bt := BackTest{
|
||||
Bot: &engine.Engine{},
|
||||
shutdown: make(chan struct{}),
|
||||
Datas: &data.HandlerPerCurrency{},
|
||||
Strategy: &dollarcostaverage.Strategy{},
|
||||
@@ -490,11 +441,11 @@ func TestReset(t *testing.T) {
|
||||
Statistic: &statistics.Statistic{},
|
||||
EventQueue: &eventholder.Holder{},
|
||||
Reports: &report.Data{},
|
||||
Funding: &funding.FundManager{},
|
||||
Funding: f,
|
||||
}
|
||||
bt.Reset()
|
||||
if bt.Bot != nil {
|
||||
t.Error("expected nil")
|
||||
if bt.Funding.IsUsingExchangeLevelFunding() {
|
||||
t.Error("expected false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,23 +457,22 @@ func TestFullCycle(t *testing.T) {
|
||||
tt := time.Now()
|
||||
|
||||
stats := &statistics.Statistic{}
|
||||
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
|
||||
port, err := portfolio.Setup(&size.Size{
|
||||
BuySide: config.MinMax{},
|
||||
SellSide: config.MinMax{},
|
||||
BuySide: exchange.MinMax{},
|
||||
SellSide: exchange.MinMax{},
|
||||
}, &risk.Risk{}, decimal.Zero)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = port.SetupCurrencySettingsMap(ex, a, cp)
|
||||
_, err = port.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ex, Asset: a, Pair: cp})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
bot := newBotWithExchange()
|
||||
f := &funding.FundManager{}
|
||||
f := funding.SetupFundingManager(false, true)
|
||||
b, err := funding.CreateItem(ex, a, cp.Base, decimal.Zero, decimal.Zero)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -540,7 +490,6 @@ func TestFullCycle(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
bt := BackTest{
|
||||
Bot: bot,
|
||||
shutdown: nil,
|
||||
Datas: &data.HandlerPerCurrency{},
|
||||
Strategy: &dollarcostaverage.Strategy{},
|
||||
@@ -613,23 +562,22 @@ func TestFullCycleMulti(t *testing.T) {
|
||||
tt := time.Now()
|
||||
|
||||
stats := &statistics.Statistic{}
|
||||
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
stats.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
stats.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
|
||||
port, err := portfolio.Setup(&size.Size{
|
||||
BuySide: config.MinMax{},
|
||||
SellSide: config.MinMax{},
|
||||
BuySide: exchange.MinMax{},
|
||||
SellSide: exchange.MinMax{},
|
||||
}, &risk.Risk{}, decimal.Zero)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_, err = port.SetupCurrencySettingsMap(ex, a, cp)
|
||||
_, err = port.SetupCurrencySettingsMap(&exchange.Settings{Exchange: ex, Asset: a, Pair: cp})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
bot := newBotWithExchange()
|
||||
f := &funding.FundManager{}
|
||||
f := funding.SetupFundingManager(false, true)
|
||||
b, err := funding.CreateItem(ex, a, cp.Base, decimal.Zero, decimal.Zero)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -647,7 +595,6 @@ func TestFullCycleMulti(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
bt := BackTest{
|
||||
Bot: bot,
|
||||
shutdown: nil,
|
||||
Datas: &data.HandlerPerCurrency{},
|
||||
Portfolio: port,
|
||||
|
||||
@@ -15,21 +15,20 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
errNilConfig = errors.New("unable to setup backtester with nil config")
|
||||
errNilBot = errors.New("unable to setup backtester without a loaded GoCryptoTrader bot")
|
||||
errInvalidConfigAsset = errors.New("invalid asset in config")
|
||||
errAmbiguousDataSource = errors.New("ambiguous settings received. Only one data type can be set")
|
||||
errNoDataSource = errors.New("no data settings set in config")
|
||||
errIntervalUnset = errors.New("candle interval unset")
|
||||
errUnhandledDatatype = errors.New("unhandled datatype")
|
||||
errLiveDataTimeout = errors.New("no data returned in 5 minutes, shutting down")
|
||||
errNilData = errors.New("nil data received")
|
||||
errNilExchange = errors.New("nil exchange received")
|
||||
errNilConfig = errors.New("unable to setup backtester with nil config")
|
||||
errInvalidConfigAsset = errors.New("invalid asset in config")
|
||||
errAmbiguousDataSource = errors.New("ambiguous settings received. Only one data type can be set")
|
||||
errNoDataSource = errors.New("no data settings set in config")
|
||||
errIntervalUnset = errors.New("candle interval unset")
|
||||
errUnhandledDatatype = errors.New("unhandled datatype")
|
||||
errLiveDataTimeout = errors.New("no data returned in 5 minutes, shutting down")
|
||||
errNilData = errors.New("nil data received")
|
||||
errNilExchange = errors.New("nil exchange received")
|
||||
errLiveUSDTrackingNotSupported = errors.New("USD tracking not supported for live data")
|
||||
)
|
||||
|
||||
// BackTest is the main holder of all backtesting functionality
|
||||
type BackTest struct {
|
||||
Bot *engine.Engine
|
||||
hasHandledEvent bool
|
||||
shutdown chan struct{}
|
||||
Datas data.Holder
|
||||
@@ -40,4 +39,7 @@ type BackTest struct {
|
||||
EventQueue eventholder.EventHolder
|
||||
Reports report.Handler
|
||||
Funding funding.IFundingManager
|
||||
exchangeManager *engine.ExchangeManager
|
||||
orderManager *engine.OrderManager
|
||||
databaseManager *engine.DatabaseConnectionManager
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ See below for a set of tables and fields, expected values and what they can do
|
||||
| CustomSettings | This is a map where you can enter custom settings for a strategy. The RSI strategy allows for customisation of the upper, lower and length variables to allow you to change them from 70, 30 and 14 respectively to 69, 36, 12 | `"custom-settings": { "rsi-high": 70, "rsi-low": 30, "rsi-period": 14 } ` |
|
||||
| UseExchangeLevelFunding | Allows shared funding at an exchange asset level. You can set funding for `USDT` and all pairs that feature `USDT` will have access to those funds when making orders. See [this](/backtester/funding/README.md) for more information | `false` |
|
||||
| ExchangeLevelFunding | An array of exchange level funding settings. See below, or [this](/backtester/funding/README.md) for more information | `[]` |
|
||||
| DisableUSDTracking | If `false`, will track all currencies used in your strategy against USD equivalent candles. For example, if you are running a strategy for BTC/XRP, then the GoCryptoTrader Backtester will also retreive candles data for BTC/USD and XRP/USD to then track strategy performance against a single currency. This also tracks against USDT and other USD tracked stablecoins, so one exchange supporting USDT and another BUSD will still allow unified strategy performance analysis. If disabled, will not track against USD, this can be especially helpful when running strategies under live, database and CSV based data | `false` |
|
||||
|
||||
##### Funding Config Settings
|
||||
|
||||
@@ -132,9 +133,30 @@ See below for a set of tables and fields, expected values and what they can do
|
||||
| Interval | The candle interval in `time.Duration` format eg set as`15000000000` for a value of `time.Second * 15` | `15000000000` |
|
||||
| StartDate | The start date to retrieve data | `2021-01-23T11:00:00+11:00` |
|
||||
| EndDate | The end date to retrieve data | `2021-01-24T11:00:00+11:00` |
|
||||
| ConfigOverride | Override GoCryptoTrader's config database data with custom settings | `true` |
|
||||
| Config | This is the same struct used as your GoCryptoTrader database config. See below tables for breakdown | `see below` |
|
||||
| Path | If using SQLite, the path to the directory, not the file. Leaving blank will use GoCryptoTrader's default database path | `` |
|
||||
| InclusiveEndDate | When enabled, the end date's candle is included in the results. ie `2021-01-24T11:00:00+11:00` with a one hour candle, the final candle will be `2021-01-24T11:00:00+11:00` to `2021-01-24T12:00:00+11:00` | `false` |
|
||||
|
||||
##### database
|
||||
|
||||
| Config | Description | Example |
|
||||
| ------ | ----------- | ------- |
|
||||
| enabled | Enabled or disables the database connection subsystem | `true` |
|
||||
| verbose | Displays more information to the logger which can be helpful for debugging | `false` |
|
||||
| driver | The SQL driver to use. Can be `postgres` or `sqlite` | `sqlite` |
|
||||
| connectionDetails | See below | |
|
||||
|
||||
##### connectionDetails
|
||||
|
||||
| Config | Description | Example |
|
||||
| ------ | ----------- | ------- |
|
||||
| host | The host address of the database | `localhost` |
|
||||
| port | The port used to connect to the database | `5432` |
|
||||
| username | An optional username to connect to the database | `username` |
|
||||
| password | An optional password to connect to the database | `password` |
|
||||
| database | The name of the database | `database.db` |
|
||||
| sslmode | The connection type of the database for Postgres databases only | `disable` |
|
||||
|
||||
#### LiveData
|
||||
|
||||
| Key | Description | Example |
|
||||
|
||||
@@ -53,6 +53,7 @@ func (c *Config) PrintSetting() {
|
||||
}
|
||||
log.Infof(log.BackTester, "Simultaneous Signal Processing: %v", c.StrategySettings.SimultaneousSignalProcessing)
|
||||
log.Infof(log.BackTester, "Use Exchange Level Funding: %v", c.StrategySettings.UseExchangeLevelFunding)
|
||||
log.Infof(log.BackTester, "USD value tracking: %v", !c.StrategySettings.DisableUSDTracking)
|
||||
if c.StrategySettings.UseExchangeLevelFunding && c.StrategySettings.SimultaneousSignalProcessing {
|
||||
log.Info(log.BackTester, "-------------------------------------------------------------")
|
||||
log.Info(log.BackTester, "------------------Funding Settings---------------------------")
|
||||
|
||||
@@ -140,7 +140,7 @@ func TestPrintSettings(t *testing.T) {
|
||||
DatabaseData: &DatabaseData{
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
ConfigOverride: nil,
|
||||
Config: database.Config{},
|
||||
InclusiveEndDate: false,
|
||||
},
|
||||
},
|
||||
@@ -225,6 +225,7 @@ func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) {
|
||||
Name: dca,
|
||||
SimultaneousSignalProcessing: true,
|
||||
UseExchangeLevelFunding: true,
|
||||
DisableUSDTracking: true,
|
||||
ExchangeLevelFunding: []ExchangeLevelFunding{
|
||||
{
|
||||
ExchangeName: testExchange,
|
||||
@@ -514,7 +515,8 @@ func TestGenerateConfigForDCALiveCandles(t *testing.T) {
|
||||
Nickname: "ExampleStrategyDCALiveCandles",
|
||||
Goal: "To demonstrate live trading proof of concept against candle data",
|
||||
StrategySettings: StrategySettings{
|
||||
Name: dca,
|
||||
Name: dca,
|
||||
DisableUSDTracking: true,
|
||||
},
|
||||
CurrencySettings: []CurrencySettings{
|
||||
{
|
||||
@@ -656,7 +658,8 @@ func TestGenerateConfigForDCACSVCandles(t *testing.T) {
|
||||
Nickname: "ExampleStrategyDCACSVCandles",
|
||||
Goal: "To demonstrate the DCA strategy using CSV candle data",
|
||||
StrategySettings: StrategySettings{
|
||||
Name: dca,
|
||||
Name: dca,
|
||||
DisableUSDTracking: true,
|
||||
},
|
||||
CurrencySettings: []CurrencySettings{
|
||||
{
|
||||
@@ -714,7 +717,8 @@ func TestGenerateConfigForDCACSVTrades(t *testing.T) {
|
||||
Nickname: "ExampleStrategyDCACSVTrades",
|
||||
Goal: "To demonstrate the DCA strategy using CSV trade data",
|
||||
StrategySettings: StrategySettings{
|
||||
Name: dca,
|
||||
Name: dca,
|
||||
DisableUSDTracking: true,
|
||||
},
|
||||
CurrencySettings: []CurrencySettings{
|
||||
{
|
||||
@@ -791,7 +795,7 @@ func TestGenerateConfigForDCADatabaseCandles(t *testing.T) {
|
||||
DatabaseData: &DatabaseData{
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
ConfigOverride: &database.Config{
|
||||
Config: database.Config{
|
||||
Enabled: true,
|
||||
Verbose: false,
|
||||
Driver: "sqlite",
|
||||
|
||||
@@ -28,14 +28,13 @@ var (
|
||||
|
||||
// Config defines what is in an individual strategy config
|
||||
type Config struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Goal string `json:"goal"`
|
||||
StrategySettings StrategySettings `json:"strategy-settings"`
|
||||
CurrencySettings []CurrencySettings `json:"currency-settings"`
|
||||
DataSettings DataSettings `json:"data-settings"`
|
||||
PortfolioSettings PortfolioSettings `json:"portfolio-settings"`
|
||||
StatisticSettings StatisticSettings `json:"statistic-settings"`
|
||||
GoCryptoTraderConfigPath string `json:"gocryptotrader-config-path"`
|
||||
Nickname string `json:"nickname"`
|
||||
Goal string `json:"goal"`
|
||||
StrategySettings StrategySettings `json:"strategy-settings"`
|
||||
CurrencySettings []CurrencySettings `json:"currency-settings"`
|
||||
DataSettings DataSettings `json:"data-settings"`
|
||||
PortfolioSettings PortfolioSettings `json:"portfolio-settings"`
|
||||
StatisticSettings StatisticSettings `json:"statistic-settings"`
|
||||
}
|
||||
|
||||
// DataSettings is a container for each type of data retrieval setting.
|
||||
@@ -57,7 +56,10 @@ type StrategySettings struct {
|
||||
SimultaneousSignalProcessing bool `json:"use-simultaneous-signal-processing"`
|
||||
UseExchangeLevelFunding bool `json:"use-exchange-level-funding"`
|
||||
ExchangeLevelFunding []ExchangeLevelFunding `json:"exchange-level-funding,omitempty"`
|
||||
CustomSettings map[string]interface{} `json:"custom-settings,omitempty"`
|
||||
// If true, won't track USD values against currency pair
|
||||
// bool language is opposite to encourage use by default
|
||||
DisableUSDTracking bool `json:"disable-usd-tracking"`
|
||||
CustomSettings map[string]interface{} `json:"custom-settings,omitempty"`
|
||||
}
|
||||
|
||||
// ExchangeLevelFunding allows the portfolio manager to access
|
||||
@@ -114,6 +116,8 @@ type CurrencySettings struct {
|
||||
Asset string `json:"asset"`
|
||||
Base string `json:"base"`
|
||||
Quote string `json:"quote"`
|
||||
// USDTrackingPair is used for price tracking data only
|
||||
USDTrackingPair bool `json:"-"`
|
||||
|
||||
InitialBaseFunds *decimal.Decimal `json:"initial-base-funds,omitempty"`
|
||||
InitialQuoteFunds *decimal.Decimal `json:"initial-quote-funds,omitempty"`
|
||||
@@ -150,10 +154,11 @@ type CSVData struct {
|
||||
|
||||
// DatabaseData defines all fields to configure database based data
|
||||
type DatabaseData struct {
|
||||
StartDate time.Time `json:"start-date"`
|
||||
EndDate time.Time `json:"end-date"`
|
||||
ConfigOverride *database.Config `json:"config-override"`
|
||||
InclusiveEndDate bool `json:"inclusive-end-date"`
|
||||
StartDate time.Time `json:"start-date"`
|
||||
EndDate time.Time `json:"end-date"`
|
||||
Config database.Config `json:"config"`
|
||||
Path string `json:"path"`
|
||||
InclusiveEndDate bool `json:"inclusive-end-date"`
|
||||
}
|
||||
|
||||
// LiveData defines all fields to configure live data
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
|
||||
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
||||
gctconfig "github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/database"
|
||||
dbPSQL "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres"
|
||||
dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3"
|
||||
@@ -64,8 +64,7 @@ func main() {
|
||||
BuySide: config.MinMax{},
|
||||
SellSide: config.MinMax{},
|
||||
},
|
||||
StatisticSettings: config.StatisticSettings{},
|
||||
GoCryptoTraderConfigPath: "",
|
||||
StatisticSettings: config.StatisticSettings{},
|
||||
}
|
||||
fmt.Println("-----Strategy Settings-----")
|
||||
var err error
|
||||
@@ -118,29 +117,11 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("-----GoCryptoTrader config Settings-----")
|
||||
firstRun = true
|
||||
for err != nil || firstRun {
|
||||
firstRun = false
|
||||
fmt.Printf("Enter the path to the GoCryptoTrader config you wish to use. Leave blank to use \"%v\"\n", gctconfig.DefaultFilePath())
|
||||
path := quickParse(reader)
|
||||
if path != "" {
|
||||
cfg.GoCryptoTraderConfigPath = path
|
||||
} else {
|
||||
cfg.GoCryptoTraderConfigPath = gctconfig.DefaultFilePath()
|
||||
}
|
||||
_, err = os.Stat(cfg.GoCryptoTraderConfigPath)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
var resp []byte
|
||||
resp, err = json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("Write strategy config to file? If no, the output will be on screen y/n")
|
||||
yn := quickParse(reader)
|
||||
if yn == y || yn == yes {
|
||||
@@ -274,8 +255,11 @@ func parseStrategySettings(cfg *config.Config, reader *bufio.Reader) error {
|
||||
if strings.Contains(customSettings, y) {
|
||||
cfg.StrategySettings.CustomSettings = customSettingsLoop(reader)
|
||||
}
|
||||
fmt.Println("Will this strategy use simultaneous processing? y/n")
|
||||
fmt.Println("Do you wish to have strategy performance tracked against USD? y/n")
|
||||
yn := quickParse(reader)
|
||||
cfg.StrategySettings.DisableUSDTracking = !strings.Contains(yn, y)
|
||||
fmt.Println("Will this strategy use simultaneous processing? y/n")
|
||||
yn = quickParse(reader)
|
||||
cfg.StrategySettings.SimultaneousSignalProcessing = strings.Contains(yn, y)
|
||||
if !cfg.StrategySettings.SimultaneousSignalProcessing {
|
||||
return nil
|
||||
@@ -410,64 +394,61 @@ func parseDatabase(reader *bufio.Reader, cfg *config.Config) error {
|
||||
fmt.Println("Is the end date inclusive? y/n")
|
||||
input = quickParse(reader)
|
||||
cfg.DataSettings.DatabaseData.InclusiveEndDate = input == y || input == yes
|
||||
|
||||
fmt.Println("Do you wish to override GoCryptoTrader's database config? y/n")
|
||||
cfg.DataSettings.DatabaseData.Config = database.Config{
|
||||
Enabled: true,
|
||||
}
|
||||
fmt.Println("Do you want database verbose output? y/n")
|
||||
input = quickParse(reader)
|
||||
if input == y || input == yes {
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride = &database.Config{
|
||||
Enabled: true,
|
||||
}
|
||||
fmt.Println("Do you want database verbose output? y/n")
|
||||
input = quickParse(reader)
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Verbose = input == y || input == yes
|
||||
cfg.DataSettings.DatabaseData.Config.Verbose = input == y || input == yes
|
||||
|
||||
fmt.Printf("What database driver to use? %v %v or %v\n", database.DBPostgreSQL, database.DBSQLite, database.DBSQLite3)
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Driver = quickParse(reader)
|
||||
fmt.Printf("What database driver to use? %v %v or %v\n", database.DBPostgreSQL, database.DBSQLite, database.DBSQLite3)
|
||||
cfg.DataSettings.DatabaseData.Config.Driver = quickParse(reader)
|
||||
if cfg.DataSettings.DatabaseData.Config.Driver == database.DBSQLite || cfg.DataSettings.DatabaseData.Config.Driver == database.DBSQLite3 {
|
||||
fmt.Printf("What is the path to the database directory? Leaving blank will use: '%v'", filepath.Join(gctcommon.GetDefaultDataDir(runtime.GOOS), "database"))
|
||||
cfg.DataSettings.DatabaseData.Path = quickParse(reader)
|
||||
}
|
||||
fmt.Println("What is the database host?")
|
||||
cfg.DataSettings.DatabaseData.Config.Host = quickParse(reader)
|
||||
|
||||
fmt.Println("What is the database host?")
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Host = quickParse(reader)
|
||||
fmt.Println("What is the database username?")
|
||||
cfg.DataSettings.DatabaseData.Config.Username = quickParse(reader)
|
||||
|
||||
fmt.Println("What is the database username?")
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Username = quickParse(reader)
|
||||
fmt.Println("What is the database password? eg 1234")
|
||||
cfg.DataSettings.DatabaseData.Config.Password = quickParse(reader)
|
||||
|
||||
fmt.Println("What is the database password? eg 1234")
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Password = quickParse(reader)
|
||||
fmt.Println("What is the database? eg database.db")
|
||||
cfg.DataSettings.DatabaseData.Config.Database = quickParse(reader)
|
||||
|
||||
fmt.Println("What is the database? eg database.db")
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Database = quickParse(reader)
|
||||
|
||||
if cfg.DataSettings.DatabaseData.ConfigOverride.Driver == database.DBPostgreSQL {
|
||||
fmt.Println("What is the database SSLMode? eg disable")
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.SSLMode = quickParse(reader)
|
||||
}
|
||||
fmt.Println("What is the database Port? eg 1337")
|
||||
input = quickParse(reader)
|
||||
var port float64
|
||||
if input != "" {
|
||||
port, err = strconv.ParseFloat(input, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Port = uint16(port)
|
||||
err = database.DB.SetConfig(cfg.DataSettings.DatabaseData.ConfigOverride)
|
||||
if cfg.DataSettings.DatabaseData.Config.Driver == database.DBPostgreSQL {
|
||||
fmt.Println("What is the database SSLMode? eg disable")
|
||||
cfg.DataSettings.DatabaseData.Config.SSLMode = quickParse(reader)
|
||||
}
|
||||
fmt.Println("What is the database Port? eg 1337")
|
||||
input = quickParse(reader)
|
||||
var port float64
|
||||
if input != "" {
|
||||
port, err = strconv.ParseFloat(input, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database failed to set config: %w", err)
|
||||
}
|
||||
if cfg.DataSettings.DatabaseData.ConfigOverride.Driver == database.DBPostgreSQL {
|
||||
_, err = dbPSQL.Connect(cfg.DataSettings.DatabaseData.ConfigOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database failed to connect: %v", err)
|
||||
}
|
||||
} else if cfg.DataSettings.DatabaseData.ConfigOverride.Driver == database.DBSQLite ||
|
||||
cfg.DataSettings.DatabaseData.ConfigOverride.Driver == database.DBSQLite3 {
|
||||
_, err = dbsqlite3.Connect(cfg.DataSettings.DatabaseData.ConfigOverride.Database)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database failed to connect: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
cfg.DataSettings.DatabaseData.Config.Port = uint16(port)
|
||||
err = database.DB.SetConfig(&cfg.DataSettings.DatabaseData.Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database failed to set config: %w", err)
|
||||
}
|
||||
if cfg.DataSettings.DatabaseData.Config.Driver == database.DBPostgreSQL {
|
||||
_, err = dbPSQL.Connect(&cfg.DataSettings.DatabaseData.Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database failed to connect: %v", err)
|
||||
}
|
||||
} else if cfg.DataSettings.DatabaseData.Config.Driver == database.DBSQLite ||
|
||||
cfg.DataSettings.DatabaseData.Config.Driver == database.DBSQLite3 {
|
||||
_, err = dbsqlite3.Connect(cfg.DataSettings.DatabaseData.Config.Database)
|
||||
if err != nil {
|
||||
return fmt.Errorf("database failed to connect: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"initial-funds": "100000",
|
||||
"transfer-fee": "0"
|
||||
}
|
||||
]
|
||||
],
|
||||
"disable-usd-tracking": true
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -101,6 +102,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": false
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -94,6 +95,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": true,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": false
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -94,6 +95,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": false
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -65,6 +66,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": false
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -65,6 +66,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": true
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -68,6 +69,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": true
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -63,6 +64,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": true
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -63,6 +64,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"strategy-settings": {
|
||||
"name": "dollarcostaverage",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": false
|
||||
},
|
||||
"currency-settings": [
|
||||
{
|
||||
@@ -43,7 +44,7 @@
|
||||
"database-data": {
|
||||
"start-date": "2020-08-01T00:00:00+10:00",
|
||||
"end-date": "2020-12-01T00:00:00+11:00",
|
||||
"config-override": {
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"verbose": false,
|
||||
"driver": "sqlite",
|
||||
@@ -56,6 +57,7 @@
|
||||
"sslmode": ""
|
||||
}
|
||||
},
|
||||
"path": "",
|
||||
"inclusive-end-date": false
|
||||
}
|
||||
},
|
||||
@@ -78,6 +80,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"name": "rsi",
|
||||
"use-simultaneous-signal-processing": false,
|
||||
"use-exchange-level-funding": false,
|
||||
"disable-usd-tracking": false,
|
||||
"custom-settings": {
|
||||
"rsi-high": 70,
|
||||
"rsi-low": 30,
|
||||
@@ -100,6 +101,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
"transfer-fee": "0"
|
||||
}
|
||||
],
|
||||
"disable-usd-tracking": false,
|
||||
"custom-settings": {
|
||||
"mfi-high": 68,
|
||||
"mfi-low": 32,
|
||||
@@ -225,6 +226,5 @@
|
||||
},
|
||||
"statistic-settings": {
|
||||
"risk-free-rate": "0.03"
|
||||
},
|
||||
"gocryptotrader-config-path": ""
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package csv
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -19,8 +20,10 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
var errNoUSDData = errors.New("could not retrieve USD CSV candle data")
|
||||
|
||||
// LoadData is a basic csv reader which converts the found CSV file into a kline item
|
||||
func LoadData(dataType int64, filepath, exchangeName string, interval time.Duration, fPair currency.Pair, a asset.Item) (*gctkline.DataFromKline, error) {
|
||||
func LoadData(dataType int64, filepath, exchangeName string, interval time.Duration, fPair currency.Pair, a asset.Item, isUSDTrackingPair bool) (*gctkline.DataFromKline, error) {
|
||||
resp := &gctkline.DataFromKline{}
|
||||
csvFile, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
@@ -100,7 +103,6 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read csv candle data for %v %v %v, %v", exchangeName, a, fPair, err)
|
||||
}
|
||||
|
||||
resp.Item = candles
|
||||
case common.DataTrade:
|
||||
var trades []trade.Data
|
||||
@@ -149,6 +151,9 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat
|
||||
return nil, fmt.Errorf("could not read csv trade data for %v %v %v, %v", exchangeName, a, fPair, err)
|
||||
}
|
||||
default:
|
||||
if isUSDTrackingPair {
|
||||
return nil, fmt.Errorf("%w for %v %v %v. Please add USD pair data to your CSV or set `disable-usd-tracking` to `true` in your config. %v", errNoUSDData, exchangeName, a, fPair, err)
|
||||
}
|
||||
return nil, fmt.Errorf("could not process csv data for %v %v %v, %w", exchangeName, a, fPair, common.ErrInvalidDataType)
|
||||
}
|
||||
resp.Item.Exchange = strings.ToLower(exchangeName)
|
||||
|
||||
@@ -23,7 +23,8 @@ func TestLoadDataCandles(t *testing.T) {
|
||||
exch,
|
||||
gctkline.FifteenMin.Duration(),
|
||||
p,
|
||||
a)
|
||||
a,
|
||||
false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -39,7 +40,8 @@ func TestLoadDataTrades(t *testing.T) {
|
||||
exch,
|
||||
gctkline.FifteenMin.Duration(),
|
||||
p,
|
||||
a)
|
||||
a,
|
||||
false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -55,8 +57,21 @@ func TestLoadDataInvalid(t *testing.T) {
|
||||
exch,
|
||||
gctkline.FifteenMin.Duration(),
|
||||
p,
|
||||
a)
|
||||
a,
|
||||
false)
|
||||
if !errors.Is(err, common.ErrInvalidDataType) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrInvalidDataType)
|
||||
}
|
||||
|
||||
_, err = LoadData(
|
||||
-1,
|
||||
filepath.Join("..", "..", "..", "..", "testdata", "binance_BTCUSDT_24h-trades_2020_11_16.csv"),
|
||||
exch,
|
||||
gctkline.FifteenMin.Duration(),
|
||||
p,
|
||||
a,
|
||||
true)
|
||||
if !errors.Is(err, errNoUSDData) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoUSDData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,8 +15,10 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
var errNoUSDData = errors.New("could not retrieve USD database candle data")
|
||||
|
||||
// LoadData retrieves data from an existing database using GoCryptoTrader's database handling implementation
|
||||
func LoadData(startDate, endDate time.Time, interval time.Duration, exchangeName string, dataType int64, fPair currency.Pair, a asset.Item) (*kline.DataFromKline, error) {
|
||||
func LoadData(startDate, endDate time.Time, interval time.Duration, exchangeName string, dataType int64, fPair currency.Pair, a asset.Item, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
|
||||
resp := &kline.DataFromKline{}
|
||||
switch dataType {
|
||||
case common.DataCandle:
|
||||
@@ -27,6 +30,9 @@ func LoadData(startDate, endDate time.Time, interval time.Duration, exchangeName
|
||||
fPair,
|
||||
a)
|
||||
if err != nil {
|
||||
if isUSDTrackingPair {
|
||||
return nil, fmt.Errorf("%w for %v %v %v. Please save USD candle pair data to the database or set `disable-usd-tracking` to `true` in your config. %v", errNoUSDData, exchangeName, a, fPair, err)
|
||||
}
|
||||
return nil, fmt.Errorf("could not retrieve database candle data for %v %v %v, %v", exchangeName, a, fPair, err)
|
||||
}
|
||||
resp.Item = klineItem
|
||||
@@ -50,10 +56,16 @@ func LoadData(startDate, endDate time.Time, interval time.Duration, exchangeName
|
||||
gctkline.Interval(interval),
|
||||
trades...)
|
||||
if err != nil {
|
||||
if isUSDTrackingPair {
|
||||
return nil, fmt.Errorf("%w for %v %v %v. Please save USD pair trade data to the database or set `disable-usd-tracking` to `true` in your config. %v", errNoUSDData, exchangeName, a, fPair, err)
|
||||
}
|
||||
return nil, fmt.Errorf("could not retrieve database trade data for %v %v %v, %v", exchangeName, a, fPair, err)
|
||||
}
|
||||
resp.Item = klineItem
|
||||
default:
|
||||
if isUSDTrackingPair {
|
||||
return nil, fmt.Errorf("%w for %v %v %v. Please add USD pair data to your CSV or set `disable-usd-tracking` to `true` in your config", errNoUSDData, exchangeName, a, fPair)
|
||||
}
|
||||
return nil, fmt.Errorf("could not retrieve database data for %v %v %v, %w", exchangeName, a, fPair, common.ErrInvalidDataType)
|
||||
}
|
||||
resp.Item.Exchange = strings.ToLower(resp.Item.Exchange)
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestLoadDataCandles(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, common.DataCandle, p, a)
|
||||
_, err = LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, common.DataCandle, p, a, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func TestLoadDataTrades(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, common.DataTrade, p, a)
|
||||
_, err = LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, common.DataTrade, p, a, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -210,8 +210,13 @@ func TestLoadDataInvalid(t *testing.T) {
|
||||
p := currency.NewPair(currency.BTC, currency.USDT)
|
||||
dStart := time.Date(2020, 1, 0, 0, 0, 0, 0, time.UTC)
|
||||
dEnd := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
_, err := LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, -1, p, a)
|
||||
_, err := LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, -1, p, a, false)
|
||||
if !errors.Is(err, common.ErrInvalidDataType) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrInvalidDataType)
|
||||
}
|
||||
|
||||
_, err = LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, -1, p, a, true)
|
||||
if !errors.Is(err, errNoUSDData) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoUSDData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ func (d *DataFromKline) Load() error {
|
||||
}
|
||||
d.addedTimes[d.Item.Candles[i].Time] = true
|
||||
}
|
||||
|
||||
d.SetStream(klineData)
|
||||
d.SortStream()
|
||||
return nil
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange/slippage"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
|
||||
@@ -28,7 +27,7 @@ func (e *Exchange) Reset() {
|
||||
|
||||
// ExecuteOrder assesses the portfolio manager's order event and if it passes validation
|
||||
// will send an order to the exchange/fake order manager to be stored and raise a fill event
|
||||
func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, bot *engine.Engine, funds funding.IPairReleaser) (*fill.Fill, error) {
|
||||
func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *engine.OrderManager, funds funding.IPairReleaser) (*fill.Fill, error) {
|
||||
f := &fill.Fill{
|
||||
Base: event.Base{
|
||||
Offset: o.GetOffset(),
|
||||
@@ -111,7 +110,7 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, bot *engine.En
|
||||
}
|
||||
f.ExchangeFee = calculateExchangeFee(adjustedPrice, limitReducedAmount, cs.ExchangeFee)
|
||||
|
||||
orderID, err := e.placeOrder(context.TODO(), adjustedPrice, limitReducedAmount, cs.UseRealOrders, cs.CanUseExchangeLimits, f, bot)
|
||||
orderID, err := e.placeOrder(context.TODO(), adjustedPrice, limitReducedAmount, cs.UseRealOrders, cs.CanUseExchangeLimits, f, orderManager)
|
||||
if err != nil {
|
||||
fundErr := funds.Release(eventFunds, eventFunds, f.GetDirection())
|
||||
if fundErr != nil {
|
||||
@@ -139,7 +138,7 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, bot *engine.En
|
||||
funds.IncreaseAvailable(limitReducedAmount.Mul(adjustedPrice), f.GetDirection())
|
||||
}
|
||||
|
||||
ords, _ := bot.OrderManager.GetOrdersSnapshot("")
|
||||
ords := orderManager.GetOrdersSnapshot("")
|
||||
for i := range ords {
|
||||
if ords[i].ID != orderID {
|
||||
continue
|
||||
@@ -168,7 +167,7 @@ func verifyOrderWithinLimits(f *fill.Fill, limitReducedAmount decimal.Decimal, c
|
||||
return errNilCurrencySettings
|
||||
}
|
||||
isBeyondLimit := false
|
||||
var minMax config.MinMax
|
||||
var minMax MinMax
|
||||
var direction gctorder.Side
|
||||
switch f.GetDirection() {
|
||||
case gctorder.Buy:
|
||||
@@ -221,7 +220,7 @@ func reduceAmountToFitPortfolioLimit(adjustedPrice, amount, sizedPortfolioTotal
|
||||
return amount
|
||||
}
|
||||
|
||||
func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal, useRealOrders, useExchangeLimits bool, f *fill.Fill, bot *engine.Engine) (string, error) {
|
||||
func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal, useRealOrders, useExchangeLimits bool, f *fill.Fill, orderManager *engine.OrderManager) (string, error) {
|
||||
if f == nil {
|
||||
return "", common.ErrNilEvent
|
||||
}
|
||||
@@ -248,7 +247,7 @@ func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal
|
||||
}
|
||||
|
||||
if useRealOrders {
|
||||
resp, err := bot.OrderManager.Submit(ctx, o)
|
||||
resp, err := orderManager.Submit(ctx, o)
|
||||
if resp != nil {
|
||||
orderID = resp.OrderID
|
||||
}
|
||||
@@ -265,7 +264,7 @@ func (e *Exchange) placeOrder(ctx context.Context, price, amount decimal.Decimal
|
||||
Cost: p,
|
||||
FullyMatched: true,
|
||||
}
|
||||
resp, err := bot.OrderManager.SubmitFakeOrder(o, submitResponse, useExchangeLimits)
|
||||
resp, err := orderManager.SubmitFakeOrder(o, submitResponse, useExchangeLimits)
|
||||
if resp != nil {
|
||||
orderID = resp.OrderID
|
||||
}
|
||||
@@ -314,16 +313,16 @@ func applySlippageToPrice(direction gctorder.Side, price, slippageRate decimal.D
|
||||
|
||||
// SetExchangeAssetCurrencySettings sets the settings for an exchange, asset, currency
|
||||
func (e *Exchange) SetExchangeAssetCurrencySettings(exch string, a asset.Item, cp currency.Pair, c *Settings) {
|
||||
if c.ExchangeName == "" ||
|
||||
c.AssetType == "" ||
|
||||
c.CurrencyPair.IsEmpty() {
|
||||
if c.Exchange == "" ||
|
||||
c.Asset == "" ||
|
||||
c.Pair.IsEmpty() {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range e.CurrencySettings {
|
||||
if e.CurrencySettings[i].CurrencyPair == cp &&
|
||||
e.CurrencySettings[i].AssetType == a &&
|
||||
exch == e.CurrencySettings[i].ExchangeName {
|
||||
if e.CurrencySettings[i].Pair == cp &&
|
||||
e.CurrencySettings[i].Asset == a &&
|
||||
exch == e.CurrencySettings[i].Exchange {
|
||||
e.CurrencySettings[i] = *c
|
||||
return
|
||||
}
|
||||
@@ -334,9 +333,9 @@ func (e *Exchange) SetExchangeAssetCurrencySettings(exch string, a asset.Item, c
|
||||
// GetCurrencySettings returns the settings for an exchange, asset currency
|
||||
func (e *Exchange) GetCurrencySettings(exch string, a asset.Item, cp currency.Pair) (Settings, error) {
|
||||
for i := range e.CurrencySettings {
|
||||
if e.CurrencySettings[i].CurrencyPair.Equal(cp) {
|
||||
if e.CurrencySettings[i].AssetType == a {
|
||||
if exch == e.CurrencySettings[i].ExchangeName {
|
||||
if e.CurrencySettings[i].Pair.Equal(cp) {
|
||||
if e.CurrencySettings[i].Asset == a {
|
||||
if exch == e.CurrencySettings[i].Exchange {
|
||||
return e.CurrencySettings[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
@@ -50,16 +49,16 @@ func TestSetCurrency(t *testing.T) {
|
||||
t.Error("expected 0")
|
||||
}
|
||||
cs := &Settings{
|
||||
ExchangeName: testExchange,
|
||||
Exchange: testExchange,
|
||||
UseRealOrders: true,
|
||||
CurrencyPair: currency.NewPair(currency.BTC, currency.USDT),
|
||||
AssetType: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USDT),
|
||||
Asset: asset.Spot,
|
||||
ExchangeFee: decimal.Zero,
|
||||
MakerFee: decimal.Zero,
|
||||
TakerFee: decimal.Zero,
|
||||
BuySide: config.MinMax{},
|
||||
SellSide: config.MinMax{},
|
||||
Leverage: config.Leverage{},
|
||||
BuySide: MinMax{},
|
||||
SellSide: MinMax{},
|
||||
Leverage: Leverage{},
|
||||
MinimumSlippageRate: decimal.Zero,
|
||||
MaximumSlippageRate: decimal.Zero,
|
||||
}
|
||||
@@ -171,25 +170,25 @@ func TestPlaceOrder(t *testing.T) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
|
||||
}
|
||||
f := &fill.Fill{}
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot)
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
|
||||
if err != nil && err.Error() != "order exchange name must be specified" {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
f.Exchange = testExchange
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot)
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
|
||||
if !errors.Is(err, gctorder.ErrPairIsEmpty) {
|
||||
t.Errorf("received: %v, expected: %v", err, gctorder.ErrPairIsEmpty)
|
||||
}
|
||||
f.CurrencyPair = currency.NewPair(currency.BTC, currency.USDT)
|
||||
f.AssetType = asset.Spot
|
||||
f.Direction = gctorder.Buy
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot)
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), true, true, f, bot)
|
||||
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), true, true, f, bot.OrderManager)
|
||||
if err != nil && !strings.Contains(err.Error(), "unset/default API keys") {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -232,16 +231,16 @@ func TestExecuteOrder(t *testing.T) {
|
||||
}
|
||||
|
||||
cs := Settings{
|
||||
ExchangeName: testExchange,
|
||||
Exchange: testExchange,
|
||||
UseRealOrders: false,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
Pair: p,
|
||||
Asset: a,
|
||||
ExchangeFee: decimal.NewFromFloat(0.01),
|
||||
MakerFee: decimal.NewFromFloat(0.01),
|
||||
TakerFee: decimal.NewFromFloat(0.01),
|
||||
BuySide: config.MinMax{},
|
||||
SellSide: config.MinMax{},
|
||||
Leverage: config.Leverage{},
|
||||
BuySide: MinMax{},
|
||||
SellSide: MinMax{},
|
||||
Leverage: Leverage{},
|
||||
MinimumSlippageRate: decimal.Zero,
|
||||
MaximumSlippageRate: decimal.NewFromInt(1),
|
||||
}
|
||||
@@ -284,7 +283,7 @@ func TestExecuteOrder(t *testing.T) {
|
||||
}
|
||||
d.Next()
|
||||
|
||||
_, err = e.ExecuteOrder(o, d, bot, &fakeFund{})
|
||||
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -293,7 +292,7 @@ func TestExecuteOrder(t *testing.T) {
|
||||
cs.CanUseExchangeLimits = true
|
||||
o.Direction = gctorder.Sell
|
||||
e.CurrencySettings = []Settings{cs}
|
||||
_, err = e.ExecuteOrder(o, d, bot, &fakeFund{})
|
||||
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
|
||||
if err != nil && !strings.Contains(err.Error(), "unset/default API keys") {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -346,22 +345,22 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
|
||||
}
|
||||
|
||||
cs := Settings{
|
||||
ExchangeName: testExchange,
|
||||
Exchange: testExchange,
|
||||
UseRealOrders: false,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
Pair: p,
|
||||
Asset: a,
|
||||
ExchangeFee: decimal.NewFromFloat(0.01),
|
||||
MakerFee: decimal.NewFromFloat(0.01),
|
||||
TakerFee: decimal.NewFromFloat(0.01),
|
||||
BuySide: config.MinMax{
|
||||
BuySide: MinMax{
|
||||
MaximumSize: decimal.NewFromFloat(0.01),
|
||||
MinimumSize: decimal.Zero,
|
||||
},
|
||||
SellSide: config.MinMax{
|
||||
SellSide: MinMax{
|
||||
MaximumSize: decimal.NewFromFloat(0.1),
|
||||
MinimumSize: decimal.Zero,
|
||||
},
|
||||
Leverage: config.Leverage{},
|
||||
Leverage: Leverage{},
|
||||
MinimumSlippageRate: decimal.Zero,
|
||||
MaximumSlippageRate: decimal.NewFromInt(1),
|
||||
Limits: limits,
|
||||
@@ -404,7 +403,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
d.Next()
|
||||
_, err = e.ExecuteOrder(o, d, bot, &fakeFund{})
|
||||
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
|
||||
if !errors.Is(err, errExceededPortfolioLimit) {
|
||||
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
|
||||
}
|
||||
@@ -417,7 +416,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
|
||||
cs.BuySide.MaximumSize = decimal.Zero
|
||||
cs.BuySide.MinimumSize = decimal.NewFromFloat(0.01)
|
||||
e.CurrencySettings = []Settings{cs}
|
||||
_, err = e.ExecuteOrder(o, d, bot, &fakeFund{})
|
||||
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
|
||||
if err != nil && !strings.Contains(err.Error(), "exceed minimum size") {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -433,7 +432,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
|
||||
cs.SellSide.MaximumSize = decimal.Zero
|
||||
cs.SellSide.MinimumSize = decimal.NewFromFloat(0.01)
|
||||
e.CurrencySettings = []Settings{cs}
|
||||
_, err = e.ExecuteOrder(o, d, bot, &fakeFund{})
|
||||
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
|
||||
if err != nil && !strings.Contains(err.Error(), "exceed minimum size") {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -450,7 +449,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
|
||||
cs.SellSide.MaximumSize = decimal.Zero
|
||||
cs.SellSide.MinimumSize = decimal.NewFromInt(1)
|
||||
e.CurrencySettings = []Settings{cs}
|
||||
_, err = e.ExecuteOrder(o, d, bot, &fakeFund{})
|
||||
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
|
||||
if !errors.Is(err, errExceededPortfolioLimit) {
|
||||
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
|
||||
}
|
||||
@@ -468,7 +467,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
|
||||
cs.CanUseExchangeLimits = true
|
||||
o.Direction = gctorder.Sell
|
||||
e.CurrencySettings = []Settings{cs}
|
||||
_, err = e.ExecuteOrder(o, d, bot, &fakeFund{})
|
||||
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
|
||||
if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) {
|
||||
t.Errorf("received %v expected %v", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
|
||||
}
|
||||
@@ -531,7 +530,7 @@ func TestVerifyOrderWithinLimits(t *testing.T) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
s := &Settings{
|
||||
BuySide: config.MinMax{
|
||||
BuySide: MinMax{
|
||||
MinimumSize: decimal.NewFromInt(1),
|
||||
MaximumSize: decimal.NewFromInt(1),
|
||||
},
|
||||
@@ -547,7 +546,7 @@ func TestVerifyOrderWithinLimits(t *testing.T) {
|
||||
}
|
||||
|
||||
f.Direction = gctorder.Sell
|
||||
s.SellSide = config.MinMax{
|
||||
s.SellSide = MinMax{
|
||||
MinimumSize: decimal.NewFromInt(1),
|
||||
MaximumSize: decimal.NewFromInt(1),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
@@ -26,7 +25,7 @@ var (
|
||||
type ExecutionHandler interface {
|
||||
SetExchangeAssetCurrencySettings(string, asset.Item, currency.Pair, *Settings)
|
||||
GetCurrencySettings(string, asset.Item, currency.Pair) (Settings, error)
|
||||
ExecuteOrder(order.Event, data.Handler, *engine.Engine, funding.IPairReleaser) (*fill.Fill, error)
|
||||
ExecuteOrder(order.Event, data.Handler, *engine.OrderManager, funding.IPairReleaser) (*fill.Fill, error)
|
||||
Reset()
|
||||
}
|
||||
|
||||
@@ -37,20 +36,20 @@ type Exchange struct {
|
||||
|
||||
// Settings allow the eventhandler to size an order within the limitations set by the config file
|
||||
type Settings struct {
|
||||
ExchangeName string
|
||||
Exchange string
|
||||
UseRealOrders bool
|
||||
|
||||
CurrencyPair currency.Pair
|
||||
AssetType asset.Item
|
||||
Pair currency.Pair
|
||||
Asset asset.Item
|
||||
|
||||
ExchangeFee decimal.Decimal
|
||||
MakerFee decimal.Decimal
|
||||
TakerFee decimal.Decimal
|
||||
|
||||
BuySide config.MinMax
|
||||
SellSide config.MinMax
|
||||
BuySide MinMax
|
||||
SellSide MinMax
|
||||
|
||||
Leverage config.Leverage
|
||||
Leverage Leverage
|
||||
|
||||
MinimumSlippageRate decimal.Decimal
|
||||
MaximumSlippageRate decimal.Decimal
|
||||
@@ -59,3 +58,18 @@ type Settings struct {
|
||||
CanUseExchangeLimits bool
|
||||
SkipCandleVolumeFitting bool
|
||||
}
|
||||
|
||||
// MinMax are the rules which limit the placement of orders.
|
||||
type MinMax struct {
|
||||
MinimumSize decimal.Decimal
|
||||
MaximumSize decimal.Decimal
|
||||
MaximumTotal decimal.Decimal
|
||||
}
|
||||
|
||||
// Leverage rules are used to allow or limit the use of leverage in orders
|
||||
// when supported
|
||||
type Leverage struct {
|
||||
CanUseLeverage bool
|
||||
MaximumOrdersWithLeverageRatio decimal.Decimal
|
||||
MaximumLeverageRate decimal.Decimal
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// Create makes a Holding struct to track total values of strategy holdings over the course of a backtesting run
|
||||
func Create(ev common.EventHandler, funding funding.IPairReader, riskFreeRate decimal.Decimal) (Holding, error) {
|
||||
func Create(ev common.EventHandler, funding funding.IPairReader) (Holding, error) {
|
||||
if ev == nil {
|
||||
return Holding{}, common.ErrNilEvent
|
||||
}
|
||||
@@ -26,7 +26,6 @@ func Create(ev common.EventHandler, funding funding.IPairReader, riskFreeRate de
|
||||
QuoteSize: funding.QuoteInitialFunds(),
|
||||
BaseInitialFunds: funding.BaseInitialFunds(),
|
||||
BaseSize: funding.BaseInitialFunds(),
|
||||
RiskFreeRate: riskFreeRate,
|
||||
TotalInitialValue: funding.BaseInitialFunds().Mul(funding.QuoteInitialFunds()).Add(funding.QuoteInitialFunds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@ func pair(t *testing.T) *funding.Pair {
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := Create(nil, pair(t), riskFreeRate)
|
||||
_, err := Create(nil, pair(t))
|
||||
if !errors.Is(err, common.ErrNilEvent) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
|
||||
}
|
||||
_, err = Create(&fill.Fill{}, pair(t), riskFreeRate)
|
||||
_, err = Create(&fill.Fill{}, pair(t))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func TestCreate(t *testing.T) {
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
h, err := Create(&fill.Fill{}, pair(t), riskFreeRate)
|
||||
h, err := Create(&fill.Fill{}, pair(t))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
func TestUpdateValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
h, err := Create(&fill.Fill{}, pair(t), riskFreeRate)
|
||||
h, err := Create(&fill.Fill{}, pair(t))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func TestUpdateBuyStats(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := Create(&fill.Fill{}, p, riskFreeRate)
|
||||
h, err := Create(&fill.Fill{}, p)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func TestUpdateSellStats(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h, err := Create(&fill.Fill{}, p, riskFreeRate)
|
||||
h, err := Create(&fill.Fill{}, p)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,4 @@ type Holding struct {
|
||||
TotalValueLostToVolumeSizing decimal.Decimal `json:"total-value-lost-to-volume-sizing"`
|
||||
TotalValueLostToSlippage decimal.Decimal `json:"total-value-lost-to-slippage"`
|
||||
TotalValueLost decimal.Decimal `json:"total-value-lost"`
|
||||
|
||||
RiskFreeRate decimal.Decimal `json:"risk-free-rate"`
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package portfolio
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
@@ -10,7 +11,6 @@ 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/eventhandlers/portfolio/risk"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/settings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
@@ -46,6 +46,31 @@ func (p *Portfolio) Reset() {
|
||||
p.exchangeAssetPairSettings = nil
|
||||
}
|
||||
|
||||
// GetLatestOrderSnapshotForEvent gets orders related to the event
|
||||
func (p *Portfolio) GetLatestOrderSnapshotForEvent(e common.EventHandler) (compliance.Snapshot, error) {
|
||||
eapSettings, ok := p.exchangeAssetPairSettings[e.GetExchange()][e.GetAssetType()][e.Pair()]
|
||||
if !ok {
|
||||
return compliance.Snapshot{}, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, e.GetExchange(), e.GetAssetType(), e.Pair())
|
||||
}
|
||||
return eapSettings.ComplianceManager.GetLatestSnapshot(), nil
|
||||
}
|
||||
|
||||
// GetLatestOrderSnapshots returns the latest snapshots from all stored pair data
|
||||
func (p *Portfolio) GetLatestOrderSnapshots() ([]compliance.Snapshot, error) {
|
||||
var resp []compliance.Snapshot
|
||||
for _, exchangeMap := range p.exchangeAssetPairSettings {
|
||||
for _, assetMap := range exchangeMap {
|
||||
for _, pairMap := range assetMap {
|
||||
resp = append(resp, pairMap.ComplianceManager.GetLatestSnapshot())
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(resp) == 0 {
|
||||
return nil, errNoPortfolioSettings
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// OnSignal receives the event from the strategy on whether it has signalled to buy, do nothing or sell
|
||||
// on buy/sell, the portfolio manager will size the order and assess the risk of the order
|
||||
// if successful, it will pass on an order.Order to be used by the exchange event handler to place an order based on
|
||||
@@ -209,7 +234,7 @@ func (p *Portfolio) OnFill(ev fill.Event, funding funding.IPairReader) (*fill.Fi
|
||||
} else {
|
||||
h = lookup.GetLatestHoldings()
|
||||
if h.Timestamp.IsZero() {
|
||||
h, err = holdings.Create(ev, funding, p.riskFreeRate)
|
||||
h, err = holdings.Create(ev, funding)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -324,7 +349,7 @@ func (p *Portfolio) UpdateHoldings(ev common.DataEventHandler, funds funding.IPa
|
||||
h := lookup.GetLatestHoldings()
|
||||
if h.Timestamp.IsZero() {
|
||||
var err error
|
||||
h, err = holdings.Create(ev, funds, p.riskFreeRate)
|
||||
h, err = holdings.Create(ev, funds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -358,14 +383,11 @@ func (p *Portfolio) setHoldingsForOffset(h *holdings.Holding, overwriteExisting
|
||||
if h.Timestamp.IsZero() {
|
||||
return errHoldingsNoTimestamp
|
||||
}
|
||||
lookup := p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair]
|
||||
if lookup == nil {
|
||||
var err error
|
||||
lookup, err = p.SetupCurrencySettingsMap(h.Exchange, h.Asset, h.Pair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lookup, ok := p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, h.Exchange, h.Asset, h.Pair)
|
||||
}
|
||||
|
||||
if overwriteExisting && len(lookup.HoldingsSnapshots) == 0 {
|
||||
return errNoHoldings
|
||||
}
|
||||
@@ -404,28 +426,54 @@ func (p *Portfolio) ViewHoldingAtTimePeriod(ev common.EventHandler) (*holdings.H
|
||||
}
|
||||
|
||||
// SetupCurrencySettingsMap ensures a map is created and no panics happen
|
||||
func (p *Portfolio) SetupCurrencySettingsMap(exch string, a asset.Item, cp currency.Pair) (*settings.Settings, error) {
|
||||
if exch == "" {
|
||||
func (p *Portfolio) SetupCurrencySettingsMap(settings *exchange.Settings) (*Settings, error) {
|
||||
if settings == nil {
|
||||
return nil, errNoPortfolioSettings
|
||||
}
|
||||
if settings.Exchange == "" {
|
||||
return nil, errExchangeUnset
|
||||
}
|
||||
if a == "" {
|
||||
if settings.Asset == "" {
|
||||
return nil, errAssetUnset
|
||||
}
|
||||
if cp.IsEmpty() {
|
||||
if settings.Pair.IsEmpty() {
|
||||
return nil, errCurrencyPairUnset
|
||||
}
|
||||
if p.exchangeAssetPairSettings == nil {
|
||||
p.exchangeAssetPairSettings = make(map[string]map[asset.Item]map[currency.Pair]*settings.Settings)
|
||||
p.exchangeAssetPairSettings = make(map[string]map[asset.Item]map[currency.Pair]*Settings)
|
||||
}
|
||||
if p.exchangeAssetPairSettings[exch] == nil {
|
||||
p.exchangeAssetPairSettings[exch] = make(map[asset.Item]map[currency.Pair]*settings.Settings)
|
||||
if p.exchangeAssetPairSettings[settings.Exchange] == nil {
|
||||
p.exchangeAssetPairSettings[settings.Exchange] = make(map[asset.Item]map[currency.Pair]*Settings)
|
||||
}
|
||||
if p.exchangeAssetPairSettings[exch][a] == nil {
|
||||
p.exchangeAssetPairSettings[exch][a] = make(map[currency.Pair]*settings.Settings)
|
||||
if p.exchangeAssetPairSettings[settings.Exchange][settings.Asset] == nil {
|
||||
p.exchangeAssetPairSettings[settings.Exchange][settings.Asset] = make(map[currency.Pair]*Settings)
|
||||
}
|
||||
if _, ok := p.exchangeAssetPairSettings[exch][a][cp]; !ok {
|
||||
p.exchangeAssetPairSettings[exch][a][cp] = &settings.Settings{}
|
||||
if _, ok := p.exchangeAssetPairSettings[settings.Exchange][settings.Asset][settings.Pair]; !ok {
|
||||
p.exchangeAssetPairSettings[settings.Exchange][settings.Asset][settings.Pair] = &Settings{}
|
||||
}
|
||||
|
||||
return p.exchangeAssetPairSettings[exch][a][cp], nil
|
||||
return p.exchangeAssetPairSettings[settings.Exchange][settings.Asset][settings.Pair], nil
|
||||
}
|
||||
|
||||
// GetLatestHoldings returns the latest holdings after being sorted by time
|
||||
func (e *Settings) GetLatestHoldings() holdings.Holding {
|
||||
if len(e.HoldingsSnapshots) == 0 {
|
||||
return holdings.Holding{}
|
||||
}
|
||||
|
||||
return e.HoldingsSnapshots[len(e.HoldingsSnapshots)-1]
|
||||
}
|
||||
|
||||
// GetHoldingsForTime returns the holdings for a time period, or an empty holding if not found
|
||||
func (e *Settings) GetHoldingsForTime(t time.Time) holdings.Holding {
|
||||
if e.HoldingsSnapshots == nil {
|
||||
// no holdings yet
|
||||
return holdings.Holding{}
|
||||
}
|
||||
for i := len(e.HoldingsSnapshots) - 1; i >= 0; i-- {
|
||||
if e.HoldingsSnapshots[i].Timestamp.Equal(t) {
|
||||
return e.HoldingsSnapshots[i]
|
||||
}
|
||||
}
|
||||
return holdings.Holding{}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ 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/eventhandlers/portfolio/risk"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/settings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ const testExchange = "binance"
|
||||
func TestReset(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := Portfolio{
|
||||
exchangeAssetPairSettings: make(map[string]map[asset.Item]map[currency.Pair]*settings.Settings),
|
||||
exchangeAssetPairSettings: make(map[string]map[asset.Item]map[currency.Pair]*Settings),
|
||||
}
|
||||
p.Reset()
|
||||
if p.exchangeAssetPairSettings != nil {
|
||||
@@ -66,22 +66,27 @@ func TestSetup(t *testing.T) {
|
||||
func TestSetupCurrencySettingsMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := &Portfolio{}
|
||||
_, err := p.SetupCurrencySettingsMap("", "", currency.Pair{})
|
||||
_, err := p.SetupCurrencySettingsMap(nil)
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{})
|
||||
if !errors.Is(err, errExchangeUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errExchangeUnset)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap("hi", "", currency.Pair{})
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi"})
|
||||
if !errors.Is(err, errAssetUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errAssetUnset)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap("hi", asset.Spot, currency.Pair{})
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot})
|
||||
if !errors.Is(err, errCurrencyPairUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errCurrencyPairUnset)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -98,10 +103,14 @@ func TestSetHoldings(t *testing.T) {
|
||||
tt := time.Now()
|
||||
|
||||
err = p.setHoldingsForOffset(&holdings.Holding{Timestamp: tt}, false)
|
||||
if !errors.Is(err, errExchangeUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errExchangeUnset)
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = p.setHoldingsForOffset(&holdings.Holding{
|
||||
Exchange: testExchange,
|
||||
Asset: asset.Spot,
|
||||
@@ -134,8 +143,13 @@ func TestGetLatestHoldingsForAllCurrencies(t *testing.T) {
|
||||
Asset: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Timestamp: tt}, true)
|
||||
if !errors.Is(err, errNoHoldings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoHoldings)
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
h = p.GetLatestHoldingsForAllCurrencies()
|
||||
if len(h) != 0 {
|
||||
@@ -195,6 +209,11 @@ func TestViewHoldingAtTimePeriod(t *testing.T) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoHoldings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = p.setHoldingsForOffset(&holdings.Holding{
|
||||
Offset: 1,
|
||||
Exchange: testExchange,
|
||||
@@ -259,6 +278,11 @@ func TestUpdate(t *testing.T) {
|
||||
Asset: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Timestamp: tt}, false)
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -284,7 +308,7 @@ func TestGetFee(t *testing.T) {
|
||||
t.Error("expected 0")
|
||||
}
|
||||
|
||||
_, err := p.SetupCurrencySettingsMap("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
|
||||
_, err := p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -304,7 +328,7 @@ func TestGetComplianceManager(t *testing.T) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -331,7 +355,7 @@ func TestAddComplianceSnapshot(t *testing.T) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -377,7 +401,7 @@ func TestOnFill(t *testing.T) {
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
_, err = p.SetupCurrencySettingsMap("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -454,7 +478,7 @@ func TestOnSignal(t *testing.T) {
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
_, err = p.SetupCurrencySettingsMap("hi", asset.Spot, currency.NewPair(currency.BTC, currency.USD))
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "hi", Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -497,6 +521,11 @@ func TestOnSignal(t *testing.T) {
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Timestamp: time.Now(),
|
||||
QuoteSize: decimal.NewFromInt(1337)}, false)
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
|
||||
_, err = p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: testExchange, Asset: asset.Spot, Pair: currency.NewPair(currency.BTC, currency.USD)})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -518,3 +547,125 @@ func TestOnSignal(t *testing.T) {
|
||||
t.Error("expected an amount to be sized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestHoldings(t *testing.T) {
|
||||
t.Parallel()
|
||||
cs := Settings{}
|
||||
h := cs.GetLatestHoldings()
|
||||
if !h.Timestamp.IsZero() {
|
||||
t.Error("expected unset holdings")
|
||||
}
|
||||
tt := time.Now()
|
||||
cs.HoldingsSnapshots = append(cs.HoldingsSnapshots, holdings.Holding{Timestamp: tt})
|
||||
|
||||
h = cs.GetLatestHoldings()
|
||||
if !h.Timestamp.Equal(tt) {
|
||||
t.Errorf("expected %v, received %v", tt, h.Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSnapshotAtTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := Portfolio{}
|
||||
_, err := p.GetLatestOrderSnapshotForEvent(&kline.Kline{})
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
cp := currency.NewPair(currency.XRP, currency.DOGE)
|
||||
s, err := p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "exch", Asset: asset.Spot, Pair: currency.NewPair(currency.XRP, currency.DOGE)})
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
tt := time.Now()
|
||||
err = s.ComplianceManager.AddSnapshot([]compliance.SnapshotOrder{
|
||||
{
|
||||
Detail: &gctorder.Detail{
|
||||
Exchange: "exch",
|
||||
AssetType: asset.Spot,
|
||||
Pair: cp,
|
||||
Amount: 1337,
|
||||
},
|
||||
},
|
||||
}, tt, 0, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
e := &kline.Kline{
|
||||
Base: event.Base{
|
||||
Exchange: "exch",
|
||||
Time: tt,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: cp,
|
||||
AssetType: asset.Spot,
|
||||
},
|
||||
}
|
||||
|
||||
ss, err := p.GetLatestOrderSnapshotForEvent(e)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
if len(ss.Orders) != 1 {
|
||||
t.Fatal("expected 1")
|
||||
}
|
||||
if ss.Orders[0].Amount != 1337 {
|
||||
t.Error("expected 1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := Portfolio{}
|
||||
_, err := p.GetLatestOrderSnapshots()
|
||||
if !errors.Is(err, errNoPortfolioSettings) {
|
||||
t.Errorf("received: %v, expected: %v", err, errNoPortfolioSettings)
|
||||
}
|
||||
cp := currency.NewPair(currency.XRP, currency.DOGE)
|
||||
s, err := p.SetupCurrencySettingsMap(&exchange.Settings{Exchange: "exch", Asset: asset.Spot, Pair: currency.NewPair(currency.XRP, currency.DOGE)})
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
tt := time.Now()
|
||||
err = s.ComplianceManager.AddSnapshot([]compliance.SnapshotOrder{
|
||||
{
|
||||
Detail: &gctorder.Detail{
|
||||
Exchange: "exch",
|
||||
AssetType: asset.Spot,
|
||||
Pair: cp,
|
||||
Amount: 1337,
|
||||
},
|
||||
},
|
||||
}, tt, 0, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
ss, err := p.GetLatestOrderSnapshots()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
|
||||
err = s.ComplianceManager.AddSnapshot([]compliance.SnapshotOrder{
|
||||
ss[0].Orders[0],
|
||||
{
|
||||
Detail: &gctorder.Detail{
|
||||
Exchange: "exch",
|
||||
AssetType: asset.Spot,
|
||||
Pair: cp,
|
||||
Amount: 1338,
|
||||
},
|
||||
},
|
||||
}, tt, 1, false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
|
||||
ss, err = p.GetLatestOrderSnapshots()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received: %v, expected: %v", err, nil)
|
||||
}
|
||||
if len(ss) != 1 {
|
||||
t.Fatal("expected 1")
|
||||
}
|
||||
if len(ss[0].Orders) != 2 {
|
||||
t.Error("expected 2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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/eventhandlers/portfolio/risk"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/settings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
@@ -38,7 +37,7 @@ type Portfolio struct {
|
||||
riskFreeRate decimal.Decimal
|
||||
sizeManager SizeHandler
|
||||
riskManager risk.Handler
|
||||
exchangeAssetPairSettings map[string]map[asset.Item]map[currency.Pair]*settings.Settings
|
||||
exchangeAssetPairSettings map[string]map[asset.Item]map[currency.Pair]*Settings
|
||||
}
|
||||
|
||||
// Handler contains all functions expected to operate a portfolio manager
|
||||
@@ -46,6 +45,9 @@ type Handler interface {
|
||||
OnSignal(signal.Event, *exchange.Settings, funding.IPairReserver) (*order.Order, error)
|
||||
OnFill(fill.Event, funding.IPairReader) (*fill.Fill, error)
|
||||
|
||||
GetLatestOrderSnapshotForEvent(common.EventHandler) (compliance.Snapshot, error)
|
||||
GetLatestOrderSnapshots() ([]compliance.Snapshot, error)
|
||||
|
||||
ViewHoldingAtTimePeriod(common.EventHandler) (*holdings.Holding, error)
|
||||
setHoldingsForOffset(*holdings.Holding, bool) error
|
||||
UpdateHoldings(common.DataEventHandler, funding.IPairReader) error
|
||||
@@ -62,3 +64,14 @@ type Handler interface {
|
||||
type SizeHandler interface {
|
||||
SizeOrder(order.Event, decimal.Decimal, *exchange.Settings) (*order.Order, error)
|
||||
}
|
||||
|
||||
// Settings holds all important information for the portfolio manager
|
||||
// to assess purchasing decisions
|
||||
type Settings struct {
|
||||
Fee decimal.Decimal
|
||||
BuySideSizing exchange.MinMax
|
||||
SellSideSizing exchange.MinMax
|
||||
Leverage exchange.Leverage
|
||||
HoldingsSnapshots []holdings.Holding
|
||||
ComplianceManager compliance.Manager
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
)
|
||||
|
||||
// GetLatestHoldings returns the latest holdings after being sorted by time
|
||||
func (e *Settings) GetLatestHoldings() holdings.Holding {
|
||||
if len(e.HoldingsSnapshots) == 0 {
|
||||
return holdings.Holding{}
|
||||
}
|
||||
|
||||
return e.HoldingsSnapshots[len(e.HoldingsSnapshots)-1]
|
||||
}
|
||||
|
||||
// GetHoldingsForTime returns the holdings for a time period, or an empty holding if not found
|
||||
func (e *Settings) GetHoldingsForTime(t time.Time) holdings.Holding {
|
||||
if e.HoldingsSnapshots == nil {
|
||||
// no holdings yet
|
||||
return holdings.Holding{}
|
||||
}
|
||||
for i := len(e.HoldingsSnapshots) - 1; i >= 0; i-- {
|
||||
if e.HoldingsSnapshots[i].Timestamp.Equal(t) {
|
||||
return e.HoldingsSnapshots[i]
|
||||
}
|
||||
}
|
||||
return holdings.Holding{}
|
||||
}
|
||||
|
||||
// Value returns the total value of the latest holdings
|
||||
func (e *Settings) Value() decimal.Decimal {
|
||||
latest := e.GetLatestHoldings()
|
||||
if latest.Timestamp.IsZero() {
|
||||
return decimal.Zero
|
||||
}
|
||||
return latest.TotalValue
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
)
|
||||
|
||||
func TestGetLatestHoldings(t *testing.T) {
|
||||
t.Parallel()
|
||||
cs := Settings{}
|
||||
h := cs.GetLatestHoldings()
|
||||
if !h.Timestamp.IsZero() {
|
||||
t.Error("expected unset holdings")
|
||||
}
|
||||
tt := time.Now()
|
||||
cs.HoldingsSnapshots = append(cs.HoldingsSnapshots, holdings.Holding{Timestamp: tt})
|
||||
|
||||
h = cs.GetLatestHoldings()
|
||||
if !h.Timestamp.Equal(tt) {
|
||||
t.Errorf("expected %v, received %v", tt, h.Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
t.Parallel()
|
||||
cs := Settings{}
|
||||
v := cs.Value()
|
||||
if !v.IsZero() {
|
||||
t.Error("expected 0")
|
||||
}
|
||||
cs.HoldingsSnapshots = append(cs.HoldingsSnapshots,
|
||||
holdings.Holding{
|
||||
Timestamp: time.Now(),
|
||||
TotalValue: decimal.NewFromInt(1337),
|
||||
},
|
||||
)
|
||||
|
||||
v = cs.Value()
|
||||
if !v.Equal(decimal.NewFromInt(1337)) {
|
||||
t.Errorf("expected %v, received %v", decimal.NewFromInt(1337), v)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
)
|
||||
|
||||
// Settings holds all important information for the portfolio manager
|
||||
// to assess purchasing decisions
|
||||
type Settings struct {
|
||||
Fee decimal.Decimal
|
||||
BuySideSizing config.MinMax
|
||||
SellSideSizing config.MinMax
|
||||
Leverage config.Leverage
|
||||
HoldingsSnapshots []holdings.Holding
|
||||
ComplianceManager compliance.Manager
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
@@ -72,7 +71,7 @@ func (s *Size) SizeOrder(o order.Event, amountAvailable decimal.Decimal, cs *exc
|
||||
// that is allowed to be spent/sold for an event.
|
||||
// As fee calculation occurs during the actual ordering process
|
||||
// this can only attempt to factor the potential fee to remain under the max rules
|
||||
func (s *Size) calculateBuySize(price, availableFunds, feeRate, buyLimit decimal.Decimal, minMaxSettings config.MinMax) (decimal.Decimal, error) {
|
||||
func (s *Size) calculateBuySize(price, availableFunds, feeRate, buyLimit decimal.Decimal, minMaxSettings exchange.MinMax) (decimal.Decimal, error) {
|
||||
if availableFunds.LessThanOrEqual(decimal.Zero) {
|
||||
return decimal.Zero, errNoFunds
|
||||
}
|
||||
@@ -104,7 +103,7 @@ func (s *Size) calculateBuySize(price, availableFunds, feeRate, buyLimit decimal
|
||||
// eg BTC-USD baseAmount will be BTC to be sold
|
||||
// As fee calculation occurs during the actual ordering process
|
||||
// this can only attempt to factor the potential fee to remain under the max rules
|
||||
func (s *Size) calculateSellSize(price, baseAmount, feeRate, sellLimit decimal.Decimal, minMaxSettings config.MinMax) (decimal.Decimal, error) {
|
||||
func (s *Size) calculateSellSize(price, baseAmount, feeRate, sellLimit decimal.Decimal, minMaxSettings exchange.MinMax) (decimal.Decimal, error) {
|
||||
if baseAmount.LessThanOrEqual(decimal.Zero) {
|
||||
return decimal.Zero, errNoFunds
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
@@ -14,7 +13,7 @@ import (
|
||||
|
||||
func TestSizingAccuracy(t *testing.T) {
|
||||
t.Parallel()
|
||||
globalMinMax := config.MinMax{
|
||||
globalMinMax := exchange.MinMax{
|
||||
MinimumSize: decimal.Zero,
|
||||
MaximumSize: decimal.NewFromInt(1),
|
||||
MaximumTotal: decimal.NewFromInt(10),
|
||||
@@ -39,7 +38,7 @@ func TestSizingAccuracy(t *testing.T) {
|
||||
|
||||
func TestSizingOverMaxSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
globalMinMax := config.MinMax{
|
||||
globalMinMax := exchange.MinMax{
|
||||
MinimumSize: decimal.Zero,
|
||||
MaximumSize: decimal.NewFromFloat(0.5),
|
||||
MaximumTotal: decimal.NewFromInt(1337),
|
||||
@@ -63,7 +62,7 @@ func TestSizingOverMaxSize(t *testing.T) {
|
||||
|
||||
func TestSizingUnderMinSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
globalMinMax := config.MinMax{
|
||||
globalMinMax := exchange.MinMax{
|
||||
MinimumSize: decimal.NewFromInt(1),
|
||||
MaximumSize: decimal.NewFromInt(2),
|
||||
MaximumTotal: decimal.NewFromInt(1337),
|
||||
@@ -84,7 +83,7 @@ func TestSizingUnderMinSize(t *testing.T) {
|
||||
|
||||
func TestMaximumBuySizeEqualZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
globalMinMax := config.MinMax{
|
||||
globalMinMax := exchange.MinMax{
|
||||
MinimumSize: decimal.NewFromInt(1),
|
||||
MaximumSize: decimal.Zero,
|
||||
MaximumTotal: decimal.NewFromInt(1437),
|
||||
@@ -104,7 +103,7 @@ func TestMaximumBuySizeEqualZero(t *testing.T) {
|
||||
}
|
||||
func TestMaximumSellSizeEqualZero(t *testing.T) {
|
||||
t.Parallel()
|
||||
globalMinMax := config.MinMax{
|
||||
globalMinMax := exchange.MinMax{
|
||||
MinimumSize: decimal.NewFromInt(1),
|
||||
MaximumSize: decimal.Zero,
|
||||
MaximumTotal: decimal.NewFromInt(1437),
|
||||
@@ -125,7 +124,7 @@ func TestMaximumSellSizeEqualZero(t *testing.T) {
|
||||
|
||||
func TestSizingErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
globalMinMax := config.MinMax{
|
||||
globalMinMax := exchange.MinMax{
|
||||
MinimumSize: decimal.NewFromInt(1),
|
||||
MaximumSize: decimal.NewFromInt(2),
|
||||
MaximumTotal: decimal.NewFromInt(1337),
|
||||
@@ -146,7 +145,7 @@ func TestSizingErrors(t *testing.T) {
|
||||
|
||||
func TestCalculateSellSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
globalMinMax := config.MinMax{
|
||||
globalMinMax := exchange.MinMax{
|
||||
MinimumSize: decimal.NewFromInt(1),
|
||||
MaximumSize: decimal.NewFromInt(2),
|
||||
MaximumTotal: decimal.NewFromInt(1337),
|
||||
|
||||
@@ -3,7 +3,7 @@ package size
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -14,6 +14,6 @@ var (
|
||||
|
||||
// Size contains buy and sell side rules
|
||||
type Size struct {
|
||||
BuySide config.MinMax
|
||||
SellSide config.MinMax
|
||||
BuySide exchange.MinMax
|
||||
SellSide exchange.MinMax
|
||||
}
|
||||
|
||||
@@ -23,6 +23,36 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
|
||||
The statistics package is used for storing all relevant data over the course of a GoCryptoTrader Backtesting run. All types of events are tracked by exchange, asset and currency pair.
|
||||
When multiple currencies are included in your strategy, the statistics package will be able to calculate which exchange asset currency pair has performed the best, along with the biggest drop downs in the market.
|
||||
|
||||
It can calculate the following:
|
||||
- Calmar ratio
|
||||
- Information ratio
|
||||
- Sharpe ratio
|
||||
- Sortino ratio
|
||||
- CAGR
|
||||
- Drawdowns, both the biggest and longest
|
||||
- Whether the strategy outperformed the market
|
||||
- If the strategy made a profit
|
||||
|
||||
## Ratios
|
||||
|
||||
| Ratio | Description | A good range |
|
||||
| ----- | ----------- | ------------ |
|
||||
| Calmar ratio | It is a function of the fund's average compounded annual rate of return versus its maximum drawdown. The higher the Calmar ratio, the better it performed on a risk-adjusted basis during the given time frame, which is mostly commonly set at 36 months | 3.0 to 5.0 |
|
||||
| Information ratio| It is a measurement of portfolio returns beyond the returns of a benchmark, usually an index, compared to the volatility of those returns. The ratio is often used as a measure of a portfolio manager's level of skill and ability to generate excess returns relative to a benchmark | 0.40-0.60. Any positive number means that it has beaten the benchmark |
|
||||
| Sharpe ratio | The Sharpe Ratio is a financial metric often used by investors when assessing the performance of investment management products and professionals. It consists of taking the excess return of the portfolio, relative to the risk-free rate, and dividing it by the standard deviation of the portfolio's excess returns | Any Sharpe ratio greater than 1.0 is good. Higher than 2.0 is very good. 3.0 or higher is excellent. Under 1.0 is sub-optimal |
|
||||
| Sortino ratio | The Sortino ratio measures the risk-adjusted return of an investment asset, portfolio, or strategy. It is a modification of the Sharpe ratio but penalizes only those returns falling below a user-specified target or required rate of return, while the Sharpe ratio penalizes both upside and downside volatility equally | The higher the better, but > 2 is considered good |
|
||||
| Compound annual growth rate | Compound annual growth rate is the rate of return that would be required for an investment to grow from its beginning balance to its ending balance, assuming the profits were reinvested at the end of each year of the investment’s lifespan | Any positive number |
|
||||
|
||||
## Arithmetic or versus geometric?
|
||||
Both! We calculate ratios where an average is required using both types. The reasoning for using either is debated by finance and mathematicians. [This](https://www.investopedia.com/ask/answers/06/geometricmean.asp) is a good breakdown of both, but here is an extra simple table
|
||||
|
||||
| Average type | A reason to use it |
|
||||
| ------------ | ------------------ |
|
||||
| Arithmetic | The arithmetic mean is the average of a sum of numbers, which reflects the central tendency of the position of the numbers |
|
||||
| Geometric | The geometric mean differs from the arithmetic average, or arithmetic mean, in how it is calculated because it takes into account the compounding that occurs from period to period. Because of this, investors usually consider the geometric mean a more accurate measure of returns than the arithmetic mean |
|
||||
|
||||
## USD total tracking
|
||||
If the strategy config setting `DisableUSDTracking` is `false`, then the GoCryptoTrader Backtester will automatically retrieve USD data that matches your backtesting currencies, eg pair BTC/LTC will track BTC/USD and LTC/USD as well. This allows for tracking overall strategic performance against one currency. This can allow for much easier performance calculations and comparisons
|
||||
|
||||
|
||||
### Please click GoDocs chevron above to view current GoDoc information for this package
|
||||
|
||||
397
backtester/eventhandlers/statistics/currencystatistics.go
Normal file
397
backtester/eventhandlers/statistics/currencystatistics.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
||||
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// CalculateResults calculates all statistics for the exchange, asset, currency pair
|
||||
func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) error {
|
||||
var errs gctcommon.Errors
|
||||
var err error
|
||||
first := c.Events[0]
|
||||
sep := fmt.Sprintf("%v %v %v |\t", first.DataEvent.GetExchange(), first.DataEvent.GetAssetType(), first.DataEvent.Pair())
|
||||
|
||||
firstPrice := first.DataEvent.ClosePrice()
|
||||
last := c.Events[len(c.Events)-1]
|
||||
lastPrice := last.DataEvent.ClosePrice()
|
||||
for i := range last.Transactions.Orders {
|
||||
if last.Transactions.Orders[i].Side == gctorder.Buy {
|
||||
c.BuyOrders++
|
||||
} else if last.Transactions.Orders[i].Side == gctorder.Sell {
|
||||
c.SellOrders++
|
||||
}
|
||||
}
|
||||
for i := range c.Events {
|
||||
price := c.Events[i].DataEvent.ClosePrice()
|
||||
if c.LowestClosePrice.IsZero() || price.LessThan(c.LowestClosePrice) {
|
||||
c.LowestClosePrice = price
|
||||
}
|
||||
if price.GreaterThan(c.HighestClosePrice) {
|
||||
c.HighestClosePrice = price
|
||||
}
|
||||
}
|
||||
|
||||
oneHundred := decimal.NewFromInt(100)
|
||||
c.MarketMovement = lastPrice.Sub(firstPrice).Div(firstPrice).Mul(oneHundred)
|
||||
if first.Holdings.TotalValue.GreaterThan(decimal.Zero) {
|
||||
c.StrategyMovement = last.Holdings.TotalValue.Sub(first.Holdings.TotalValue).Div(first.Holdings.TotalValue).Mul(oneHundred)
|
||||
}
|
||||
c.calculateHighestCommittedFunds()
|
||||
returnsPerCandle := make([]decimal.Decimal, len(c.Events))
|
||||
benchmarkRates := make([]decimal.Decimal, len(c.Events))
|
||||
|
||||
var allDataEvents []common.DataEventHandler
|
||||
for i := range c.Events {
|
||||
returnsPerCandle[i] = c.Events[i].Holdings.ChangeInTotalValuePercent
|
||||
allDataEvents = append(allDataEvents, c.Events[i].DataEvent)
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
if c.Events[i].SignalEvent != nil && c.Events[i].SignalEvent.GetDirection() == common.MissingData {
|
||||
c.ShowMissingDataWarning = true
|
||||
}
|
||||
benchmarkRates[i] = c.Events[i].DataEvent.ClosePrice().Sub(
|
||||
c.Events[i-1].DataEvent.ClosePrice()).Div(
|
||||
c.Events[i-1].DataEvent.ClosePrice())
|
||||
}
|
||||
|
||||
// remove the first entry as its zero and impacts
|
||||
// ratio calculations as no movement has been made
|
||||
benchmarkRates = benchmarkRates[1:]
|
||||
returnsPerCandle = returnsPerCandle[1:]
|
||||
c.MaxDrawdown, err = CalculateBiggestEventDrawdown(allDataEvents)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
interval := first.DataEvent.GetInterval()
|
||||
intervalsPerYear := interval.IntervalsPerYear()
|
||||
riskFreeRatePerCandle := riskFreeRate.Div(decimal.NewFromFloat(intervalsPerYear))
|
||||
c.ArithmeticRatios, c.GeometricRatios, err = CalculateRatios(benchmarkRates, returnsPerCandle, riskFreeRatePerCandle, &c.MaxDrawdown, sep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if last.Holdings.QuoteInitialFunds.GreaterThan(decimal.Zero) {
|
||||
cagr, err := gctmath.DecimalCompoundAnnualGrowthRate(
|
||||
last.Holdings.QuoteInitialFunds,
|
||||
last.Holdings.TotalValue,
|
||||
decimal.NewFromFloat(intervalsPerYear),
|
||||
decimal.NewFromInt(int64(len(c.Events))),
|
||||
)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if !cagr.IsZero() {
|
||||
c.CompoundAnnualGrowthRate = cagr
|
||||
}
|
||||
}
|
||||
c.IsStrategyProfitable = last.Holdings.TotalValue.GreaterThan(first.Holdings.TotalValue)
|
||||
c.DoesPerformanceBeatTheMarket = c.StrategyMovement.GreaterThan(c.MarketMovement)
|
||||
|
||||
c.TotalFees = last.Holdings.TotalFees.Round(8)
|
||||
c.TotalValueLostToVolumeSizing = last.Holdings.TotalValueLostToVolumeSizing.Round(2)
|
||||
c.TotalValueLost = last.Holdings.TotalValueLost.Round(2)
|
||||
c.TotalValueLostToSlippage = last.Holdings.TotalValueLostToSlippage.Round(2)
|
||||
c.TotalAssetValue = last.Holdings.BaseValue.Round(8)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintResults outputs all calculated statistics to the command line
|
||||
func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.Pair, usingExchangeLevelFunding bool) {
|
||||
var errs gctcommon.Errors
|
||||
sort.Slice(c.Events, func(i, j int) bool {
|
||||
return c.Events[i].DataEvent.GetTime().Before(c.Events[j].DataEvent.GetTime())
|
||||
})
|
||||
last := c.Events[len(c.Events)-1]
|
||||
first := c.Events[0]
|
||||
c.StartingClosePrice = first.DataEvent.ClosePrice()
|
||||
c.EndingClosePrice = last.DataEvent.ClosePrice()
|
||||
c.TotalOrders = c.BuyOrders + c.SellOrders
|
||||
last.Holdings.TotalValueLost = last.Holdings.TotalValueLostToSlippage.Add(last.Holdings.TotalValueLostToVolumeSizing)
|
||||
sep := fmt.Sprintf("%v %v %v |\t", e, a, p)
|
||||
currStr := fmt.Sprintf("------------------Stats for %v %v %v------------------------------------------", e, a, p)
|
||||
log.Infof(log.BackTester, currStr[:61])
|
||||
log.Infof(log.BackTester, "%s Highest committed funds: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestCommittedFunds.Value, 8, ".", ","), c.HighestCommittedFunds.Time)
|
||||
log.Infof(log.BackTester, "%s Buy orders: %s", sep, convert.IntToHumanFriendlyString(c.BuyOrders, ","))
|
||||
log.Infof(log.BackTester, "%s Buy value: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BoughtValue, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Buy amount: %s %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BoughtAmount, 8, ".", ","), last.Holdings.Pair.Base)
|
||||
log.Infof(log.BackTester, "%s Sell orders: %s", sep, convert.IntToHumanFriendlyString(c.SellOrders, ","))
|
||||
log.Infof(log.BackTester, "%s Sell value: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.SoldValue, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Sell amount: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.SoldAmount, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Total orders: %s\n\n", sep, convert.IntToHumanFriendlyString(c.TotalOrders, ","))
|
||||
|
||||
log.Info(log.BackTester, "------------------Max Drawdown-------------------------------")
|
||||
log.Infof(log.BackTester, "%s Highest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value, 8, ".", ","), c.MaxDrawdown.Highest.Time)
|
||||
log.Infof(log.BackTester, "%s Lowest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Lowest.Value, 8, ".", ","), c.MaxDrawdown.Lowest.Time)
|
||||
log.Infof(log.BackTester, "%s Calculated Drawdown: %s%%", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.DrawdownPercent, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Difference: %s", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value.Sub(c.MaxDrawdown.Lowest.Value), 2, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Drawdown length: %s\n\n", sep, convert.IntToHumanFriendlyString(c.MaxDrawdown.IntervalDuration, ","))
|
||||
if !usingExchangeLevelFunding {
|
||||
log.Info(log.BackTester, "------------------Ratios------------------------------------------------")
|
||||
log.Info(log.BackTester, "------------------Rates-------------------------------------------------")
|
||||
log.Infof(log.BackTester, "%s Compound Annual Growth Rate: %s", sep, convert.DecimalToHumanFriendlyString(c.CompoundAnnualGrowthRate, 2, ".", ","))
|
||||
log.Info(log.BackTester, "------------------Arithmetic--------------------------------------------")
|
||||
if c.ShowMissingDataWarning {
|
||||
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
|
||||
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, c.ArithmeticRatios.SharpeRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, c.ArithmeticRatios.SortinoRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Information ratio: %v", sep, c.ArithmeticRatios.InformationRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Calmar ratio: %v", sep, c.ArithmeticRatios.CalmarRatio.Round(4))
|
||||
|
||||
log.Info(log.BackTester, "------------------Geometric--------------------------------------------")
|
||||
if c.ShowMissingDataWarning {
|
||||
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
|
||||
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, c.GeometricRatios.SharpeRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, c.GeometricRatios.SortinoRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Information ratio: %v", sep, c.GeometricRatios.InformationRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Calmar ratio: %v\n\n", sep, c.GeometricRatios.CalmarRatio.Round(4))
|
||||
}
|
||||
|
||||
log.Info(log.BackTester, "------------------Results------------------------------------")
|
||||
log.Infof(log.BackTester, "%s Starting Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.StartingClosePrice, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Finishing Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.EndingClosePrice, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Lowest Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.LowestClosePrice, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Highest Close Price: %s", sep, convert.DecimalToHumanFriendlyString(c.HighestClosePrice, 8, ".", ","))
|
||||
|
||||
log.Infof(log.BackTester, "%s Market movement: %s%%", sep, convert.DecimalToHumanFriendlyString(c.MarketMovement, 2, ".", ","))
|
||||
if !usingExchangeLevelFunding {
|
||||
log.Infof(log.BackTester, "%s Strategy movement: %s%%", sep, convert.DecimalToHumanFriendlyString(c.StrategyMovement, 2, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Did it beat the market: %v", sep, c.StrategyMovement.GreaterThan(c.MarketMovement))
|
||||
}
|
||||
|
||||
log.Infof(log.BackTester, "%s Value lost to volume sizing: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLostToVolumeSizing, 2, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Value lost to slippage: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLostToSlippage, 2, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Total Value lost: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalValueLost, 2, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Total Fees: %s\n\n", sep, convert.DecimalToHumanFriendlyString(c.TotalFees, 8, ".", ","))
|
||||
|
||||
log.Infof(log.BackTester, "%s Final holdings value: %s", sep, convert.DecimalToHumanFriendlyString(c.TotalAssetValue, 8, ".", ","))
|
||||
if !usingExchangeLevelFunding {
|
||||
// the following have no direct translation to individual exchange level funds as they
|
||||
// combine base and quote values
|
||||
log.Infof(log.BackTester, "%s Final funds: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.QuoteSize, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Final holdings: %s", sep, convert.DecimalToHumanFriendlyString(last.Holdings.BaseSize, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Final total value: %s\n\n", sep, convert.DecimalToHumanFriendlyString(last.Holdings.TotalValue, 8, ".", ","))
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
log.Info(log.BackTester, "------------------Errors-------------------------------------")
|
||||
for i := range errs {
|
||||
log.Error(log.BackTester, errs[i].Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateBiggestEventDrawdown calculates the biggest drawdown using a slice of DataEvents
|
||||
func CalculateBiggestEventDrawdown(closePrices []common.DataEventHandler) (Swing, error) {
|
||||
if len(closePrices) == 0 {
|
||||
return Swing{}, fmt.Errorf("%w to calculate drawdowns", errReceivedNoData)
|
||||
}
|
||||
var swings []Swing
|
||||
lowestPrice := closePrices[0].LowPrice()
|
||||
highestPrice := closePrices[0].HighPrice()
|
||||
lowestTime := closePrices[0].GetTime()
|
||||
highestTime := closePrices[0].GetTime()
|
||||
interval := closePrices[0].GetInterval()
|
||||
|
||||
for i := range closePrices {
|
||||
currHigh := closePrices[i].HighPrice()
|
||||
currLow := closePrices[i].LowPrice()
|
||||
currTime := closePrices[i].GetTime()
|
||||
if lowestPrice.GreaterThan(currLow) && !currLow.IsZero() {
|
||||
lowestPrice = currLow
|
||||
lowestTime = currTime
|
||||
}
|
||||
if highestPrice.LessThan(currHigh) && highestPrice.IsPositive() {
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
|
||||
}
|
||||
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[i].GetInterval(), 0)
|
||||
if err != nil {
|
||||
log.Error(log.BackTester, err)
|
||||
continue
|
||||
}
|
||||
swings = append(swings, Swing{
|
||||
Highest: ValueAtTime{
|
||||
Time: highestTime,
|
||||
Value: highestPrice,
|
||||
},
|
||||
Lowest: ValueAtTime{
|
||||
Time: lowestTime,
|
||||
Value: lowestPrice,
|
||||
},
|
||||
DrawdownPercent: lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100)),
|
||||
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
|
||||
})
|
||||
// reset the drawdown
|
||||
highestPrice = currHigh
|
||||
highestTime = currTime
|
||||
lowestPrice = currLow
|
||||
lowestTime = currTime
|
||||
}
|
||||
}
|
||||
if (len(swings) > 0 && swings[len(swings)-1].Lowest.Value != closePrices[len(closePrices)-1].LowPrice()) || swings == nil {
|
||||
// need to close out the final drawdown
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
|
||||
}
|
||||
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[0].GetInterval(), 0)
|
||||
if err != nil {
|
||||
return Swing{}, err
|
||||
}
|
||||
drawdownPercent := decimal.Zero
|
||||
if highestPrice.GreaterThan(decimal.Zero) {
|
||||
drawdownPercent = lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100))
|
||||
}
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
|
||||
}
|
||||
swings = append(swings, Swing{
|
||||
Highest: ValueAtTime{
|
||||
Time: highestTime,
|
||||
Value: highestPrice,
|
||||
},
|
||||
Lowest: ValueAtTime{
|
||||
Time: lowestTime,
|
||||
Value: lowestPrice,
|
||||
},
|
||||
DrawdownPercent: drawdownPercent,
|
||||
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
|
||||
})
|
||||
}
|
||||
|
||||
var maxDrawdown Swing
|
||||
if len(swings) > 0 {
|
||||
maxDrawdown = swings[0]
|
||||
}
|
||||
for i := range swings {
|
||||
if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
|
||||
maxDrawdown = swings[i]
|
||||
}
|
||||
}
|
||||
|
||||
return maxDrawdown, nil
|
||||
}
|
||||
|
||||
func (c *CurrencyPairStatistic) calculateHighestCommittedFunds() {
|
||||
for i := range c.Events {
|
||||
if c.Events[i].Holdings.BaseSize.Mul(c.Events[i].DataEvent.ClosePrice()).GreaterThan(c.HighestCommittedFunds.Value) {
|
||||
c.HighestCommittedFunds.Value = c.Events[i].Holdings.BaseSize.Mul(c.Events[i].DataEvent.ClosePrice())
|
||||
c.HighestCommittedFunds.Time = c.Events[i].Holdings.Timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateBiggestValueAtTimeDrawdown calculates the biggest drawdown using a slice of ValueAtTimes
|
||||
func CalculateBiggestValueAtTimeDrawdown(closePrices []ValueAtTime, interval gctkline.Interval) (Swing, error) {
|
||||
if len(closePrices) == 0 {
|
||||
return Swing{}, fmt.Errorf("%w to calculate drawdowns", errReceivedNoData)
|
||||
}
|
||||
var swings []Swing
|
||||
lowestPrice := closePrices[0].Value
|
||||
highestPrice := closePrices[0].Value
|
||||
lowestTime := closePrices[0].Time
|
||||
highestTime := closePrices[0].Time
|
||||
|
||||
for i := range closePrices {
|
||||
currHigh := closePrices[i].Value
|
||||
currLow := closePrices[i].Value
|
||||
currTime := closePrices[i].Time
|
||||
if lowestPrice.GreaterThan(currLow) && !currLow.IsZero() {
|
||||
lowestPrice = currLow
|
||||
lowestTime = currTime
|
||||
}
|
||||
if highestPrice.LessThan(currHigh) && highestPrice.IsPositive() {
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
|
||||
}
|
||||
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, interval, 0)
|
||||
if err != nil {
|
||||
return Swing{}, err
|
||||
}
|
||||
swings = append(swings, Swing{
|
||||
Highest: ValueAtTime{
|
||||
Time: highestTime,
|
||||
Value: highestPrice,
|
||||
},
|
||||
Lowest: ValueAtTime{
|
||||
Time: lowestTime,
|
||||
Value: lowestPrice,
|
||||
},
|
||||
DrawdownPercent: lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100)),
|
||||
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
|
||||
})
|
||||
// reset the drawdown
|
||||
highestPrice = currHigh
|
||||
highestTime = currTime
|
||||
lowestPrice = currLow
|
||||
lowestTime = currTime
|
||||
}
|
||||
}
|
||||
if (len(swings) > 0 && !swings[len(swings)-1].Lowest.Value.Equal(closePrices[len(closePrices)-1].Value)) || swings == nil {
|
||||
// need to close out the final drawdown
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
|
||||
}
|
||||
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, interval, 0)
|
||||
if err != nil {
|
||||
log.Error(log.BackTester, err)
|
||||
}
|
||||
drawdownPercent := decimal.Zero
|
||||
if highestPrice.GreaterThan(decimal.Zero) {
|
||||
drawdownPercent = lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100))
|
||||
}
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add(interval.Duration() - time.Nanosecond)
|
||||
}
|
||||
swings = append(swings, Swing{
|
||||
Highest: ValueAtTime{
|
||||
Time: highestTime,
|
||||
Value: highestPrice,
|
||||
},
|
||||
Lowest: ValueAtTime{
|
||||
Time: lowestTime,
|
||||
Value: lowestPrice,
|
||||
},
|
||||
DrawdownPercent: drawdownPercent,
|
||||
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
|
||||
})
|
||||
}
|
||||
|
||||
var maxDrawdown Swing
|
||||
if len(swings) > 0 {
|
||||
maxDrawdown = swings[0]
|
||||
}
|
||||
for i := range swings {
|
||||
if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
|
||||
maxDrawdown = swings[i]
|
||||
}
|
||||
}
|
||||
|
||||
return maxDrawdown, nil
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
# GoCryptoTrader Backtester: Currencystatistics package
|
||||
|
||||
<img src="/backtester/common/backtester.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics/currencystatistics)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
|
||||
|
||||
|
||||
This currencystatistics package is part of the GoCryptoTrader codebase.
|
||||
|
||||
## This is still in active development
|
||||
|
||||
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
|
||||
|
||||
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
|
||||
|
||||
## Currencystatistics package overview
|
||||
|
||||
Currency Statistics is an important package to verify the effectiveness of your strategies.
|
||||
It can calculate the following:
|
||||
- Calmar ratio
|
||||
- Information ratio
|
||||
- Sharpe ratio
|
||||
- Sortino ratio
|
||||
- CAGR
|
||||
- Drawdowns, both the biggest and longest
|
||||
- Whether the strategy outperformed the market
|
||||
- If the strategy made a profit
|
||||
|
||||
## Ratios
|
||||
|
||||
| Ratio | Description | A good range |
|
||||
| ----- | ----------- | ------------ |
|
||||
| Calmar ratio | It is a function of the fund's average compounded annual rate of return versus its maximum drawdown. The higher the Calmar ratio, the better it performed on a risk-adjusted basis during the given time frame, which is mostly commonly set at 36 months | 3.0 to 5.0 |
|
||||
| Information ratio| It is a measurement of portfolio returns beyond the returns of a benchmark, usually an index, compared to the volatility of those returns. The ratio is often used as a measure of a portfolio manager's level of skill and ability to generate excess returns relative to a benchmark | 0.40-0.60. Any positive number means that it has beaten the benchmark |
|
||||
| Sharpe ratio | The Sharpe Ratio is a financial metric often used by investors when assessing the performance of investment management products and professionals. It consists of taking the excess return of the portfolio, relative to the risk-free rate, and dividing it by the standard deviation of the portfolio's excess returns | Any Sharpe ratio greater than 1.0 is good. Higher than 2.0 is very good. 3.0 or higher is excellent. Under 1.0 is sub-optimal |
|
||||
| Sortino ratio | The Sortino ratio measures the risk-adjusted return of an investment asset, portfolio, or strategy. It is a modification of the Sharpe ratio but penalizes only those returns falling below a user-specified target or required rate of return, while the Sharpe ratio penalizes both upside and downside volatility equally | The higher the better, but > 2 is considered good |
|
||||
| Compound annual growth rate | Compound annual growth rate is the rate of return that would be required for an investment to grow from its beginning balance to its ending balance, assuming the profits were reinvested at the end of each year of the investment’s lifespan | Any positive number |
|
||||
|
||||
## Arithmetic or versus geometric?
|
||||
Both! We calculate ratios where an average is required using both types. The reasoning for using either is debated by finance and mathematicians. [This](https://www.investopedia.com/ask/answers/06/geometricmean.asp) is a good breakdown of both, but here is an extra simple table
|
||||
|
||||
| Average type | A reason to use it |
|
||||
| ------------ | ------------------ |
|
||||
| Arithmetic | The arithmetic mean is the average of a sum of numbers, which reflects the central tendency of the position of the numbers |
|
||||
| Geometric | The geometric mean differs from the arithmetic average, or arithmetic mean, in how it is calculated because it takes into account the compounding that occurs from period to period. Because of this, investors usually consider the geometric mean a more accurate measure of returns than the arithmetic mean |
|
||||
|
||||
|
||||
|
||||
### Please click GoDocs chevron above to view current GoDoc information for this package
|
||||
|
||||
## Contribution
|
||||
|
||||
Please feel free to submit any pull requests or suggest any desired features to be added.
|
||||
|
||||
When submitting a PR, please abide by our coding guidelines:
|
||||
|
||||
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
|
||||
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
|
||||
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
|
||||
+ Pull requests need to be based on and opened against the `master` branch.
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***
|
||||
@@ -1,397 +0,0 @@
|
||||
package currencystatistics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
||||
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// CalculateResults calculates all statistics for the exchange, asset, currency pair
|
||||
func (c *CurrencyStatistic) CalculateResults(f funding.IPairReader) error {
|
||||
var errs gctcommon.Errors
|
||||
var err error
|
||||
first := c.Events[0]
|
||||
sep := fmt.Sprintf("%v %v %v |\t", first.DataEvent.GetExchange(), first.DataEvent.GetAssetType(), first.DataEvent.Pair())
|
||||
|
||||
firstPrice := first.DataEvent.ClosePrice()
|
||||
last := c.Events[len(c.Events)-1]
|
||||
lastPrice := last.DataEvent.ClosePrice()
|
||||
for i := range last.Transactions.Orders {
|
||||
if last.Transactions.Orders[i].Side == gctorder.Buy {
|
||||
c.BuyOrders++
|
||||
} else if last.Transactions.Orders[i].Side == gctorder.Sell {
|
||||
c.SellOrders++
|
||||
}
|
||||
}
|
||||
for i := range c.Events {
|
||||
price := c.Events[i].DataEvent.ClosePrice()
|
||||
if c.LowestClosePrice.IsZero() || price.LessThan(c.LowestClosePrice) {
|
||||
c.LowestClosePrice = price
|
||||
}
|
||||
if price.GreaterThan(c.HighestClosePrice) {
|
||||
c.HighestClosePrice = price
|
||||
}
|
||||
}
|
||||
|
||||
oneHundred := decimal.NewFromInt(100)
|
||||
c.MarketMovement = lastPrice.Sub(firstPrice).Div(firstPrice).Mul(oneHundred)
|
||||
if first.Holdings.TotalValue.GreaterThan(decimal.Zero) {
|
||||
c.StrategyMovement = last.Holdings.TotalValue.Sub(first.Holdings.TotalValue).Div(first.Holdings.TotalValue).Mul(oneHundred)
|
||||
}
|
||||
c.calculateHighestCommittedFunds()
|
||||
c.RiskFreeRate = last.Holdings.RiskFreeRate.Mul(oneHundred)
|
||||
returnPerCandle := make([]decimal.Decimal, len(c.Events))
|
||||
benchmarkRates := make([]decimal.Decimal, len(c.Events))
|
||||
|
||||
var allDataEvents []common.DataEventHandler
|
||||
for i := range c.Events {
|
||||
returnPerCandle[i] = c.Events[i].Holdings.ChangeInTotalValuePercent
|
||||
allDataEvents = append(allDataEvents, c.Events[i].DataEvent)
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
if c.Events[i].SignalEvent != nil && c.Events[i].SignalEvent.GetDirection() == common.MissingData {
|
||||
c.ShowMissingDataWarning = true
|
||||
}
|
||||
benchmarkRates[i] = c.Events[i].DataEvent.ClosePrice().Sub(
|
||||
c.Events[i-1].DataEvent.ClosePrice()).Div(
|
||||
c.Events[i-1].DataEvent.ClosePrice())
|
||||
}
|
||||
|
||||
// remove the first entry as its zero and impacts
|
||||
// ratio calculations as no movement has been made
|
||||
benchmarkRates = benchmarkRates[1:]
|
||||
returnPerCandle = returnPerCandle[1:]
|
||||
|
||||
var arithmeticBenchmarkAverage, geometricBenchmarkAverage decimal.Decimal
|
||||
arithmeticBenchmarkAverage, err = gctmath.DecimalArithmeticMean(benchmarkRates)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
geometricBenchmarkAverage, err = gctmath.DecimalFinancialGeometricMean(benchmarkRates)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
c.MaxDrawdown = calculateMaxDrawdown(allDataEvents)
|
||||
interval := first.DataEvent.GetInterval()
|
||||
intervalsPerYear := interval.IntervalsPerYear()
|
||||
|
||||
riskFreeRatePerCandle := first.Holdings.RiskFreeRate.Div(decimal.NewFromFloat(intervalsPerYear))
|
||||
riskFreeRateForPeriod := riskFreeRatePerCandle.Mul(decimal.NewFromInt(int64(len(benchmarkRates))))
|
||||
|
||||
var arithmeticReturnsPerCandle, geometricReturnsPerCandle, arithmeticSharpe, arithmeticSortino,
|
||||
arithmeticInformation, arithmeticCalmar, geomSharpe, geomSortino, geomInformation, geomCalmar decimal.Decimal
|
||||
|
||||
arithmeticReturnsPerCandle, err = gctmath.DecimalArithmeticMean(returnPerCandle)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
geometricReturnsPerCandle, err = gctmath.DecimalFinancialGeometricMean(returnPerCandle)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
arithmeticSharpe, err = gctmath.DecimalSharpeRatio(returnPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
arithmeticSortino, err = gctmath.DecimalSortinoRatio(returnPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
|
||||
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
|
||||
if errors.Is(err, gctmath.ErrInexactConversion) {
|
||||
log.Warnf(log.BackTester, "%v arithmetic sortino ratio %v", sep, err)
|
||||
} else {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
arithmeticInformation, err = gctmath.DecimalInformationRatio(returnPerCandle, benchmarkRates, arithmeticReturnsPerCandle, arithmeticBenchmarkAverage)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
mxhp := c.MaxDrawdown.Highest.Price
|
||||
mdlp := c.MaxDrawdown.Lowest.Price
|
||||
arithmeticCalmar, err = gctmath.DecimalCalmarRatio(mxhp, mdlp, arithmeticReturnsPerCandle, riskFreeRateForPeriod)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
c.ArithmeticRatios = Ratios{}
|
||||
if !arithmeticSharpe.IsZero() {
|
||||
c.ArithmeticRatios.SharpeRatio = arithmeticSharpe
|
||||
}
|
||||
if !arithmeticSortino.IsZero() {
|
||||
c.ArithmeticRatios.SortinoRatio = arithmeticSortino
|
||||
}
|
||||
if !arithmeticInformation.IsZero() {
|
||||
c.ArithmeticRatios.InformationRatio = arithmeticInformation
|
||||
}
|
||||
if !arithmeticCalmar.IsZero() {
|
||||
c.ArithmeticRatios.CalmarRatio = arithmeticCalmar
|
||||
}
|
||||
|
||||
geomSharpe, err = gctmath.DecimalSharpeRatio(returnPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
geomSortino, err = gctmath.DecimalSortinoRatio(returnPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
|
||||
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
|
||||
if errors.Is(err, gctmath.ErrInexactConversion) {
|
||||
log.Warnf(log.BackTester, "%v geometric sortino ratio %v", sep, err)
|
||||
} else {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
geomInformation, err = gctmath.DecimalInformationRatio(returnPerCandle, benchmarkRates, geometricReturnsPerCandle, geometricBenchmarkAverage)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
geomCalmar, err = gctmath.DecimalCalmarRatio(mxhp, mdlp, geometricReturnsPerCandle, riskFreeRateForPeriod)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
c.GeometricRatios = Ratios{}
|
||||
if !arithmeticSharpe.IsZero() {
|
||||
c.GeometricRatios.SharpeRatio = geomSharpe
|
||||
}
|
||||
if !arithmeticSortino.IsZero() {
|
||||
c.GeometricRatios.SortinoRatio = geomSortino
|
||||
}
|
||||
if !arithmeticInformation.IsZero() {
|
||||
c.GeometricRatios.InformationRatio = geomInformation
|
||||
}
|
||||
if !arithmeticCalmar.IsZero() {
|
||||
c.GeometricRatios.CalmarRatio = geomCalmar
|
||||
}
|
||||
|
||||
if last.Holdings.QuoteInitialFunds.GreaterThan(decimal.Zero) {
|
||||
cagr, err := gctmath.DecimalCompoundAnnualGrowthRate(
|
||||
last.Holdings.QuoteInitialFunds,
|
||||
last.Holdings.TotalValue,
|
||||
decimal.NewFromFloat(intervalsPerYear),
|
||||
decimal.NewFromInt(int64(len(c.Events))),
|
||||
)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if !cagr.IsZero() {
|
||||
c.CompoundAnnualGrowthRate = cagr
|
||||
}
|
||||
}
|
||||
c.IsStrategyProfitable = last.Holdings.TotalValue.GreaterThan(first.Holdings.TotalValue)
|
||||
c.DoesPerformanceBeatTheMarket = c.StrategyMovement.GreaterThan(c.MarketMovement)
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintResults outputs all calculated statistics to the command line
|
||||
func (c *CurrencyStatistic) PrintResults(e string, a asset.Item, p currency.Pair, f funding.IPairReader, usingExchangeLevelFunding bool) {
|
||||
var errs gctcommon.Errors
|
||||
sort.Slice(c.Events, func(i, j int) bool {
|
||||
return c.Events[i].DataEvent.GetTime().Before(c.Events[j].DataEvent.GetTime())
|
||||
})
|
||||
last := c.Events[len(c.Events)-1]
|
||||
first := c.Events[0]
|
||||
c.StartingClosePrice = first.DataEvent.ClosePrice()
|
||||
c.EndingClosePrice = last.DataEvent.ClosePrice()
|
||||
c.TotalOrders = c.BuyOrders + c.SellOrders
|
||||
last.Holdings.TotalValueLost = last.Holdings.TotalValueLostToSlippage.Add(last.Holdings.TotalValueLostToVolumeSizing)
|
||||
sep := fmt.Sprintf("%v %v %v |\t", e, a, p)
|
||||
currStr := fmt.Sprintf("------------------Stats for %v %v %v------------------------------------------", e, a, p)
|
||||
log.Infof(log.BackTester, currStr[:61])
|
||||
log.Infof(log.BackTester, "%s Initial base funds: %v", sep, f.BaseInitialFunds())
|
||||
log.Infof(log.BackTester, "%s Initial base quote: %v", sep, f.QuoteInitialFunds())
|
||||
log.Infof(log.BackTester, "%s Highest committed funds: %v at %v\n\n", sep, c.HighestCommittedFunds.Value.Round(8), c.HighestCommittedFunds.Time)
|
||||
|
||||
log.Infof(log.BackTester, "%s Buy orders: %d", sep, c.BuyOrders)
|
||||
log.Infof(log.BackTester, "%s Buy value: %v", sep, last.Holdings.BoughtValue.Round(8))
|
||||
log.Infof(log.BackTester, "%s Buy amount: %v %v", sep, last.Holdings.BoughtAmount.Round(8), last.Holdings.Pair.Base)
|
||||
log.Infof(log.BackTester, "%s Sell orders: %d", sep, c.SellOrders)
|
||||
log.Infof(log.BackTester, "%s Sell value: %v", sep, last.Holdings.SoldValue.Round(8))
|
||||
log.Infof(log.BackTester, "%s Sell amount: %v %v", sep, last.Holdings.SoldAmount.Round(8), last.Holdings.Pair.Base)
|
||||
log.Infof(log.BackTester, "%s Total orders: %d\n\n", sep, c.TotalOrders)
|
||||
|
||||
log.Info(log.BackTester, "------------------Max Drawdown-------------------------------")
|
||||
log.Infof(log.BackTester, "%s Highest Price of drawdown: %v", sep, c.MaxDrawdown.Highest.Price.Round(8))
|
||||
log.Infof(log.BackTester, "%s Time of highest price of drawdown: %v", sep, c.MaxDrawdown.Highest.Time)
|
||||
log.Infof(log.BackTester, "%s Lowest Price of drawdown: %v", sep, c.MaxDrawdown.Lowest.Price.Round(8))
|
||||
log.Infof(log.BackTester, "%s Time of lowest price of drawdown: %v", sep, c.MaxDrawdown.Lowest.Time)
|
||||
log.Infof(log.BackTester, "%s Calculated Drawdown: %v%%", sep, c.MaxDrawdown.DrawdownPercent.Round(2))
|
||||
log.Infof(log.BackTester, "%s Difference: %v", sep, c.MaxDrawdown.Highest.Price.Sub(c.MaxDrawdown.Lowest.Price).Round(2))
|
||||
log.Infof(log.BackTester, "%s Drawdown length: %d\n\n", sep, c.MaxDrawdown.IntervalDuration)
|
||||
|
||||
log.Info(log.BackTester, "------------------Rates-------------------------------------------------")
|
||||
log.Infof(log.BackTester, "%s Risk free rate: %v%%", sep, c.RiskFreeRate.Round(2))
|
||||
log.Infof(log.BackTester, "%s Compound Annual Growth Rate: %v\n\n", sep, c.CompoundAnnualGrowthRate.Round(2))
|
||||
|
||||
log.Info(log.BackTester, "------------------Ratios------------------------------------------------")
|
||||
if usingExchangeLevelFunding {
|
||||
log.Warnf(log.BackTester, "%s This strategy is using Exchange Level Funding. Calculation of ratios may be inaccurate\n", sep)
|
||||
}
|
||||
log.Info(log.BackTester, "------------------Arithmetic--------------------------------------------")
|
||||
if c.ShowMissingDataWarning {
|
||||
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
|
||||
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, c.ArithmeticRatios.SharpeRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, c.ArithmeticRatios.SortinoRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Information ratio: %v", sep, c.ArithmeticRatios.InformationRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Calmar ratio: %v\n\n", sep, c.ArithmeticRatios.CalmarRatio.Round(4))
|
||||
|
||||
log.Info(log.BackTester, "------------------Geometric--------------------------------------------")
|
||||
if c.ShowMissingDataWarning {
|
||||
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
|
||||
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, c.GeometricRatios.SharpeRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, c.GeometricRatios.SortinoRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Information ratio: %v", sep, c.GeometricRatios.InformationRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Calmar ratio: %v\n\n", sep, c.GeometricRatios.CalmarRatio.Round(4))
|
||||
|
||||
log.Info(log.BackTester, "------------------Results------------------------------------")
|
||||
log.Infof(log.BackTester, "%s Starting Close Price: %v", sep, c.StartingClosePrice.Round(8))
|
||||
log.Infof(log.BackTester, "%s Finishing Close Price: %v", sep, c.EndingClosePrice.Round(8))
|
||||
log.Infof(log.BackTester, "%s Lowest Close Price: %v", sep, c.LowestClosePrice.Round(8))
|
||||
log.Infof(log.BackTester, "%s Highest Close Price: %v", sep, c.HighestClosePrice.Round(8))
|
||||
|
||||
log.Infof(log.BackTester, "%s Market movement: %v%%", sep, c.MarketMovement.Round(2))
|
||||
if usingExchangeLevelFunding {
|
||||
log.Warnf(log.BackTester, "%s This strategy is using Exchange Level Funding. Calculation of strategic performance may be inaccurate", sep)
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Strategy movement: %v%%", sep, c.StrategyMovement.Round(2))
|
||||
log.Infof(log.BackTester, "%s Did it beat the market: %v", sep, c.StrategyMovement.GreaterThan(c.MarketMovement))
|
||||
|
||||
log.Infof(log.BackTester, "%s Value lost to volume sizing: %v", sep, last.Holdings.TotalValueLostToVolumeSizing.Round(2))
|
||||
log.Infof(log.BackTester, "%s Value lost to slippage: %v", sep, last.Holdings.TotalValueLostToSlippage.Round(2))
|
||||
log.Infof(log.BackTester, "%s Total Value lost: %v", sep, last.Holdings.TotalValueLost.Round(2))
|
||||
log.Infof(log.BackTester, "%s Total Fees: %v\n\n", sep, last.Holdings.TotalFees.Round(8))
|
||||
|
||||
log.Infof(log.BackTester, "%s Final funds: %v", sep, last.Holdings.QuoteSize.Round(8))
|
||||
log.Infof(log.BackTester, "%s Final holdings: %v", sep, last.Holdings.BaseSize.Round(8))
|
||||
if usingExchangeLevelFunding {
|
||||
log.Warnf(log.BackTester, "%s This strategy is using Exchange Level Funding. Calculation of holding values may be inaccurate", sep)
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Final holdings value: %v", sep, last.Holdings.BaseValue.Round(8))
|
||||
log.Infof(log.BackTester, "%s Final total value: %v\n\n", sep, last.Holdings.TotalValue.Round(8))
|
||||
if len(errs) > 0 {
|
||||
log.Info(log.BackTester, "------------------Errors-------------------------------------")
|
||||
for i := range errs {
|
||||
log.Info(log.BackTester, errs[i].Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateMaxDrawdown(closePrices []common.DataEventHandler) Swing {
|
||||
var lowestPrice, highestPrice decimal.Decimal
|
||||
var lowestTime, highestTime time.Time
|
||||
var swings []Swing
|
||||
if len(closePrices) > 0 {
|
||||
lowestPrice = closePrices[0].LowPrice()
|
||||
highestPrice = closePrices[0].HighPrice()
|
||||
lowestTime = closePrices[0].GetTime()
|
||||
highestTime = closePrices[0].GetTime()
|
||||
}
|
||||
for i := range closePrices {
|
||||
currHigh := closePrices[i].HighPrice()
|
||||
currLow := closePrices[i].LowPrice()
|
||||
currTime := closePrices[i].GetTime()
|
||||
if lowestPrice.GreaterThan(currLow) && !currLow.IsZero() {
|
||||
lowestPrice = currLow
|
||||
lowestTime = currTime
|
||||
}
|
||||
if highestPrice.LessThan(currHigh) && highestPrice.IsPositive() {
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add((time.Hour * 23) + (time.Minute * 59) + (time.Second * 59))
|
||||
}
|
||||
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[i].GetInterval(), 0)
|
||||
if err != nil {
|
||||
log.Error(log.BackTester, err)
|
||||
continue
|
||||
}
|
||||
swings = append(swings, Swing{
|
||||
Highest: Iteration{
|
||||
Time: highestTime,
|
||||
Price: highestPrice,
|
||||
},
|
||||
Lowest: Iteration{
|
||||
Time: lowestTime,
|
||||
Price: lowestPrice,
|
||||
},
|
||||
DrawdownPercent: lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100)),
|
||||
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
|
||||
})
|
||||
// reset the drawdown
|
||||
highestPrice = currHigh
|
||||
highestTime = currTime
|
||||
lowestPrice = currLow
|
||||
lowestTime = currTime
|
||||
}
|
||||
}
|
||||
if (len(swings) > 0 && swings[len(swings)-1].Lowest.Price != closePrices[len(closePrices)-1].LowPrice()) || swings == nil {
|
||||
// need to close out the final drawdown
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add((time.Hour * 23) + (time.Minute * 59) + (time.Second * 59))
|
||||
}
|
||||
intervals, err := gctkline.CalculateCandleDateRanges(highestTime, lowestTime, closePrices[0].GetInterval(), 0)
|
||||
if err != nil {
|
||||
log.Error(log.BackTester, err)
|
||||
}
|
||||
drawdownPercent := decimal.Zero
|
||||
if highestPrice.GreaterThan(decimal.Zero) {
|
||||
drawdownPercent = lowestPrice.Sub(highestPrice).Div(highestPrice).Mul(decimal.NewFromInt(100))
|
||||
}
|
||||
if lowestTime.Equal(highestTime) {
|
||||
// create distinction if the greatest drawdown occurs within the same candle
|
||||
lowestTime = lowestTime.Add((time.Hour * 23) + (time.Minute * 59) + (time.Second * 59))
|
||||
}
|
||||
swings = append(swings, Swing{
|
||||
Highest: Iteration{
|
||||
Time: highestTime,
|
||||
Price: highestPrice,
|
||||
},
|
||||
Lowest: Iteration{
|
||||
Time: lowestTime,
|
||||
Price: lowestPrice,
|
||||
},
|
||||
DrawdownPercent: drawdownPercent,
|
||||
IntervalDuration: int64(len(intervals.Ranges[0].Intervals)),
|
||||
})
|
||||
}
|
||||
|
||||
var maxDrawdown Swing
|
||||
if len(swings) > 0 {
|
||||
maxDrawdown = swings[0]
|
||||
}
|
||||
for i := range swings {
|
||||
if swings[i].DrawdownPercent.LessThan(maxDrawdown.DrawdownPercent) {
|
||||
// drawdowns are negative
|
||||
maxDrawdown = swings[i]
|
||||
}
|
||||
}
|
||||
|
||||
return maxDrawdown
|
||||
}
|
||||
|
||||
func (c *CurrencyStatistic) calculateHighestCommittedFunds() {
|
||||
for i := range c.Events {
|
||||
if c.Events[i].Holdings.BaseSize.Mul(c.Events[i].DataEvent.ClosePrice()).GreaterThan(c.HighestCommittedFunds.Value) {
|
||||
c.HighestCommittedFunds.Value = c.Events[i].Holdings.BaseSize.Mul(c.Events[i].DataEvent.ClosePrice())
|
||||
c.HighestCommittedFunds.Time = c.Events[i].Holdings.Timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package currencystatistics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
)
|
||||
|
||||
// CurrencyStats defines what is expected in order to
|
||||
// calculate statistics based on an exchange, asset type and currency pair
|
||||
type CurrencyStats interface {
|
||||
TotalEquityReturn() (decimal.Decimal, error)
|
||||
MaxDrawdown() Swing
|
||||
LongestDrawdown() Swing
|
||||
SharpeRatio(decimal.Decimal) decimal.Decimal
|
||||
SortinoRatio(decimal.Decimal) decimal.Decimal
|
||||
}
|
||||
|
||||
// EventStore is used to hold all event information
|
||||
// at a time interval
|
||||
type EventStore struct {
|
||||
Holdings holdings.Holding
|
||||
Transactions compliance.Snapshot
|
||||
DataEvent common.DataEventHandler
|
||||
SignalEvent signal.Event
|
||||
OrderEvent order.Event
|
||||
FillEvent fill.Event
|
||||
}
|
||||
|
||||
// CurrencyStatistic Holds all events and statistics relevant to an exchange, asset type and currency pair
|
||||
type CurrencyStatistic struct {
|
||||
Events []EventStore `json:"-"`
|
||||
MaxDrawdown Swing `json:"max-drawdown,omitempty"`
|
||||
StartingClosePrice decimal.Decimal `json:"starting-close-price"`
|
||||
EndingClosePrice decimal.Decimal `json:"ending-close-price"`
|
||||
LowestClosePrice decimal.Decimal `json:"lowest-close-price"`
|
||||
HighestClosePrice decimal.Decimal `json:"highest-close-price"`
|
||||
MarketMovement decimal.Decimal `json:"market-movement"`
|
||||
StrategyMovement decimal.Decimal `json:"strategy-movement"`
|
||||
HighestCommittedFunds HighestCommittedFunds `json:"highest-committed-funds"`
|
||||
RiskFreeRate decimal.Decimal `json:"risk-free-rate"`
|
||||
BuyOrders int64 `json:"buy-orders"`
|
||||
GeometricRatios Ratios `json:"geometric-ratios"`
|
||||
ArithmeticRatios Ratios `json:"arithmetic-ratios"`
|
||||
CompoundAnnualGrowthRate decimal.Decimal `json:"compound-annual-growth-rate"`
|
||||
SellOrders int64 `json:"sell-orders"`
|
||||
TotalOrders int64 `json:"total-orders"`
|
||||
InitialHoldings holdings.Holding `json:"initial-holdings-holdings"`
|
||||
FinalHoldings holdings.Holding `json:"final-holdings"`
|
||||
FinalOrders compliance.Snapshot `json:"final-orders"`
|
||||
ShowMissingDataWarning bool `json:"-"`
|
||||
IsStrategyProfitable bool `json:"is-strategy-profitable"`
|
||||
DoesPerformanceBeatTheMarket bool `json:"does-performance-beat-the-market"`
|
||||
}
|
||||
|
||||
// Ratios stores all the ratios used for statistics
|
||||
type Ratios struct {
|
||||
SharpeRatio decimal.Decimal `json:"sharpe-ratio"`
|
||||
SortinoRatio decimal.Decimal `json:"sortino-ratio"`
|
||||
InformationRatio decimal.Decimal `json:"information-ratio"`
|
||||
CalmarRatio decimal.Decimal `json:"calmar-ratio"`
|
||||
}
|
||||
|
||||
// Swing holds a drawdown
|
||||
type Swing struct {
|
||||
Highest Iteration `json:"highest"`
|
||||
Lowest Iteration `json:"lowest"`
|
||||
DrawdownPercent decimal.Decimal `json:"drawdown"`
|
||||
IntervalDuration int64
|
||||
}
|
||||
|
||||
// Iteration is an individual iteration of price at a time
|
||||
type Iteration struct {
|
||||
Time time.Time `json:"time"`
|
||||
Price decimal.Decimal `json:"price"`
|
||||
}
|
||||
|
||||
// HighestCommittedFunds is an individual iteration of price at a time
|
||||
type HighestCommittedFunds struct {
|
||||
Time time.Time `json:"time"`
|
||||
Value decimal.Decimal `json:"value"`
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
package currencystatistics
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
const testExchange = "binance"
|
||||
|
||||
func TestCalculateResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
cs := CurrencyStatistic{}
|
||||
cs := CurrencyPairStatistic{}
|
||||
tt1 := time.Now()
|
||||
tt2 := time.Now().Add(gctkline.OneDay.Duration())
|
||||
exch := testExchange
|
||||
@@ -40,7 +36,6 @@ func TestCalculateResults(t *testing.T) {
|
||||
ChangeInTotalValuePercent: decimal.NewFromFloat(0.1333),
|
||||
Timestamp: tt1,
|
||||
QuoteInitialFunds: decimal.NewFromInt(1337),
|
||||
RiskFreeRate: decimal.NewFromInt(1),
|
||||
},
|
||||
Transactions: compliance.Snapshot{
|
||||
Orders: []compliance.SnapshotOrder{
|
||||
@@ -80,7 +75,6 @@ func TestCalculateResults(t *testing.T) {
|
||||
ChangeInTotalValuePercent: decimal.NewFromFloat(0.1337),
|
||||
Timestamp: tt2,
|
||||
QuoteInitialFunds: decimal.NewFromInt(1337),
|
||||
RiskFreeRate: decimal.NewFromInt(1),
|
||||
},
|
||||
Transactions: compliance.Snapshot{
|
||||
Orders: []compliance.SnapshotOrder{
|
||||
@@ -115,19 +109,7 @@ func TestCalculateResults(t *testing.T) {
|
||||
}
|
||||
|
||||
cs.Events = append(cs.Events, ev, ev2)
|
||||
b, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(13337), decimal.Zero)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(13337), decimal.Zero)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pair, err := funding.CreatePair(b, q)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = cs.CalculateResults(pair)
|
||||
err := cs.CalculateResults(decimal.NewFromFloat(0.03))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -137,7 +119,7 @@ func TestCalculateResults(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrintResults(t *testing.T) {
|
||||
cs := CurrencyStatistic{}
|
||||
cs := CurrencyPairStatistic{}
|
||||
tt1 := time.Now()
|
||||
tt2 := time.Now().Add(gctkline.OneDay.Duration())
|
||||
exch := testExchange
|
||||
@@ -228,112 +210,12 @@ func TestPrintResults(t *testing.T) {
|
||||
}
|
||||
|
||||
cs.Events = append(cs.Events, ev, ev2)
|
||||
b, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(1), decimal.Zero)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
q, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(100), decimal.Zero)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pair, err := funding.CreatePair(b, q)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = cs.CalculateResults(pair)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
cs.PrintResults(exch, a, p, pair, true)
|
||||
}
|
||||
|
||||
func TestCalculateMaxDrawdown(t *testing.T) {
|
||||
tt1 := time.Now().Add(-gctkline.OneDay.Duration() * 7).Round(gctkline.OneDay.Duration())
|
||||
exch := testExchange
|
||||
a := asset.Spot
|
||||
p := currency.NewPair(currency.BTC, currency.USDT)
|
||||
var events []common.DataEventHandler
|
||||
for i := int64(0); i < 100; i++ {
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even := event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
if i == 50 {
|
||||
// throw in a wrench, a spike in price
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1336),
|
||||
High: decimal.NewFromInt(1336),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
})
|
||||
} else {
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1337).Sub(decimal.NewFromInt(i)),
|
||||
High: decimal.NewFromInt(1337).Sub(decimal.NewFromInt(i)),
|
||||
Low: decimal.NewFromInt(1337).Sub(decimal.NewFromInt(i)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even := event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1338),
|
||||
High: decimal.NewFromInt(1338),
|
||||
Low: decimal.NewFromInt(1338),
|
||||
})
|
||||
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even = event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1337),
|
||||
Low: decimal.NewFromInt(1337),
|
||||
})
|
||||
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even = event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1339),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1339),
|
||||
})
|
||||
|
||||
resp := calculateMaxDrawdown(events)
|
||||
if resp.Highest.Price != decimal.NewFromInt(1337) && !resp.Lowest.Price.Equal(decimal.NewFromInt(1238)) {
|
||||
t.Error("unexpected max drawdown")
|
||||
}
|
||||
cs.PrintResults(exch, a, p, true)
|
||||
}
|
||||
|
||||
func TestCalculateHighestCommittedFunds(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := CurrencyStatistic{}
|
||||
c := CurrencyPairStatistic{}
|
||||
c.calculateHighestCommittedFunds()
|
||||
if !c.HighestCommittedFunds.Time.IsZero() {
|
||||
t.Error("expected no time with not committed funds")
|
||||
306
backtester/eventhandlers/statistics/fundingstatistics.go
Normal file
306
backtester/eventhandlers/statistics/fundingstatistics.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
||||
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// CalculateFundingStatistics calculates funding statistics for total USD strategy results
|
||||
// along with individual funding item statistics
|
||||
func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic, riskFreeRate decimal.Decimal, interval gctkline.Interval) (*FundingStatistics, error) {
|
||||
if currStats == nil {
|
||||
return nil, common.ErrNilArguments
|
||||
}
|
||||
report := funds.GenerateReport()
|
||||
response := &FundingStatistics{
|
||||
Report: report,
|
||||
}
|
||||
for i := range report.Items {
|
||||
exchangeAssetStats, ok := currStats[report.Items[i].Exchange][report.Items[i].Asset]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w for %v %v",
|
||||
errNoRelevantStatsFound,
|
||||
report.Items[i].Exchange,
|
||||
report.Items[i].Asset)
|
||||
}
|
||||
var relevantStats []relatedCurrencyPairStatistics
|
||||
for k, v := range exchangeAssetStats {
|
||||
if k.Base == report.Items[i].Currency {
|
||||
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{isBaseCurrency: true, stat: v})
|
||||
continue
|
||||
}
|
||||
if k.Quote == report.Items[i].Currency {
|
||||
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{stat: v})
|
||||
}
|
||||
}
|
||||
fundingStat, err := CalculateIndividualFundingStatistics(report.DisableUSDTracking, &report.Items[i], relevantStats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.Items = append(response.Items, *fundingStat)
|
||||
}
|
||||
if report.DisableUSDTracking {
|
||||
return response, nil
|
||||
}
|
||||
usdStats := &TotalFundingStatistics{
|
||||
HighestHoldingValue: ValueAtTime{},
|
||||
LowestHoldingValue: ValueAtTime{},
|
||||
RiskFreeRate: riskFreeRate,
|
||||
}
|
||||
for i := range response.Items {
|
||||
usdStats.TotalOrders += response.Items[i].TotalOrders
|
||||
usdStats.BuyOrders += response.Items[i].BuyOrders
|
||||
usdStats.SellOrders += response.Items[i].SellOrders
|
||||
}
|
||||
for k, v := range report.USDTotalsOverTime {
|
||||
if usdStats.HighestHoldingValue.Value.LessThan(v.USDValue) {
|
||||
usdStats.HighestHoldingValue.Time = k
|
||||
usdStats.HighestHoldingValue.Value = v.USDValue
|
||||
}
|
||||
if usdStats.LowestHoldingValue.Value.IsZero() {
|
||||
usdStats.LowestHoldingValue.Time = k
|
||||
usdStats.LowestHoldingValue.Value = v.USDValue
|
||||
}
|
||||
if usdStats.LowestHoldingValue.Value.GreaterThan(v.USDValue) && !usdStats.LowestHoldingValue.Value.IsZero() {
|
||||
usdStats.LowestHoldingValue.Time = k
|
||||
usdStats.LowestHoldingValue.Value = v.USDValue
|
||||
}
|
||||
usdStats.HoldingValues = append(usdStats.HoldingValues, ValueAtTime{Time: k, Value: v.USDValue})
|
||||
}
|
||||
sort.Slice(usdStats.HoldingValues, func(i, j int) bool {
|
||||
return usdStats.HoldingValues[i].Time.Before(usdStats.HoldingValues[j].Time)
|
||||
})
|
||||
|
||||
if len(usdStats.HoldingValues) == 0 {
|
||||
return nil, fmt.Errorf("%w and holding values", errMissingSnapshots)
|
||||
}
|
||||
|
||||
if !usdStats.HoldingValues[0].Value.IsZero() {
|
||||
usdStats.StrategyMovement = usdStats.HoldingValues[len(usdStats.HoldingValues)-1].Value.Sub(
|
||||
usdStats.HoldingValues[0].Value).Div(
|
||||
usdStats.HoldingValues[0].Value).Mul(
|
||||
decimal.NewFromInt(100))
|
||||
}
|
||||
usdStats.InitialHoldingValue = usdStats.HoldingValues[0]
|
||||
usdStats.FinalHoldingValue = usdStats.HoldingValues[len(usdStats.HoldingValues)-1]
|
||||
usdStats.HoldingValueDifference = usdStats.FinalHoldingValue.Value.Sub(usdStats.InitialHoldingValue.Value).Div(usdStats.InitialHoldingValue.Value).Mul(decimal.NewFromInt(100))
|
||||
|
||||
riskFreeRatePerCandle := usdStats.RiskFreeRate.Div(decimal.NewFromFloat(interval.IntervalsPerYear()))
|
||||
returnsPerCandle := make([]decimal.Decimal, len(usdStats.HoldingValues))
|
||||
benchmarkRates := make([]decimal.Decimal, len(usdStats.HoldingValues))
|
||||
benchmarkMovement := usdStats.HoldingValues[0].Value
|
||||
benchmarkRates[0] = usdStats.HoldingValues[0].Value
|
||||
for j := range usdStats.HoldingValues {
|
||||
if j != 0 && !usdStats.HoldingValues[j-1].Value.IsZero() {
|
||||
benchmarkMovement = benchmarkMovement.Add(benchmarkMovement.Mul(riskFreeRatePerCandle))
|
||||
benchmarkRates[j] = riskFreeRatePerCandle
|
||||
returnsPerCandle[j] = usdStats.HoldingValues[j].Value.Sub(usdStats.HoldingValues[j-1].Value).Div(usdStats.HoldingValues[j-1].Value)
|
||||
}
|
||||
}
|
||||
benchmarkRates = benchmarkRates[1:]
|
||||
returnsPerCandle = returnsPerCandle[1:]
|
||||
usdStats.BenchmarkMarketMovement = benchmarkMovement.Sub(usdStats.HoldingValues[0].Value).Div(usdStats.HoldingValues[0].Value).Mul(decimal.NewFromInt(100))
|
||||
var err error
|
||||
usdStats.MaxDrawdown, err = CalculateBiggestValueAtTimeDrawdown(usdStats.HoldingValues, interval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sep := "USD Totals |\t"
|
||||
usdStats.ArithmeticRatios, usdStats.GeometricRatios, err = CalculateRatios(benchmarkRates, returnsPerCandle, riskFreeRatePerCandle, &usdStats.MaxDrawdown, sep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !usdStats.HoldingValues[0].Value.IsZero() {
|
||||
cagr, err := gctmath.DecimalCompoundAnnualGrowthRate(
|
||||
usdStats.HoldingValues[0].Value,
|
||||
usdStats.HoldingValues[len(usdStats.HoldingValues)-1].Value,
|
||||
decimal.NewFromFloat(interval.IntervalsPerYear()),
|
||||
decimal.NewFromInt(int64(len(usdStats.HoldingValues))),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !cagr.IsZero() {
|
||||
usdStats.CompoundAnnualGrowthRate = cagr
|
||||
}
|
||||
}
|
||||
usdStats.DidStrategyMakeProfit = usdStats.HoldingValues[len(usdStats.HoldingValues)-1].Value.GreaterThan(usdStats.HoldingValues[0].Value)
|
||||
usdStats.DidStrategyBeatTheMarket = usdStats.StrategyMovement.GreaterThan(usdStats.BenchmarkMarketMovement)
|
||||
response.TotalUSDStatistics = usdStats
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// CalculateIndividualFundingStatistics calculates statistics for an individual report item
|
||||
func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *funding.ReportItem, relatedStats []relatedCurrencyPairStatistics) (*FundingItemStatistics, error) {
|
||||
if reportItem == nil {
|
||||
return nil, fmt.Errorf("%w - nil report item", common.ErrNilArguments)
|
||||
}
|
||||
item := &FundingItemStatistics{
|
||||
ReportItem: reportItem,
|
||||
}
|
||||
if disableUSDTracking {
|
||||
return item, nil
|
||||
}
|
||||
closePrices := reportItem.Snapshots
|
||||
if len(closePrices) == 0 {
|
||||
return nil, errMissingSnapshots
|
||||
}
|
||||
item.StartingClosePrice = ValueAtTime{
|
||||
Time: closePrices[0].Time,
|
||||
Value: closePrices[0].USDClosePrice,
|
||||
}
|
||||
item.EndingClosePrice = ValueAtTime{
|
||||
Time: closePrices[len(closePrices)-1].Time,
|
||||
Value: closePrices[len(closePrices)-1].USDClosePrice,
|
||||
}
|
||||
for i := range closePrices {
|
||||
if closePrices[i].USDClosePrice.LessThan(item.LowestClosePrice.Value) || item.LowestClosePrice.Value.IsZero() {
|
||||
item.LowestClosePrice.Value = closePrices[i].USDClosePrice
|
||||
item.LowestClosePrice.Time = closePrices[i].Time
|
||||
}
|
||||
if closePrices[i].USDClosePrice.GreaterThan(item.HighestClosePrice.Value) || item.HighestClosePrice.Value.IsZero() {
|
||||
item.HighestClosePrice.Value = closePrices[i].USDClosePrice
|
||||
item.HighestClosePrice.Time = closePrices[i].Time
|
||||
}
|
||||
}
|
||||
|
||||
for i := range relatedStats {
|
||||
if relatedStats[i].stat == nil {
|
||||
return nil, fmt.Errorf("%w related stats", common.ErrNilArguments)
|
||||
}
|
||||
if relatedStats[i].isBaseCurrency {
|
||||
item.BuyOrders += relatedStats[i].stat.BuyOrders
|
||||
item.SellOrders += relatedStats[i].stat.SellOrders
|
||||
}
|
||||
}
|
||||
item.TotalOrders = item.BuyOrders + item.SellOrders
|
||||
if !item.ReportItem.ShowInfinite {
|
||||
if item.ReportItem.Snapshots[0].USDValue.IsZero() {
|
||||
item.ReportItem.ShowInfinite = true
|
||||
} else {
|
||||
item.StrategyMovement = item.ReportItem.Snapshots[len(item.ReportItem.Snapshots)-1].USDValue.Sub(
|
||||
item.ReportItem.Snapshots[0].USDValue).Div(
|
||||
item.ReportItem.Snapshots[0].USDValue).Mul(
|
||||
decimal.NewFromInt(100))
|
||||
}
|
||||
}
|
||||
|
||||
if !item.ReportItem.Snapshots[0].USDClosePrice.IsZero() {
|
||||
item.MarketMovement = item.ReportItem.Snapshots[len(item.ReportItem.Snapshots)-1].USDClosePrice.Sub(
|
||||
item.ReportItem.Snapshots[0].USDClosePrice).Div(
|
||||
item.ReportItem.Snapshots[0].USDClosePrice).Mul(
|
||||
decimal.NewFromInt(100))
|
||||
}
|
||||
item.DidStrategyBeatTheMarket = item.StrategyMovement.GreaterThan(item.MarketMovement)
|
||||
item.HighestCommittedFunds = ValueAtTime{}
|
||||
for j := range item.ReportItem.Snapshots {
|
||||
if item.ReportItem.Snapshots[j].USDValue.GreaterThan(item.HighestCommittedFunds.Value) {
|
||||
item.HighestCommittedFunds = ValueAtTime{
|
||||
Time: item.ReportItem.Snapshots[j].Time,
|
||||
Value: item.ReportItem.Snapshots[j].USDValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
if item.ReportItem.USDPairCandle == nil {
|
||||
return nil, fmt.Errorf("%w usd candles missing", errMissingSnapshots)
|
||||
}
|
||||
s := item.ReportItem.USDPairCandle.GetStream()
|
||||
if len(s) == 0 {
|
||||
return nil, fmt.Errorf("%w stream missing", errMissingSnapshots)
|
||||
}
|
||||
var err error
|
||||
item.MaxDrawdown, err = CalculateBiggestEventDrawdown(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// PrintResults outputs all calculated funding statistics to the command line
|
||||
func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
|
||||
if f.Report == nil {
|
||||
return fmt.Errorf("%w requires report to be generated", common.ErrNilArguments)
|
||||
}
|
||||
log.Info(log.BackTester, "------------------Funding------------------------------------")
|
||||
log.Info(log.BackTester, "------------------Funding Item Results-----------------------")
|
||||
for i := range f.Report.Items {
|
||||
sep := fmt.Sprintf("%v %v %v |\t", f.Report.Items[i].Exchange, f.Report.Items[i].Asset, f.Report.Items[i].Currency)
|
||||
if !f.Report.Items[i].PairedWith.IsEmpty() {
|
||||
log.Infof(log.BackTester, "%s Paired with: %v", sep, f.Report.Items[i].PairedWith)
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Initial funds: %s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].InitialFunds, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Final funds: %s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].FinalFunds, 8, ".", ","))
|
||||
if !f.Report.DisableUSDTracking && f.Report.UsingExchangeLevelFunding {
|
||||
log.Infof(log.BackTester, "%s Initial funds in USD: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].USDInitialFunds, 2, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Final funds in USD: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].USDFinalFunds, 2, ".", ","))
|
||||
}
|
||||
if f.Report.Items[i].ShowInfinite {
|
||||
log.Infof(log.BackTester, "%s Difference: ∞%%", sep)
|
||||
} else {
|
||||
log.Infof(log.BackTester, "%s Difference: %s%%", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].Difference, 8, ".", ","))
|
||||
}
|
||||
if f.Report.Items[i].TransferFee.GreaterThan(decimal.Zero) {
|
||||
log.Infof(log.BackTester, "%s Transfer fee: %s", sep, convert.DecimalToHumanFriendlyString(f.Report.Items[i].TransferFee, 8, ".", ","))
|
||||
}
|
||||
log.Info(log.BackTester, "")
|
||||
}
|
||||
if f.Report.DisableUSDTracking {
|
||||
return nil
|
||||
}
|
||||
log.Info(log.BackTester, "------------------USD Tracking Totals------------------------")
|
||||
sep := "USD Tracking Total |\t"
|
||||
|
||||
log.Infof(log.BackTester, "%s Initial value: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.InitialHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.InitialHoldingValue.Time)
|
||||
log.Infof(log.BackTester, "%s Final value: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.FinalHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.FinalHoldingValue.Time)
|
||||
log.Infof(log.BackTester, "%s Benchmark Market Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.BenchmarkMarketMovement, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Strategy Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.StrategyMovement, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Did strategy make a profit: %v", sep, f.TotalUSDStatistics.DidStrategyMakeProfit)
|
||||
log.Infof(log.BackTester, "%s Did strategy beat the benchmark: %v", sep, f.TotalUSDStatistics.DidStrategyBeatTheMarket)
|
||||
log.Infof(log.BackTester, "%s Buy Orders: %s", sep, convert.IntToHumanFriendlyString(f.TotalUSDStatistics.BuyOrders, ","))
|
||||
log.Infof(log.BackTester, "%s Sell Orders: %s", sep, convert.IntToHumanFriendlyString(f.TotalUSDStatistics.SellOrders, ","))
|
||||
log.Infof(log.BackTester, "%s Total Orders: %s", sep, convert.IntToHumanFriendlyString(f.TotalUSDStatistics.TotalOrders, ","))
|
||||
log.Infof(log.BackTester, "%s Highest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.HighestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.HighestHoldingValue.Time)
|
||||
log.Infof(log.BackTester, "%s Lowest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.LowestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.LowestHoldingValue.Time)
|
||||
|
||||
log.Info(log.BackTester, "------------------Ratios------------------------------------------------")
|
||||
log.Info(log.BackTester, "------------------Rates-------------------------------------------------")
|
||||
log.Infof(log.BackTester, "%s Risk free rate: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.RiskFreeRate.Mul(decimal.NewFromInt(100)), 2, ".", ","))
|
||||
log.Infof(log.BackTester, "%s Compound Annual Growth Rate: %v%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.CompoundAnnualGrowthRate, 8, ".", ","))
|
||||
if f.TotalUSDStatistics.ArithmeticRatios == nil || f.TotalUSDStatistics.GeometricRatios == nil {
|
||||
return fmt.Errorf("%w missing ratio calculations", common.ErrNilArguments)
|
||||
}
|
||||
log.Info(log.BackTester, "------------------Arithmetic--------------------------------------------")
|
||||
if wasAnyDataMissing {
|
||||
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
|
||||
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.SharpeRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.SortinoRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Information ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.InformationRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Calmar ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.CalmarRatio.Round(4))
|
||||
|
||||
log.Info(log.BackTester, "------------------Geometric--------------------------------------------")
|
||||
if wasAnyDataMissing {
|
||||
log.Infoln(log.BackTester, "Missing data was detected during this backtesting run")
|
||||
log.Infoln(log.BackTester, "Ratio calculations will be skewed")
|
||||
}
|
||||
log.Infof(log.BackTester, "%s Sharpe ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.SharpeRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Sortino ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.SortinoRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Information ratio: %v", sep, f.TotalUSDStatistics.GeometricRatios.InformationRatio.Round(4))
|
||||
log.Infof(log.BackTester, "%s Calmar ratio: %v\n\n", sep, f.TotalUSDStatistics.GeometricRatios.CalmarRatio.Round(4))
|
||||
|
||||
return nil
|
||||
}
|
||||
224
backtester/eventhandlers/statistics/fundingstatistics_test.go
Normal file
224
backtester/eventhandlers/statistics/fundingstatistics_test.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package statistics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
)
|
||||
|
||||
func TestCalculateFundingStatistics(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := CalculateFundingStatistics(nil, nil, decimal.Zero, gctkline.OneHour)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
|
||||
}
|
||||
f := funding.SetupFundingManager(true, true)
|
||||
item, err := funding.CreateItem("binance", asset.Spot, currency.BTC, decimal.NewFromInt(1337), decimal.Zero)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
err = f.AddItem(item)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
|
||||
item2, err := funding.CreateItem("binance", asset.Spot, currency.USD, decimal.NewFromInt(1337), decimal.Zero)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
err = f.AddItem(item2)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
|
||||
_, err = CalculateFundingStatistics(f, nil, decimal.Zero, gctkline.OneHour)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
usdKline := gctkline.Item{
|
||||
Exchange: "binance",
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Asset: asset.Spot,
|
||||
Interval: gctkline.OneHour,
|
||||
Candles: []gctkline.Candle{
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour),
|
||||
},
|
||||
{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
dfk := &kline.DataFromKline{
|
||||
Item: usdKline,
|
||||
}
|
||||
err = dfk.Load()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
err = f.AddUSDTrackingData(dfk)
|
||||
if !errors.Is(err, funding.ErrUSDTrackingDisabled) {
|
||||
t.Errorf("received %v expected %v", err, funding.ErrUSDTrackingDisabled)
|
||||
}
|
||||
|
||||
cs := make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
_, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour)
|
||||
if !errors.Is(err, errNoRelevantStatsFound) {
|
||||
t.Errorf("received %v expected %v", err, errNoRelevantStatsFound)
|
||||
}
|
||||
|
||||
f = funding.SetupFundingManager(true, false)
|
||||
err = f.AddItem(item)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
err = f.AddItem(item2)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
err = f.AddUSDTrackingData(dfk)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
cs["binance"] = make(map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
cs["binance"][asset.Spot] = make(map[currency.Pair]*CurrencyPairStatistic)
|
||||
cs["binance"][asset.Spot][currency.NewPair(currency.LTC, currency.USD)] = &CurrencyPairStatistic{}
|
||||
_, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour)
|
||||
if !errors.Is(err, errMissingSnapshots) {
|
||||
t.Errorf("received %v expected %v", err, errMissingSnapshots)
|
||||
}
|
||||
f.CreateSnapshot(usdKline.Candles[0].Time)
|
||||
f.CreateSnapshot(usdKline.Candles[1].Time)
|
||||
cs["binance"][asset.Spot][currency.NewPair(currency.BTC, currency.USDT)] = &CurrencyPairStatistic{}
|
||||
|
||||
_, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateIndividualFundingStatistics(t *testing.T) {
|
||||
_, err := CalculateIndividualFundingStatistics(true, nil, nil)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
_, err = CalculateIndividualFundingStatistics(true, &funding.ReportItem{}, nil)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
|
||||
_, err = CalculateIndividualFundingStatistics(false, &funding.ReportItem{}, nil)
|
||||
if !errors.Is(err, errMissingSnapshots) {
|
||||
t.Errorf("received %v expected %v", err, errMissingSnapshots)
|
||||
}
|
||||
|
||||
ri := &funding.ReportItem{
|
||||
Snapshots: []funding.ItemSnapshot{
|
||||
{
|
||||
USDValue: decimal.NewFromInt(1337),
|
||||
},
|
||||
{
|
||||
USDValue: decimal.Zero,
|
||||
},
|
||||
},
|
||||
}
|
||||
rs := []relatedCurrencyPairStatistics{
|
||||
{
|
||||
isBaseCurrency: false,
|
||||
stat: nil,
|
||||
},
|
||||
{
|
||||
isBaseCurrency: true,
|
||||
stat: &CurrencyPairStatistic{},
|
||||
},
|
||||
}
|
||||
_, err = CalculateIndividualFundingStatistics(false, ri, rs)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
rs[0].stat = &CurrencyPairStatistic{}
|
||||
_, err = CalculateIndividualFundingStatistics(false, ri, rs)
|
||||
if !errors.Is(err, errMissingSnapshots) {
|
||||
t.Errorf("received %v expected %v", err, errMissingSnapshots)
|
||||
}
|
||||
|
||||
ri.USDPairCandle = &kline.DataFromKline{
|
||||
Item: gctkline.Item{
|
||||
Interval: gctkline.OneHour,
|
||||
Candles: []gctkline.Candle{
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour),
|
||||
},
|
||||
{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = ri.USDPairCandle.Load()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
_, err = CalculateIndividualFundingStatistics(false, ri, rs)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFundingStatisticsPrintResults(t *testing.T) {
|
||||
f := FundingStatistics{}
|
||||
err := f.PrintResults(false)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
funds := funding.SetupFundingManager(true, true)
|
||||
item1, err := funding.CreateItem("test", asset.Spot, currency.BTC, decimal.NewFromInt(1337), decimal.NewFromFloat(0.04))
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
item2, err := funding.CreateItem("test", asset.Spot, currency.LTC, decimal.NewFromInt(1337), decimal.NewFromFloat(0.04))
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
p, err := funding.CreatePair(item1, item2)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
err = funds.AddPair(p)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
f.Report = funds.GenerateReport()
|
||||
err = f.PrintResults(false)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
|
||||
f.TotalUSDStatistics = &TotalFundingStatistics{}
|
||||
f.Report.DisableUSDTracking = false
|
||||
err = f.PrintResults(false)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
f.TotalUSDStatistics = &TotalFundingStatistics{
|
||||
GeometricRatios: &Ratios{},
|
||||
ArithmeticRatios: &Ratios{},
|
||||
}
|
||||
err = f.PrintResults(true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package statistics
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
@@ -10,12 +11,12 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics/currencystatistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
||||
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -37,7 +38,7 @@ func (s *Statistic) SetupEventForTime(ev common.DataEventHandler) error {
|
||||
s.setupMap(ex, a)
|
||||
lookup := s.ExchangeAssetPairStatistics[ex][a][p]
|
||||
if lookup == nil {
|
||||
lookup = ¤cystatistics.CurrencyStatistic{}
|
||||
lookup = &CurrencyPairStatistic{}
|
||||
}
|
||||
for i := range lookup.Events {
|
||||
if lookup.Events[i].DataEvent.GetTime().Equal(ev.GetTime()) &&
|
||||
@@ -49,7 +50,7 @@ func (s *Statistic) SetupEventForTime(ev common.DataEventHandler) error {
|
||||
}
|
||||
}
|
||||
lookup.Events = append(lookup.Events,
|
||||
currencystatistics.EventStore{
|
||||
EventStore{
|
||||
DataEvent: ev,
|
||||
},
|
||||
)
|
||||
@@ -60,13 +61,13 @@ func (s *Statistic) SetupEventForTime(ev common.DataEventHandler) error {
|
||||
|
||||
func (s *Statistic) setupMap(ex string, a asset.Item) {
|
||||
if s.ExchangeAssetPairStatistics == nil {
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
}
|
||||
if s.ExchangeAssetPairStatistics[ex] == nil {
|
||||
s.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics[ex] = make(map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
}
|
||||
if s.ExchangeAssetPairStatistics[ex][a] == nil {
|
||||
s.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*CurrencyPairStatistic)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ func (s *Statistic) SetEventForOffset(ev common.EventHandler) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyEventAtOffset(ev common.EventHandler, lookup *currencystatistics.CurrencyStatistic, i int) error {
|
||||
func applyEventAtOffset(ev common.EventHandler, lookup *CurrencyPairStatistic, i int) error {
|
||||
switch t := ev.(type) {
|
||||
case common.DataEventHandler:
|
||||
lookup.Events[i].DataEvent = t
|
||||
@@ -156,43 +157,27 @@ func (s *Statistic) AddComplianceSnapshotForTime(c compliance.Snapshot, e fill.E
|
||||
|
||||
// CalculateAllResults calculates the statistics of all exchange asset pair holdings,
|
||||
// orders, ratios and drawdowns
|
||||
func (s *Statistic) CalculateAllResults(funds funding.IFundingManager) error {
|
||||
func (s *Statistic) CalculateAllResults() error {
|
||||
log.Info(log.BackTester, "calculating backtesting results")
|
||||
s.PrintAllEventsChronologically()
|
||||
currCount := 0
|
||||
var finalResults []FinalResultsHolder
|
||||
var err error
|
||||
var startDate, endDate time.Time
|
||||
for exchangeName, exchangeMap := range s.ExchangeAssetPairStatistics {
|
||||
for assetItem, assetMap := range exchangeMap {
|
||||
for pair, stats := range assetMap {
|
||||
currCount++
|
||||
var f funding.IPairReader
|
||||
last := stats.Events[len(stats.Events)-1]
|
||||
startDate = stats.Events[0].DataEvent.GetTime()
|
||||
endDate = last.DataEvent.GetTime()
|
||||
var event common.EventHandler
|
||||
switch {
|
||||
case last.FillEvent != nil:
|
||||
event = last.FillEvent
|
||||
case last.SignalEvent != nil:
|
||||
event = last.SignalEvent
|
||||
default:
|
||||
event = last.DataEvent
|
||||
}
|
||||
f, err = funds.GetFundingForEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = stats.CalculateResults(f)
|
||||
err = stats.CalculateResults(s.RiskFreeRate)
|
||||
if err != nil {
|
||||
log.Error(log.BackTester, err)
|
||||
}
|
||||
stats.PrintResults(exchangeName, assetItem, pair, f, funds.IsUsingExchangeLevelFunding())
|
||||
stats.PrintResults(exchangeName, assetItem, pair, s.FundManager.IsUsingExchangeLevelFunding())
|
||||
stats.FinalHoldings = last.Holdings
|
||||
stats.InitialHoldings = stats.Events[0].Holdings
|
||||
stats.FinalOrders = last.Transactions
|
||||
s.AllStats = append(s.AllStats, *stats)
|
||||
s.StartDate = stats.Events[0].DataEvent.GetTime()
|
||||
s.EndDate = last.DataEvent.GetTime()
|
||||
|
||||
finalResults = append(finalResults, FinalResultsHolder{
|
||||
Exchange: exchangeName,
|
||||
@@ -210,71 +195,54 @@ func (s *Statistic) CalculateAllResults(funds funding.IFundingManager) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
s.Funding = funds.GenerateReport(startDate, endDate)
|
||||
s.FundingStatistics, err = CalculateFundingStatistics(s.FundManager, s.ExchangeAssetPairStatistics, s.RiskFreeRate, s.CandleInterval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.FundingStatistics.PrintResults(s.WasAnyDataMissing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.TotalOrders = s.TotalBuyOrders + s.TotalSellOrders
|
||||
if currCount > 1 {
|
||||
s.BiggestDrawdown = s.GetTheBiggestDrawdownAcrossCurrencies(finalResults)
|
||||
s.BestMarketMovement = s.GetBestMarketPerformer(finalResults)
|
||||
s.BestStrategyResults = s.GetBestStrategyPerformer(finalResults)
|
||||
s.PrintTotalResults(funds.IsUsingExchangeLevelFunding())
|
||||
s.PrintTotalResults()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintTotalResults outputs all results to the CMD
|
||||
func (s *Statistic) PrintTotalResults(isUsingExchangeLevelFunding bool) {
|
||||
func (s *Statistic) PrintTotalResults() {
|
||||
log.Info(log.BackTester, "------------------Strategy-----------------------------------")
|
||||
log.Infof(log.BackTester, "Strategy Name: %v", s.StrategyName)
|
||||
log.Infof(log.BackTester, "Strategy Nickname: %v", s.StrategyNickname)
|
||||
log.Infof(log.BackTester, "Strategy Goal: %v\n\n", s.StrategyGoal)
|
||||
log.Info(log.BackTester, "------------------Funding------------------------------------")
|
||||
for i := range s.Funding.Items {
|
||||
log.Infof(log.BackTester, "Exchange: %v", s.Funding.Items[i].Exchange)
|
||||
log.Infof(log.BackTester, "Asset: %v", s.Funding.Items[i].Asset)
|
||||
log.Infof(log.BackTester, "Currency: %v", s.Funding.Items[i].Currency)
|
||||
if !s.Funding.Items[i].PairedWith.IsEmpty() {
|
||||
log.Infof(log.BackTester, "Paired with: %v", s.Funding.Items[i].PairedWith)
|
||||
}
|
||||
log.Infof(log.BackTester, "Initial funds: %v", s.Funding.Items[i].InitialFunds)
|
||||
log.Infof(log.BackTester, "Initial funds in USD: $%v", s.Funding.Items[i].InitialFundsUSD)
|
||||
log.Infof(log.BackTester, "Final funds: %v", s.Funding.Items[i].FinalFunds)
|
||||
log.Infof(log.BackTester, "Final funds in USD: $%v", s.Funding.Items[i].FinalFundsUSD)
|
||||
if s.Funding.Items[i].InitialFunds.IsZero() {
|
||||
log.Info(log.BackTester, "Difference: ∞%")
|
||||
} else {
|
||||
log.Infof(log.BackTester, "Difference: %v%%", s.Funding.Items[i].Difference)
|
||||
}
|
||||
if s.Funding.Items[i].TransferFee.GreaterThan(decimal.Zero) {
|
||||
log.Infof(log.BackTester, "Transfer fee: %v", s.Funding.Items[i].TransferFee)
|
||||
}
|
||||
log.Info(log.BackTester, "")
|
||||
}
|
||||
log.Infof(log.BackTester, "Initial total funds in USD: $%v", s.Funding.InitialTotalUSD)
|
||||
log.Infof(log.BackTester, "Final total funds in USD: $%v", s.Funding.FinalTotalUSD)
|
||||
log.Infof(log.BackTester, "Difference: %v%%\n", s.Funding.Difference)
|
||||
|
||||
log.Info(log.BackTester, "------------------Total Results------------------------------")
|
||||
log.Info(log.BackTester, "------------------Orders-------------------------------------")
|
||||
log.Infof(log.BackTester, "Total buy orders: %v", s.TotalBuyOrders)
|
||||
log.Infof(log.BackTester, "Total sell orders: %v", s.TotalSellOrders)
|
||||
log.Infof(log.BackTester, "Total orders: %v\n\n", s.TotalOrders)
|
||||
log.Infof(log.BackTester, "Total buy orders: %v", convert.IntToHumanFriendlyString(s.TotalBuyOrders, ","))
|
||||
log.Infof(log.BackTester, "Total sell orders: %v", convert.IntToHumanFriendlyString(s.TotalSellOrders, ","))
|
||||
log.Infof(log.BackTester, "Total orders: %v\n\n", convert.IntToHumanFriendlyString(s.TotalOrders, ","))
|
||||
|
||||
if s.BiggestDrawdown != nil {
|
||||
log.Info(log.BackTester, "------------------Biggest Drawdown-----------------------")
|
||||
log.Infof(log.BackTester, "Exchange: %v Asset: %v Currency: %v", s.BiggestDrawdown.Exchange, s.BiggestDrawdown.Asset, s.BiggestDrawdown.Pair)
|
||||
log.Infof(log.BackTester, "Highest Price: %v", s.BiggestDrawdown.MaxDrawdown.Highest.Price.Round(8))
|
||||
log.Infof(log.BackTester, "Highest Price: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Highest.Value, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "Highest Price Time: %v", s.BiggestDrawdown.MaxDrawdown.Highest.Time)
|
||||
log.Infof(log.BackTester, "Lowest Price: %v", s.BiggestDrawdown.MaxDrawdown.Lowest.Price.Round(8))
|
||||
log.Infof(log.BackTester, "Lowest Price: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Lowest.Value, 8, ".", ","))
|
||||
log.Infof(log.BackTester, "Lowest Price Time: %v", s.BiggestDrawdown.MaxDrawdown.Lowest.Time)
|
||||
log.Infof(log.BackTester, "Calculated Drawdown: %v%%", s.BiggestDrawdown.MaxDrawdown.DrawdownPercent.Round(2))
|
||||
log.Infof(log.BackTester, "Difference: %v", s.BiggestDrawdown.MaxDrawdown.Highest.Price.Sub(s.BiggestDrawdown.MaxDrawdown.Lowest.Price).Round(8))
|
||||
log.Infof(log.BackTester, "Drawdown length: %v\n\n", s.BiggestDrawdown.MaxDrawdown.IntervalDuration)
|
||||
log.Infof(log.BackTester, "Calculated Drawdown: %s%%", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.DrawdownPercent, 2, ".", ","))
|
||||
log.Infof(log.BackTester, "Difference: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Highest.Value.Sub(s.BiggestDrawdown.MaxDrawdown.Lowest.Value), 8, ".", ","))
|
||||
log.Infof(log.BackTester, "Drawdown length: %v\n\n", convert.IntToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.IntervalDuration, ","))
|
||||
}
|
||||
if s.BestMarketMovement != nil && s.BestStrategyResults != nil {
|
||||
log.Info(log.BackTester, "------------------Orders----------------------------------")
|
||||
log.Infof(log.BackTester, "Best performing market movement: %v %v %v %v%%", s.BestMarketMovement.Exchange, s.BestMarketMovement.Asset, s.BestMarketMovement.Pair, s.BestMarketMovement.MarketMovement.Round(2))
|
||||
log.Infof(log.BackTester, "Best performing strategy movement: %v %v %v %v%%\n\n", s.BestStrategyResults.Exchange, s.BestStrategyResults.Asset, s.BestStrategyResults.Pair, s.BestStrategyResults.StrategyMovement.Round(2))
|
||||
log.Infof(log.BackTester, "Best performing market movement: %v %v %v %v%%", s.BestMarketMovement.Exchange, s.BestMarketMovement.Asset, s.BestMarketMovement.Pair, convert.DecimalToHumanFriendlyString(s.BestMarketMovement.MarketMovement, 2, ".", ","))
|
||||
log.Infof(log.BackTester, "Best performing strategy movement: %v %v %v %v%%\n\n", s.BestStrategyResults.Exchange, s.BestStrategyResults.Asset, s.BestStrategyResults.Pair, convert.DecimalToHumanFriendlyString(s.BestStrategyResults.StrategyMovement, 2, ".", ","))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +377,7 @@ func (s *Statistic) PrintAllEventsChronologically() {
|
||||
if len(errs) > 0 {
|
||||
log.Info(log.BackTester, "------------------Errors-------------------------------------")
|
||||
for i := range errs {
|
||||
log.Info(log.BackTester, errs[i].Error())
|
||||
log.Error(log.BackTester, errs[i].Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,3 +396,101 @@ func (s *Statistic) Serialise() (string, error) {
|
||||
|
||||
return string(resp), nil
|
||||
}
|
||||
|
||||
// CalculateRatios creates arithmetic and geometric ratios from funding or currency pair data
|
||||
func CalculateRatios(benchmarkRates, returnsPerCandle []decimal.Decimal, riskFreeRatePerCandle decimal.Decimal, maxDrawdown *Swing, logMessage string) (arithmeticStats, geometricStats *Ratios, err error) {
|
||||
var arithmeticBenchmarkAverage, geometricBenchmarkAverage decimal.Decimal
|
||||
arithmeticBenchmarkAverage, err = gctmath.DecimalArithmeticMean(benchmarkRates)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
geometricBenchmarkAverage, err = gctmath.DecimalFinancialGeometricMean(benchmarkRates)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
riskFreeRateForPeriod := riskFreeRatePerCandle.Mul(decimal.NewFromInt(int64(len(benchmarkRates))))
|
||||
|
||||
var arithmeticReturnsPerCandle, geometricReturnsPerCandle, arithmeticSharpe, arithmeticSortino,
|
||||
arithmeticInformation, arithmeticCalmar, geomSharpe, geomSortino, geomInformation, geomCalmar decimal.Decimal
|
||||
|
||||
arithmeticReturnsPerCandle, err = gctmath.DecimalArithmeticMean(returnsPerCandle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
geometricReturnsPerCandle, err = gctmath.DecimalFinancialGeometricMean(returnsPerCandle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
arithmeticSharpe, err = gctmath.DecimalSharpeRatio(returnsPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
arithmeticSortino, err = gctmath.DecimalSortinoRatio(returnsPerCandle, riskFreeRatePerCandle, arithmeticReturnsPerCandle)
|
||||
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
|
||||
if errors.Is(err, gctmath.ErrInexactConversion) {
|
||||
log.Warnf(log.BackTester, "%s funding arithmetic sortino ratio %v", logMessage, err)
|
||||
} else {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
arithmeticInformation, err = gctmath.DecimalInformationRatio(returnsPerCandle, benchmarkRates, arithmeticReturnsPerCandle, arithmeticBenchmarkAverage)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
arithmeticCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, arithmeticReturnsPerCandle, riskFreeRateForPeriod)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
arithmeticStats = &Ratios{}
|
||||
if !arithmeticSharpe.IsZero() {
|
||||
arithmeticStats.SharpeRatio = arithmeticSharpe
|
||||
}
|
||||
if !arithmeticSortino.IsZero() {
|
||||
arithmeticStats.SortinoRatio = arithmeticSortino
|
||||
}
|
||||
if !arithmeticInformation.IsZero() {
|
||||
arithmeticStats.InformationRatio = arithmeticInformation
|
||||
}
|
||||
if !arithmeticCalmar.IsZero() {
|
||||
arithmeticStats.CalmarRatio = arithmeticCalmar
|
||||
}
|
||||
|
||||
geomSharpe, err = gctmath.DecimalSharpeRatio(returnsPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
geomSortino, err = gctmath.DecimalSortinoRatio(returnsPerCandle, riskFreeRatePerCandle, geometricReturnsPerCandle)
|
||||
if err != nil && !errors.Is(err, gctmath.ErrNoNegativeResults) {
|
||||
if errors.Is(err, gctmath.ErrInexactConversion) {
|
||||
log.Warnf(log.BackTester, "%s geometric sortino ratio %v", logMessage, err)
|
||||
} else {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
geomInformation, err = gctmath.DecimalInformationRatio(returnsPerCandle, benchmarkRates, geometricReturnsPerCandle, geometricBenchmarkAverage)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
geomCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, geometricReturnsPerCandle, riskFreeRateForPeriod)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
geometricStats = &Ratios{}
|
||||
if !arithmeticSharpe.IsZero() {
|
||||
geometricStats.SharpeRatio = geomSharpe
|
||||
}
|
||||
if !arithmeticSortino.IsZero() {
|
||||
geometricStats.SortinoRatio = geomSortino
|
||||
}
|
||||
if !arithmeticInformation.IsZero() {
|
||||
geometricStats.InformationRatio = geomInformation
|
||||
}
|
||||
if !arithmeticCalmar.IsZero() {
|
||||
geometricStats.CalmarRatio = geomCalmar
|
||||
}
|
||||
|
||||
return arithmeticStats, geometricStats, nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics/currencystatistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
|
||||
@@ -94,7 +93,7 @@ func TestAddSignalEventForTime(t *testing.T) {
|
||||
t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset)
|
||||
}
|
||||
s.setupMap(exch, a)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
err = s.SetEventForOffset(&signal.Signal{})
|
||||
if !errors.Is(err, errCurrencyStatisticsUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
|
||||
@@ -149,7 +148,7 @@ func TestAddExchangeEventForTime(t *testing.T) {
|
||||
t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset)
|
||||
}
|
||||
s.setupMap(exch, a)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
err = s.SetEventForOffset(&order.Order{})
|
||||
if !errors.Is(err, errCurrencyStatisticsUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
|
||||
@@ -209,7 +208,7 @@ func TestAddFillEventForTime(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
s.setupMap(exch, a)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
err = s.SetEventForOffset(&fill.Fill{})
|
||||
if !errors.Is(err, errCurrencyStatisticsUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
|
||||
@@ -264,7 +263,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.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
err = s.AddHoldingsForTime(&holdings.Holding{})
|
||||
if !errors.Is(err, errCurrencyStatisticsUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
|
||||
@@ -310,7 +309,6 @@ func TestAddHoldingsForTime(t *testing.T) {
|
||||
TotalValueLostToVolumeSizing: eleet,
|
||||
TotalValueLostToSlippage: eleet,
|
||||
TotalValueLost: eleet,
|
||||
RiskFreeRate: eleet,
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -334,7 +332,7 @@ func TestAddComplianceSnapshotForTime(t *testing.T) {
|
||||
t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset)
|
||||
}
|
||||
s.setupMap(exch, a)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
|
||||
err = s.AddComplianceSnapshotForTime(compliance.Snapshot{}, &fill.Fill{})
|
||||
if !errors.Is(err, errCurrencyStatisticsUnset) {
|
||||
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
|
||||
@@ -393,14 +391,12 @@ func TestSetStrategyName(t *testing.T) {
|
||||
func TestPrintTotalResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := Statistic{
|
||||
Funding: &funding.Report{
|
||||
Items: []funding.ReportItem{{}},
|
||||
},
|
||||
FundingStatistics: &FundingStatistics{},
|
||||
}
|
||||
s.BiggestDrawdown = s.GetTheBiggestDrawdownAcrossCurrencies([]FinalResultsHolder{
|
||||
{
|
||||
Exchange: "test",
|
||||
MaxDrawdown: currencystatistics.Swing{
|
||||
MaxDrawdown: Swing{
|
||||
DrawdownPercent: eleet,
|
||||
},
|
||||
},
|
||||
@@ -410,7 +406,7 @@ func TestPrintTotalResults(t *testing.T) {
|
||||
Exchange: "test",
|
||||
Asset: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.DOGE),
|
||||
MaxDrawdown: currencystatistics.Swing{},
|
||||
MaxDrawdown: Swing{},
|
||||
MarketMovement: eleet,
|
||||
StrategyMovement: eleet,
|
||||
},
|
||||
@@ -421,7 +417,7 @@ func TestPrintTotalResults(t *testing.T) {
|
||||
MarketMovement: eleet,
|
||||
},
|
||||
})
|
||||
s.PrintTotalResults(true)
|
||||
s.PrintTotalResults()
|
||||
}
|
||||
|
||||
func TestGetBestStrategyPerformer(t *testing.T) {
|
||||
@@ -437,7 +433,7 @@ func TestGetBestStrategyPerformer(t *testing.T) {
|
||||
Exchange: "test",
|
||||
Asset: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.DOGE),
|
||||
MaxDrawdown: currencystatistics.Swing{},
|
||||
MaxDrawdown: Swing{},
|
||||
MarketMovement: eleet,
|
||||
StrategyMovement: eleet,
|
||||
},
|
||||
@@ -445,7 +441,7 @@ func TestGetBestStrategyPerformer(t *testing.T) {
|
||||
Exchange: "test2",
|
||||
Asset: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.DOGE),
|
||||
MaxDrawdown: currencystatistics.Swing{},
|
||||
MaxDrawdown: Swing{},
|
||||
MarketMovement: eleeb,
|
||||
StrategyMovement: eleeb,
|
||||
},
|
||||
@@ -467,13 +463,13 @@ func TestGetTheBiggestDrawdownAcrossCurrencies(t *testing.T) {
|
||||
result = s.GetTheBiggestDrawdownAcrossCurrencies([]FinalResultsHolder{
|
||||
{
|
||||
Exchange: "test",
|
||||
MaxDrawdown: currencystatistics.Swing{
|
||||
MaxDrawdown: Swing{
|
||||
DrawdownPercent: eleet,
|
||||
},
|
||||
},
|
||||
{
|
||||
Exchange: "test2",
|
||||
MaxDrawdown: currencystatistics.Swing{
|
||||
MaxDrawdown: Swing{
|
||||
DrawdownPercent: eleeb,
|
||||
},
|
||||
},
|
||||
@@ -577,9 +573,9 @@ func TestPrintAllEventsChronologically(t *testing.T) {
|
||||
func TestCalculateTheResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := Statistic{}
|
||||
err := s.CalculateAllResults(&funding.FundManager{})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
err := s.CalculateAllResults()
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
tt := time.Now().Add(-gctkline.OneDay.Duration() * 7)
|
||||
@@ -741,7 +737,7 @@ func TestCalculateTheResults(t *testing.T) {
|
||||
s.ExchangeAssetPairStatistics[exch][a][p2].Events[1].Holdings.QuoteInitialFunds = eleet
|
||||
s.ExchangeAssetPairStatistics[exch][a][p2].Events[1].Holdings.TotalValue = eleeet
|
||||
|
||||
funds := &funding.FundManager{}
|
||||
funds := funding.SetupFundingManager(false, false)
|
||||
pBase, err := funding.CreateItem(exch, a, p.Base, eleeet, decimal.Zero)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
@@ -775,8 +771,133 @@ func TestCalculateTheResults(t *testing.T) {
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
err = s.CalculateAllResults(funds)
|
||||
s.FundManager = funds
|
||||
err = s.CalculateAllResults()
|
||||
if !errors.Is(err, errMissingSnapshots) {
|
||||
t.Errorf("received '%v' expected '%v'", err, errMissingSnapshots)
|
||||
}
|
||||
err = s.CalculateAllResults()
|
||||
if !errors.Is(err, errMissingSnapshots) {
|
||||
t.Errorf("received '%v' expected '%v'", err, errMissingSnapshots)
|
||||
}
|
||||
|
||||
funds = funding.SetupFundingManager(false, true)
|
||||
err = funds.AddPair(pair)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
err = funds.AddPair(pair2)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
s.FundManager = funds
|
||||
err = s.CalculateAllResults()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateMaxDrawdown(t *testing.T) {
|
||||
tt1 := time.Now().Add(-gctkline.OneDay.Duration() * 7).Round(gctkline.OneDay.Duration())
|
||||
exch := testExchange
|
||||
a := asset.Spot
|
||||
p := currency.NewPair(currency.BTC, currency.USDT)
|
||||
var events []common.DataEventHandler
|
||||
for i := int64(0); i < 100; i++ {
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even := event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
if i == 50 {
|
||||
// throw in a wrench, a spike in price
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1336),
|
||||
High: decimal.NewFromInt(1336),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
})
|
||||
} else {
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1337).Sub(decimal.NewFromInt(i)),
|
||||
High: decimal.NewFromInt(1337).Sub(decimal.NewFromInt(i)),
|
||||
Low: decimal.NewFromInt(1337).Sub(decimal.NewFromInt(i)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even := event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1338),
|
||||
High: decimal.NewFromInt(1338),
|
||||
Low: decimal.NewFromInt(1338),
|
||||
})
|
||||
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even = event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1337),
|
||||
Low: decimal.NewFromInt(1337),
|
||||
})
|
||||
|
||||
tt1 = tt1.Add(gctkline.OneDay.Duration())
|
||||
even = event.Base{
|
||||
Exchange: exch,
|
||||
Time: tt1,
|
||||
Interval: gctkline.OneDay,
|
||||
CurrencyPair: p,
|
||||
AssetType: a,
|
||||
}
|
||||
events = append(events, &kline.Kline{
|
||||
Base: even,
|
||||
Close: decimal.NewFromInt(1339),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1339),
|
||||
})
|
||||
|
||||
_, err := CalculateBiggestEventDrawdown(nil)
|
||||
if !errors.Is(err, errReceivedNoData) {
|
||||
t.Errorf("received %v expected %v", err, errReceivedNoData)
|
||||
}
|
||||
|
||||
resp, err := CalculateBiggestEventDrawdown(events)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received %v expected %v", err, nil)
|
||||
}
|
||||
if resp.Highest.Value != decimal.NewFromInt(1337) && !resp.Lowest.Value.Equal(decimal.NewFromInt(1238)) {
|
||||
t.Error("unexpected max drawdown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateBiggestValueAtTimeDrawdown(t *testing.T) {
|
||||
var interval gctkline.Interval
|
||||
_, err := CalculateBiggestValueAtTimeDrawdown(nil, interval)
|
||||
if !errors.Is(err, errReceivedNoData) {
|
||||
t.Errorf("received %v expected %v", err, errReceivedNoData)
|
||||
}
|
||||
|
||||
_, err = CalculateBiggestValueAtTimeDrawdown(nil, interval)
|
||||
if !errors.Is(err, errReceivedNoData) {
|
||||
t.Errorf("received %v expected %v", err, errReceivedNoData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics/currencystatistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
@@ -21,36 +23,43 @@ var (
|
||||
ErrAlreadyProcessed = errors.New("this event has been processed already")
|
||||
errExchangeAssetPairStatsUnset = errors.New("exchangeAssetPairStatistics not setup")
|
||||
errCurrencyStatisticsUnset = errors.New("no data")
|
||||
errMissingSnapshots = errors.New("funding report item missing USD snapshots")
|
||||
errNoRelevantStatsFound = errors.New("no relevant currency pair statistics found")
|
||||
errReceivedNoData = errors.New("received no data")
|
||||
)
|
||||
|
||||
// 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"`
|
||||
ExchangeAssetPairStatistics map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic `json:"-"`
|
||||
RiskFreeRate decimal.Decimal `json:"risk-free-rate"`
|
||||
TotalBuyOrders int64 `json:"total-buy-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"`
|
||||
AllStats []currencystatistics.CurrencyStatistic `json:"results"` // as ExchangeAssetPairStatistics cannot be rendered via json.Marshall, we append all result to this slice instead
|
||||
WasAnyDataMissing bool `json:"was-any-data-missing"`
|
||||
Funding *funding.Report `json:"funding"`
|
||||
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.Pair]*CurrencyPairStatistic `json:"exchange-asset-pair-statistics"`
|
||||
TotalBuyOrders int64 `json:"total-buy-orders"`
|
||||
TotalSellOrders int64 `json:"total-sell-orders"`
|
||||
TotalOrders int64 `json:"total-orders"`
|
||||
BiggestDrawdown *FinalResultsHolder `json:"biggest-drawdown,omitempty"`
|
||||
BestStrategyResults *FinalResultsHolder `json:"best-start-results,omitempty"`
|
||||
BestMarketMovement *FinalResultsHolder `json:"best-market-movement,omitempty"`
|
||||
CurrencyPairStatistics []CurrencyPairStatistic `json:"currency-pair-statistics"` // as ExchangeAssetPairStatistics cannot be rendered via json.Marshall, we append all result to this slice instead
|
||||
WasAnyDataMissing bool `json:"was-any-data-missing"`
|
||||
FundingStatistics *FundingStatistics `json:"funding-statistics"`
|
||||
FundManager funding.IFundingManager `json:"-"`
|
||||
}
|
||||
|
||||
// FinalResultsHolder holds important stats about a currency's performance
|
||||
type FinalResultsHolder struct {
|
||||
Exchange string `json:"exchange"`
|
||||
Asset asset.Item `json:"asset"`
|
||||
Pair currency.Pair `json:"currency"`
|
||||
MaxDrawdown currencystatistics.Swing `json:"max-drawdown"`
|
||||
MarketMovement decimal.Decimal `json:"market-movement"`
|
||||
StrategyMovement decimal.Decimal `json:"strategy-movement"`
|
||||
Exchange string `json:"exchange"`
|
||||
Asset asset.Item `json:"asset"`
|
||||
Pair currency.Pair `json:"currency"`
|
||||
MaxDrawdown Swing `json:"max-drawdown"`
|
||||
MarketMovement decimal.Decimal `json:"market-movement"`
|
||||
StrategyMovement decimal.Decimal `json:"strategy-movement"`
|
||||
}
|
||||
|
||||
// Handler interface details what a statistic is expected to do
|
||||
@@ -60,7 +69,7 @@ type Handler interface {
|
||||
SetEventForOffset(common.EventHandler) error
|
||||
AddHoldingsForTime(*holdings.Holding) error
|
||||
AddComplianceSnapshotForTime(compliance.Snapshot, fill.Event) error
|
||||
CalculateAllResults(funding.IFundingManager) error
|
||||
CalculateAllResults() error
|
||||
Reset()
|
||||
Serialise() (string, error)
|
||||
}
|
||||
@@ -93,3 +102,134 @@ type eventOutputHolder struct {
|
||||
Time time.Time
|
||||
Events []string
|
||||
}
|
||||
|
||||
// CurrencyStats defines what is expected in order to
|
||||
// calculate statistics based on an exchange, asset type and currency pair
|
||||
type CurrencyStats interface {
|
||||
TotalEquityReturn() (decimal.Decimal, error)
|
||||
MaxDrawdown() Swing
|
||||
LongestDrawdown() Swing
|
||||
SharpeRatio(decimal.Decimal) decimal.Decimal
|
||||
SortinoRatio(decimal.Decimal) decimal.Decimal
|
||||
}
|
||||
|
||||
// EventStore is used to hold all event information
|
||||
// at a time interval
|
||||
type EventStore struct {
|
||||
Holdings holdings.Holding
|
||||
Transactions compliance.Snapshot
|
||||
DataEvent common.DataEventHandler
|
||||
SignalEvent signal.Event
|
||||
OrderEvent order.Event
|
||||
FillEvent fill.Event
|
||||
}
|
||||
|
||||
// CurrencyPairStatistic Holds all events and statistics relevant to an exchange, asset type and currency pair
|
||||
type CurrencyPairStatistic struct {
|
||||
ShowMissingDataWarning bool `json:"-"`
|
||||
IsStrategyProfitable bool `json:"is-strategy-profitable"`
|
||||
DoesPerformanceBeatTheMarket bool `json:"does-performance-beat-the-market"`
|
||||
|
||||
BuyOrders int64 `json:"buy-orders"`
|
||||
SellOrders int64 `json:"sell-orders"`
|
||||
TotalOrders int64 `json:"total-orders"`
|
||||
|
||||
StartingClosePrice decimal.Decimal `json:"starting-close-price"`
|
||||
EndingClosePrice decimal.Decimal `json:"ending-close-price"`
|
||||
LowestClosePrice decimal.Decimal `json:"lowest-close-price"`
|
||||
HighestClosePrice decimal.Decimal `json:"highest-close-price"`
|
||||
MarketMovement decimal.Decimal `json:"market-movement"`
|
||||
StrategyMovement decimal.Decimal `json:"strategy-movement"`
|
||||
CompoundAnnualGrowthRate decimal.Decimal `json:"compound-annual-growth-rate"`
|
||||
TotalAssetValue decimal.Decimal
|
||||
TotalFees decimal.Decimal
|
||||
TotalValueLostToVolumeSizing decimal.Decimal
|
||||
TotalValueLostToSlippage decimal.Decimal
|
||||
TotalValueLost decimal.Decimal
|
||||
|
||||
Events []EventStore `json:"-"`
|
||||
|
||||
MaxDrawdown Swing `json:"max-drawdown,omitempty"`
|
||||
HighestCommittedFunds ValueAtTime `json:"highest-committed-funds"`
|
||||
GeometricRatios *Ratios `json:"geometric-ratios"`
|
||||
ArithmeticRatios *Ratios `json:"arithmetic-ratios"`
|
||||
InitialHoldings holdings.Holding `json:"initial-holdings-holdings"`
|
||||
FinalHoldings holdings.Holding `json:"final-holdings"`
|
||||
FinalOrders compliance.Snapshot `json:"final-orders"`
|
||||
}
|
||||
|
||||
// Ratios stores all the ratios used for statistics
|
||||
type Ratios struct {
|
||||
SharpeRatio decimal.Decimal `json:"sharpe-ratio"`
|
||||
SortinoRatio decimal.Decimal `json:"sortino-ratio"`
|
||||
InformationRatio decimal.Decimal `json:"information-ratio"`
|
||||
CalmarRatio decimal.Decimal `json:"calmar-ratio"`
|
||||
}
|
||||
|
||||
// Swing holds a drawdown
|
||||
type Swing struct {
|
||||
Highest ValueAtTime `json:"highest"`
|
||||
Lowest ValueAtTime `json:"lowest"`
|
||||
DrawdownPercent decimal.Decimal `json:"drawdown"`
|
||||
IntervalDuration int64
|
||||
}
|
||||
|
||||
// ValueAtTime is an individual iteration of price at a time
|
||||
type ValueAtTime struct {
|
||||
Time time.Time `json:"time"`
|
||||
Value decimal.Decimal `json:"value"`
|
||||
}
|
||||
|
||||
type relatedCurrencyPairStatistics struct {
|
||||
isBaseCurrency bool
|
||||
stat *CurrencyPairStatistic
|
||||
}
|
||||
|
||||
// FundingStatistics stores all funding related statistics
|
||||
type FundingStatistics struct {
|
||||
Report *funding.Report
|
||||
Items []FundingItemStatistics
|
||||
TotalUSDStatistics *TotalFundingStatistics
|
||||
}
|
||||
|
||||
// FundingItemStatistics holds statistics for funding items
|
||||
type FundingItemStatistics struct {
|
||||
ReportItem *funding.ReportItem
|
||||
// USD stats
|
||||
StartingClosePrice ValueAtTime
|
||||
EndingClosePrice ValueAtTime
|
||||
LowestClosePrice ValueAtTime
|
||||
HighestClosePrice ValueAtTime
|
||||
MarketMovement decimal.Decimal
|
||||
StrategyMovement decimal.Decimal
|
||||
DidStrategyBeatTheMarket bool
|
||||
RiskFreeRate decimal.Decimal
|
||||
CompoundAnnualGrowthRate decimal.Decimal
|
||||
BuyOrders int64
|
||||
SellOrders int64
|
||||
TotalOrders int64
|
||||
MaxDrawdown Swing
|
||||
HighestCommittedFunds ValueAtTime
|
||||
}
|
||||
|
||||
// TotalFundingStatistics holds values for overal statistics for funding items
|
||||
type TotalFundingStatistics struct {
|
||||
HoldingValues []ValueAtTime
|
||||
InitialHoldingValue ValueAtTime
|
||||
FinalHoldingValue ValueAtTime
|
||||
HighestHoldingValue ValueAtTime
|
||||
LowestHoldingValue ValueAtTime
|
||||
BenchmarkMarketMovement decimal.Decimal
|
||||
StrategyMovement decimal.Decimal
|
||||
RiskFreeRate decimal.Decimal
|
||||
CompoundAnnualGrowthRate decimal.Decimal
|
||||
BuyOrders int64
|
||||
SellOrders int64
|
||||
TotalOrders int64
|
||||
MaxDrawdown Swing
|
||||
GeometricRatios *Ratios
|
||||
ArithmeticRatios *Ratios
|
||||
DidStrategyBeatTheMarket bool
|
||||
DidStrategyMakeProfit bool
|
||||
HoldingValueDifference decimal.Decimal
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
@@ -36,7 +37,7 @@ func (s *Strategy) Description() string {
|
||||
|
||||
// OnSignal handles a data event and returns what action the strategy believes should occur
|
||||
// For dollarcostaverage, this means returning a buy signal on every event
|
||||
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer) (signal.Event, error) {
|
||||
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) (signal.Event, error) {
|
||||
if d == nil {
|
||||
return nil, common.ErrNilEvent
|
||||
}
|
||||
@@ -65,11 +66,11 @@ func (s *Strategy) SupportsSimultaneousProcessing() bool {
|
||||
// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility
|
||||
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
|
||||
// For dollarcostaverage, the strategy is always "buy", so it uses the OnSignal function
|
||||
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundTransferer) ([]signal.Event, error) {
|
||||
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) ([]signal.Event, error) {
|
||||
var resp []signal.Event
|
||||
var errs gctcommon.Errors
|
||||
for i := range d {
|
||||
sigEvent, err := s.OnSignal(d[i], nil)
|
||||
sigEvent, err := s.OnSignal(d[i], nil, nil)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestSetCustomSettings(t *testing.T) {
|
||||
|
||||
func TestOnSignal(t *testing.T) {
|
||||
s := Strategy{}
|
||||
_, err := s.OnSignal(nil, nil)
|
||||
_, err := s.OnSignal(nil, nil, nil)
|
||||
if !errors.Is(err, common.ErrNilEvent) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func TestOnSignal(t *testing.T) {
|
||||
RangeHolder: &gctkline.IntervalRangeHolder{},
|
||||
}
|
||||
var resp signal.Event
|
||||
resp, err = s.OnSignal(da, nil)
|
||||
resp, err = s.OnSignal(da, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func TestOnSignal(t *testing.T) {
|
||||
}
|
||||
da.RangeHolder = ranger
|
||||
da.RangeHolder.SetHasDataFromCandles(da.Item.Candles)
|
||||
resp, err = s.OnSignal(da, nil)
|
||||
resp, err = s.OnSignal(da, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func TestOnSignal(t *testing.T) {
|
||||
|
||||
func TestOnSignals(t *testing.T) {
|
||||
s := Strategy{}
|
||||
_, err := s.OnSignal(nil, nil)
|
||||
_, err := s.OnSignal(nil, nil, nil)
|
||||
if !errors.Is(err, common.ErrNilEvent) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func TestOnSignals(t *testing.T) {
|
||||
RangeHolder: &gctkline.IntervalRangeHolder{},
|
||||
}
|
||||
var resp []signal.Event
|
||||
resp, err = s.OnSimultaneousSignals([]data.Handler{da}, nil)
|
||||
resp, err = s.OnSimultaneousSignals([]data.Handler{da}, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -193,7 +193,7 @@ func TestOnSignals(t *testing.T) {
|
||||
}
|
||||
da.RangeHolder = ranger
|
||||
da.RangeHolder.SetHasDataFromCandles(da.Item.Candles)
|
||||
resp, err = s.OnSimultaneousSignals([]data.Handler{da}, nil)
|
||||
resp, err = s.OnSimultaneousSignals([]data.Handler{da}, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/thrasher-corp/gct-ta/indicators"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
@@ -46,7 +47,7 @@ func (s *Strategy) Description() string {
|
||||
// OnSignal handles a data event and returns what action the strategy believes should occur
|
||||
// For rsi, this means returning a buy signal when rsi is at or below a certain level, and a
|
||||
// sell signal when it is at or above a certain level
|
||||
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer) (signal.Event, error) {
|
||||
func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) (signal.Event, error) {
|
||||
if d == nil {
|
||||
return nil, common.ErrNilEvent
|
||||
}
|
||||
@@ -98,11 +99,11 @@ func (s *Strategy) SupportsSimultaneousProcessing() bool {
|
||||
|
||||
// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility
|
||||
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
|
||||
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundTransferer) ([]signal.Event, error) {
|
||||
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) ([]signal.Event, error) {
|
||||
var resp []signal.Event
|
||||
var errs gctcommon.Errors
|
||||
for i := range d {
|
||||
sigEvent, err := s.OnSignal(d[i], nil)
|
||||
sigEvent, err := s.OnSignal(d[i], nil, nil)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("%v %v %v %w", d[i].Latest().GetExchange(), d[i].Latest().GetAssetType(), d[i].Latest().Pair(), err))
|
||||
} else {
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestSetCustomSettings(t *testing.T) {
|
||||
func TestOnSignal(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := Strategy{}
|
||||
_, err := s.OnSignal(nil, nil)
|
||||
_, err := s.OnSignal(nil, nil, nil)
|
||||
if !errors.Is(err, common.ErrNilEvent) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
|
||||
}
|
||||
@@ -118,13 +118,13 @@ func TestOnSignal(t *testing.T) {
|
||||
RangeHolder: &gctkline.IntervalRangeHolder{},
|
||||
}
|
||||
var resp signal.Event
|
||||
_, err = s.OnSignal(da, nil)
|
||||
_, err = s.OnSignal(da, nil, nil)
|
||||
if !errors.Is(err, base.ErrTooMuchBadData) {
|
||||
t.Fatalf("expected: %v, received %v", base.ErrTooMuchBadData, err)
|
||||
}
|
||||
|
||||
s.rsiPeriod = decimal.NewFromInt(1)
|
||||
_, err = s.OnSignal(da, nil)
|
||||
_, err = s.OnSignal(da, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func TestOnSignal(t *testing.T) {
|
||||
}
|
||||
da.RangeHolder = ranger
|
||||
da.RangeHolder.SetHasDataFromCandles(da.Item.Candles)
|
||||
resp, err = s.OnSignal(da, nil)
|
||||
resp, err = s.OnSignal(da, nil, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func TestOnSignal(t *testing.T) {
|
||||
func TestOnSignals(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := Strategy{}
|
||||
_, err := s.OnSignal(nil, nil)
|
||||
_, err := s.OnSignal(nil, nil, nil)
|
||||
if !errors.Is(err, common.ErrNilEvent) {
|
||||
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
|
||||
}
|
||||
@@ -197,7 +197,7 @@ func TestOnSignals(t *testing.T) {
|
||||
Base: d,
|
||||
RangeHolder: &gctkline.IntervalRangeHolder{},
|
||||
}
|
||||
_, err = s.OnSimultaneousSignals([]data.Handler{da}, nil)
|
||||
_, err = s.OnSimultaneousSignals([]data.Handler{da}, nil, nil)
|
||||
if !strings.Contains(err.Error(), base.ErrTooMuchBadData.Error()) {
|
||||
// common.Errs type doesn't keep type
|
||||
t.Errorf("received: %v, expected: %v", err, base.ErrTooMuchBadData)
|
||||
|
||||
@@ -2,6 +2,7 @@ package strategies
|
||||
|
||||
import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
)
|
||||
@@ -10,8 +11,8 @@ import (
|
||||
type Handler interface {
|
||||
Name() string
|
||||
Description() string
|
||||
OnSignal(data.Handler, funding.IFundTransferer) (signal.Event, error)
|
||||
OnSimultaneousSignals([]data.Handler, funding.IFundTransferer) ([]signal.Event, error)
|
||||
OnSignal(data.Handler, funding.IFundTransferer, portfolio.Handler) (signal.Event, error)
|
||||
OnSimultaneousSignals([]data.Handler, funding.IFundTransferer, portfolio.Handler) ([]signal.Event, error)
|
||||
UsingSimultaneousProcessing() bool
|
||||
SupportsSimultaneousProcessing() bool
|
||||
SetSimultaneousProcessing(bool)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/thrasher-corp/gct-ta/indicators"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
@@ -52,7 +53,7 @@ func (s *Strategy) Description() string {
|
||||
|
||||
// OnSignal handles a data event and returns what action the strategy believes should occur
|
||||
// however,this complex strategy cannot function on an individual basis
|
||||
func (s *Strategy) OnSignal(_ data.Handler, _ funding.IFundTransferer) (signal.Event, error) {
|
||||
func (s *Strategy) OnSignal(_ data.Handler, _ funding.IFundTransferer, _ portfolio.Handler) (signal.Event, error) {
|
||||
return nil, errStrategyOnlySupportsSimultaneousProcessing
|
||||
}
|
||||
|
||||
@@ -87,7 +88,7 @@ func sortByMFI(o *[]mfiFundEvent, reverse bool) {
|
||||
|
||||
// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility
|
||||
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
|
||||
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundTransferer) ([]signal.Event, error) {
|
||||
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundTransferer, _ portfolio.Handler) ([]signal.Event, error) {
|
||||
if len(d) < 4 {
|
||||
return nil, errStrategyCurrencyRequirements
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestSetCustomSettings(t *testing.T) {
|
||||
func TestOnSignal(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := Strategy{}
|
||||
if _, err := s.OnSignal(nil, nil); !errors.Is(err, errStrategyOnlySupportsSimultaneousProcessing) {
|
||||
if _, err := s.OnSignal(nil, nil, nil); !errors.Is(err, errStrategyOnlySupportsSimultaneousProcessing) {
|
||||
t.Errorf("received: %v, expected: %v", err, errStrategyOnlySupportsSimultaneousProcessing)
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func TestOnSignal(t *testing.T) {
|
||||
func TestOnSignals(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := Strategy{}
|
||||
_, err := s.OnSignal(nil, nil)
|
||||
_, err := s.OnSignal(nil, nil, nil)
|
||||
if !errors.Is(err, errStrategyOnlySupportsSimultaneousProcessing) {
|
||||
t.Errorf("received: %v, expected: %v", err, errStrategyOnlySupportsSimultaneousProcessing)
|
||||
}
|
||||
@@ -130,13 +130,13 @@ func TestOnSignals(t *testing.T) {
|
||||
Base: d,
|
||||
RangeHolder: &gctkline.IntervalRangeHolder{},
|
||||
}
|
||||
_, err = s.OnSimultaneousSignals([]data.Handler{da}, nil)
|
||||
_, err = s.OnSimultaneousSignals([]data.Handler{da}, nil, nil)
|
||||
if !strings.Contains(err.Error(), errStrategyCurrencyRequirements.Error()) {
|
||||
// common.Errs type doesn't keep type
|
||||
t.Errorf("received: %v, expected: %v", err, errStrategyCurrencyRequirements)
|
||||
}
|
||||
|
||||
_, err = s.OnSimultaneousSignals([]data.Handler{da, da, da, da}, nil)
|
||||
_, err = s.OnSimultaneousSignals([]data.Handler{da, da, da, da}, nil, nil)
|
||||
if !strings.Contains(err.Error(), base.ErrTooMuchBadData.Error()) {
|
||||
// common.Errs type doesn't keep type
|
||||
t.Errorf("received: %v, expected: %v", err, base.ErrTooMuchBadData)
|
||||
|
||||
@@ -3,36 +3,43 @@ package funding
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding/trackingcurrencies"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
fbase "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
|
||||
exchangeratehost "github.com/thrasher-corp/gocryptotrader/currency/forexprovider/exchangerate.host"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFundsNotFound used when funds are requested but the funding is not found in the manager
|
||||
ErrFundsNotFound = errors.New("funding not found")
|
||||
// ErrAlreadyExists used when a matching item or pair is already in the funding manager
|
||||
ErrAlreadyExists = errors.New("funding already exists")
|
||||
ErrAlreadyExists = errors.New("funding already exists")
|
||||
// ErrUSDTrackingDisabled used when attempting to track USD values when disabled
|
||||
ErrUSDTrackingDisabled = errors.New("USD tracking disabled")
|
||||
errCannotAllocate = errors.New("cannot allocate funds")
|
||||
errZeroAmountReceived = errors.New("amount received less than or equal to zero")
|
||||
errNegativeAmountReceived = errors.New("received negative decimal")
|
||||
errNotEnoughFunds = errors.New("not enough funds")
|
||||
errCannotTransferToSameFunds = errors.New("cannot send funds to self")
|
||||
errTransferMustBeSameCurrency = errors.New("cannot transfer to different currency")
|
||||
errCannotMatchTrackingToItem = errors.New("cannot match tracking data to funding items")
|
||||
)
|
||||
|
||||
// SetupFundingManager creates the funding holder. It carries knowledge about levels of funding
|
||||
// across all execution handlers and enables fund transfers
|
||||
func SetupFundingManager(usingExchangeLevelFunding bool) *FundManager {
|
||||
return &FundManager{usingExchangeLevelFunding: usingExchangeLevelFunding}
|
||||
func SetupFundingManager(usingExchangeLevelFunding, disableUSDTracking bool) *FundManager {
|
||||
return &FundManager{
|
||||
usingExchangeLevelFunding: usingExchangeLevelFunding,
|
||||
disableUSDTracking: disableUSDTracking,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateItem creates a new funding item
|
||||
@@ -51,9 +58,103 @@ func CreateItem(exch string, a asset.Item, ci currency.Code, initialFunds, trans
|
||||
initialFunds: initialFunds,
|
||||
available: initialFunds,
|
||||
transferFee: transferFee,
|
||||
snapshot: make(map[time.Time]ItemSnapshot),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a Snapshot for an event's point in time
|
||||
// as funding.snapshots is a map, it allows for the last event
|
||||
// in the chronological list to establish the canon at X time
|
||||
func (f *FundManager) CreateSnapshot(t time.Time) {
|
||||
for i := range f.items {
|
||||
if f.items[i].snapshot == nil {
|
||||
f.items[i].snapshot = make(map[time.Time]ItemSnapshot)
|
||||
}
|
||||
iss := ItemSnapshot{
|
||||
Available: f.items[i].available,
|
||||
Time: t,
|
||||
}
|
||||
if !f.disableUSDTracking {
|
||||
var usdClosePrice decimal.Decimal
|
||||
if f.items[i].usdTrackingCandles == nil {
|
||||
continue
|
||||
}
|
||||
usdCandles := f.items[i].usdTrackingCandles.GetStream()
|
||||
for j := range usdCandles {
|
||||
if usdCandles[j].GetTime().Equal(t) {
|
||||
usdClosePrice = usdCandles[j].ClosePrice()
|
||||
break
|
||||
}
|
||||
}
|
||||
iss.USDClosePrice = usdClosePrice
|
||||
iss.USDValue = usdClosePrice.Mul(f.items[i].available)
|
||||
}
|
||||
|
||||
f.items[i].snapshot[t] = iss
|
||||
}
|
||||
}
|
||||
|
||||
// AddUSDTrackingData adds USD tracking data to a funding item
|
||||
// only in the event that it is not USD and there is data
|
||||
func (f *FundManager) AddUSDTrackingData(k *kline.DataFromKline) error {
|
||||
if f == nil || f.items == nil {
|
||||
return common.ErrNilArguments
|
||||
}
|
||||
if f.disableUSDTracking {
|
||||
return ErrUSDTrackingDisabled
|
||||
}
|
||||
baseSet := false
|
||||
quoteSet := false
|
||||
for i := range f.items {
|
||||
if baseSet && quoteSet {
|
||||
return nil
|
||||
}
|
||||
if strings.EqualFold(f.items[i].exchange, k.Item.Exchange) &&
|
||||
f.items[i].asset == k.Item.Asset {
|
||||
if f.items[i].currency == k.Item.Pair.Base {
|
||||
if f.items[i].usdTrackingCandles == nil &&
|
||||
trackingcurrencies.CurrencyIsUSDTracked(k.Item.Pair.Quote) {
|
||||
f.items[i].usdTrackingCandles = k
|
||||
}
|
||||
baseSet = true
|
||||
}
|
||||
if trackingcurrencies.CurrencyIsUSDTracked(f.items[i].currency) {
|
||||
if f.items[i].usdTrackingCandles == nil {
|
||||
usdCandles := gctkline.Item{
|
||||
Exchange: k.Item.Exchange,
|
||||
Pair: currency.Pair{Delimiter: k.Item.Pair.Delimiter, Base: f.items[i].currency, Quote: currency.USD},
|
||||
Asset: k.Item.Asset,
|
||||
Interval: k.Item.Interval,
|
||||
Candles: make([]gctkline.Candle, len(k.Item.Candles)),
|
||||
}
|
||||
copy(usdCandles.Candles, k.Item.Candles)
|
||||
for j := range usdCandles.Candles {
|
||||
// usd stablecoins do not always match in value,
|
||||
// this is a simplified implementation that can allow
|
||||
// USD tracking for many different currencies across many exchanges
|
||||
// without retrieving n candle history and exchange rates
|
||||
usdCandles.Candles[j].Open = 1
|
||||
usdCandles.Candles[j].High = 1
|
||||
usdCandles.Candles[j].Low = 1
|
||||
usdCandles.Candles[j].Close = 1
|
||||
}
|
||||
cpy := *k
|
||||
cpy.Item = usdCandles
|
||||
if err := cpy.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
f.items[i].usdTrackingCandles = &cpy
|
||||
}
|
||||
quoteSet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if baseSet {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%w %v %v %v", errCannotMatchTrackingToItem, k.Item.Exchange, k.Item.Asset, k.Item.Pair)
|
||||
}
|
||||
|
||||
// CreatePair adds two funding items and associates them with one another
|
||||
// the association allows for the same currency to be used multiple times when
|
||||
// usingExchangeLevelFunding is false. eg BTC-USDT and LTC-USDT do not share the same
|
||||
@@ -79,65 +180,52 @@ func (f *FundManager) Reset() {
|
||||
*f = FundManager{}
|
||||
}
|
||||
|
||||
// USDTrackingDisabled clears all settings
|
||||
func (f *FundManager) USDTrackingDisabled() bool {
|
||||
return f.disableUSDTracking
|
||||
}
|
||||
|
||||
// GenerateReport builds report data for result HTML report
|
||||
func (f *FundManager) GenerateReport(startDate, endDate time.Time) *Report {
|
||||
report := &Report{}
|
||||
var items []ReportItem
|
||||
var erh exchangeratehost.ExchangeRateHost
|
||||
var skipAPICheck bool
|
||||
err := erh.Setup(fbase.Settings{Enabled: true})
|
||||
if err != nil {
|
||||
log.Errorf(log.CommunicationMgr, "issue setting up exchangerate.host API %v", err)
|
||||
skipAPICheck = true
|
||||
func (f *FundManager) GenerateReport() *Report {
|
||||
report := Report{
|
||||
USDTotalsOverTime: make(map[time.Time]ItemSnapshot),
|
||||
UsingExchangeLevelFunding: f.usingExchangeLevelFunding,
|
||||
DisableUSDTracking: f.disableUSDTracking,
|
||||
}
|
||||
var items []ReportItem
|
||||
for i := range f.items {
|
||||
// exact conversion not required for initial version
|
||||
fInitialFunds, _ := f.items[i].initialFunds.Float64()
|
||||
fFinalFunds, _ := f.items[i].available.Float64()
|
||||
var initialWorthDecimal, finalWorthDecimal decimal.Decimal
|
||||
if !skipAPICheck {
|
||||
// calculating totals for shared funding across multiple currency pairs is difficult
|
||||
// converting totals using a free API is better suited as an initial concept
|
||||
// TODO convert currencies without external dependency
|
||||
if strings.Contains(f.items[i].currency.String(), "USD") {
|
||||
// not worth converting
|
||||
initialWorthDecimal = f.items[i].initialFunds
|
||||
finalWorthDecimal = f.items[i].available
|
||||
} else {
|
||||
from := f.items[i].currency.String()
|
||||
to := "USD"
|
||||
if from == "BTC" {
|
||||
// api has conversion difficulties for BTC to USD only
|
||||
to = "BUSD"
|
||||
}
|
||||
if fInitialFunds > 0 {
|
||||
initialWorth, err := erh.ConvertCurrency(from, to, "", "", "crypto", startDate, fInitialFunds, 0)
|
||||
if err != nil {
|
||||
log.Errorf(log.CommunicationMgr, "issue converting %v to %v at %v on exchangerate.host API %v", from, to, startDate, err)
|
||||
} else {
|
||||
initialWorthDecimal = decimal.NewFromFloat(initialWorth.Result)
|
||||
}
|
||||
}
|
||||
if fFinalFunds > 0 {
|
||||
finalWorth, err := erh.ConvertCurrency(from, to, "", "", "crypto", endDate, fFinalFunds, 0)
|
||||
if err != nil {
|
||||
log.Errorf(log.CommunicationMgr, "issue converting %v to %v at %v on exchangerate.host API %v", from, to, endDate, err)
|
||||
} else {
|
||||
finalWorthDecimal = decimal.NewFromFloat(finalWorth.Result)
|
||||
}
|
||||
}
|
||||
item := ReportItem{
|
||||
Exchange: f.items[i].exchange,
|
||||
Asset: f.items[i].asset,
|
||||
Currency: f.items[i].currency,
|
||||
InitialFunds: f.items[i].initialFunds,
|
||||
TransferFee: f.items[i].transferFee,
|
||||
FinalFunds: f.items[i].available,
|
||||
}
|
||||
if !f.disableUSDTracking &&
|
||||
f.items[i].usdTrackingCandles != nil {
|
||||
usdStream := f.items[i].usdTrackingCandles.GetStream()
|
||||
item.USDInitialFunds = f.items[i].initialFunds.Mul(usdStream[0].ClosePrice())
|
||||
item.USDFinalFunds = f.items[i].available.Mul(usdStream[len(usdStream)-1].ClosePrice())
|
||||
item.USDInitialCostForOne = usdStream[0].ClosePrice()
|
||||
item.USDFinalCostForOne = usdStream[len(usdStream)-1].ClosePrice()
|
||||
item.USDPairCandle = f.items[i].usdTrackingCandles
|
||||
}
|
||||
|
||||
var pricingOverTime []ItemSnapshot
|
||||
for _, v := range f.items[i].snapshot {
|
||||
pricingOverTime = append(pricingOverTime, v)
|
||||
if !f.disableUSDTracking {
|
||||
usdTotalForPeriod := report.USDTotalsOverTime[v.Time]
|
||||
usdTotalForPeriod.Time = v.Time
|
||||
usdTotalForPeriod.USDValue = usdTotalForPeriod.USDValue.Add(v.USDValue)
|
||||
report.USDTotalsOverTime[v.Time] = usdTotalForPeriod
|
||||
}
|
||||
}
|
||||
item := ReportItem{
|
||||
Exchange: f.items[i].exchange,
|
||||
Asset: f.items[i].asset,
|
||||
Currency: f.items[i].currency,
|
||||
InitialFunds: f.items[i].initialFunds,
|
||||
InitialFundsUSD: initialWorthDecimal.Round(2),
|
||||
TransferFee: f.items[i].transferFee,
|
||||
FinalFunds: f.items[i].available,
|
||||
FinalFundsUSD: finalWorthDecimal.Round(2),
|
||||
}
|
||||
sort.Slice(pricingOverTime, func(i, j int) bool {
|
||||
return pricingOverTime[i].Time.Before(pricingOverTime[j].Time)
|
||||
})
|
||||
item.Snapshots = pricingOverTime
|
||||
|
||||
if f.items[i].initialFunds.IsZero() {
|
||||
item.ShowInfinite = true
|
||||
@@ -147,15 +235,11 @@ func (f *FundManager) GenerateReport(startDate, endDate time.Time) *Report {
|
||||
if f.items[i].pairedWith != nil {
|
||||
item.PairedWith = f.items[i].pairedWith.currency
|
||||
}
|
||||
report.InitialTotalUSD = report.InitialTotalUSD.Add(initialWorthDecimal).Round(2)
|
||||
report.FinalTotalUSD = report.FinalTotalUSD.Add(finalWorthDecimal).Round(2)
|
||||
|
||||
items = append(items, item)
|
||||
}
|
||||
if !report.InitialTotalUSD.IsZero() {
|
||||
report.Difference = report.FinalTotalUSD.Sub(report.InitialTotalUSD).Div(report.InitialTotalUSD).Mul(decimal.NewFromInt(100))
|
||||
}
|
||||
report.Items = items
|
||||
return report
|
||||
return &report
|
||||
}
|
||||
|
||||
// Transfer allows transferring funds from one pretend exchange to another
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
@@ -26,19 +27,25 @@ var (
|
||||
|
||||
func TestSetupFundingManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := SetupFundingManager(true)
|
||||
f := SetupFundingManager(true, false)
|
||||
if !f.usingExchangeLevelFunding {
|
||||
t.Errorf("expected '%v received '%v'", true, false)
|
||||
}
|
||||
f = SetupFundingManager(false)
|
||||
if f.disableUSDTracking {
|
||||
t.Errorf("expected '%v received '%v'", false, true)
|
||||
}
|
||||
f = SetupFundingManager(false, true)
|
||||
if f.usingExchangeLevelFunding {
|
||||
t.Errorf("expected '%v received '%v'", false, true)
|
||||
}
|
||||
if !f.disableUSDTracking {
|
||||
t.Errorf("expected '%v received '%v'", true, false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReset(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := SetupFundingManager(true)
|
||||
f := SetupFundingManager(true, false)
|
||||
baseItem, err := CreateItem(exch, a, base, decimal.Zero, decimal.Zero)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
@@ -58,7 +65,7 @@ func TestReset(t *testing.T) {
|
||||
|
||||
func TestIsUsingExchangeLevelFunding(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := SetupFundingManager(true)
|
||||
f := SetupFundingManager(true, false)
|
||||
if !f.IsUsingExchangeLevelFunding() {
|
||||
t.Errorf("expected '%v received '%v'", true, false)
|
||||
}
|
||||
@@ -749,9 +756,7 @@ func TestMatchesExchange(t *testing.T) {
|
||||
func TestGenerateReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := FundManager{}
|
||||
s := time.Now().Add(-time.Hour).Round(time.Hour)
|
||||
e := time.Now()
|
||||
report := f.GenerateReport(s, e)
|
||||
report := f.GenerateReport()
|
||||
if report == nil {
|
||||
t.Fatal("shouldn't be nil")
|
||||
}
|
||||
@@ -759,16 +764,17 @@ func TestGenerateReport(t *testing.T) {
|
||||
t.Error("expected 0")
|
||||
}
|
||||
item := &Item{
|
||||
exchange: "hello :)",
|
||||
exchange: exch,
|
||||
initialFunds: decimal.NewFromInt(100),
|
||||
available: decimal.NewFromInt(200),
|
||||
currency: currency.BTC,
|
||||
asset: a,
|
||||
}
|
||||
err := f.AddItem(item)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report = f.GenerateReport(s, e)
|
||||
report = f.GenerateReport()
|
||||
if len(report.Items) != 1 {
|
||||
t.Fatal("expected 1")
|
||||
}
|
||||
@@ -778,25 +784,48 @@ func TestGenerateReport(t *testing.T) {
|
||||
|
||||
f.usingExchangeLevelFunding = true
|
||||
err = f.AddItem(&Item{
|
||||
exchange: "hello :)",
|
||||
exchange: exch,
|
||||
initialFunds: decimal.NewFromInt(100),
|
||||
available: decimal.NewFromInt(200),
|
||||
currency: currency.USD,
|
||||
asset: a,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report = f.GenerateReport(s, e)
|
||||
|
||||
dfk := &kline.DataFromKline{
|
||||
Item: gctkline.Item{
|
||||
Exchange: exch,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Asset: a,
|
||||
Interval: gctkline.OneHour,
|
||||
Candles: []gctkline.Candle{
|
||||
{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = dfk.Load()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
err = f.AddUSDTrackingData(dfk)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
f.items[0].usdTrackingCandles = dfk
|
||||
f.CreateSnapshot(dfk.Item.Candles[0].Time)
|
||||
|
||||
report = f.GenerateReport()
|
||||
if len(report.Items) != 2 {
|
||||
t.Fatal("expected 2")
|
||||
}
|
||||
if report.Items[0].Exchange != item.exchange {
|
||||
t.Error("expected matching name")
|
||||
}
|
||||
if report.Items[0].FinalFundsUSD.Equal(decimal.NewFromInt(200)) {
|
||||
t.Errorf("received %v expected converted values", decimal.NewFromInt(200))
|
||||
}
|
||||
if !report.Items[1].FinalFundsUSD.Equal(decimal.NewFromInt(200)) {
|
||||
if !report.Items[1].FinalFunds.Equal(decimal.NewFromInt(200)) {
|
||||
t.Errorf("received %v expected %v", report.Items[1].FinalFunds, decimal.NewFromInt(200))
|
||||
}
|
||||
}
|
||||
@@ -819,3 +848,138 @@ func TestMatchesCurrency(t *testing.T) {
|
||||
t.Error("expected false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSnapshot(t *testing.T) {
|
||||
f := FundManager{}
|
||||
f.CreateSnapshot(time.Time{})
|
||||
f.items = append(f.items, &Item{
|
||||
exchange: "",
|
||||
asset: "",
|
||||
currency: currency.Code{},
|
||||
initialFunds: decimal.Decimal{},
|
||||
available: decimal.Decimal{},
|
||||
reserved: decimal.Decimal{},
|
||||
transferFee: decimal.Decimal{},
|
||||
pairedWith: nil,
|
||||
usdTrackingCandles: nil,
|
||||
snapshot: nil,
|
||||
})
|
||||
f.CreateSnapshot(time.Time{})
|
||||
|
||||
dfk := &kline.DataFromKline{
|
||||
Item: gctkline.Item{
|
||||
Candles: []gctkline.Candle{
|
||||
{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := dfk.Load(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
f.items = append(f.items, &Item{
|
||||
exchange: "test",
|
||||
asset: asset.Spot,
|
||||
currency: currency.BTC,
|
||||
initialFunds: decimal.NewFromInt(1337),
|
||||
available: decimal.NewFromInt(1337),
|
||||
reserved: decimal.NewFromInt(1337),
|
||||
transferFee: decimal.NewFromInt(1337),
|
||||
usdTrackingCandles: dfk,
|
||||
})
|
||||
f.CreateSnapshot(dfk.Item.Candles[0].Time)
|
||||
}
|
||||
|
||||
func TestAddUSDTrackingData(t *testing.T) {
|
||||
f := FundManager{}
|
||||
err := f.AddUSDTrackingData(nil)
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received '%v' expected '%v'", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
err = f.AddUSDTrackingData(&kline.DataFromKline{})
|
||||
if !errors.Is(err, common.ErrNilArguments) {
|
||||
t.Errorf("received '%v' expected '%v'", err, common.ErrNilArguments)
|
||||
}
|
||||
|
||||
dfk := &kline.DataFromKline{
|
||||
Item: gctkline.Item{
|
||||
Candles: []gctkline.Candle{
|
||||
{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = dfk.Load()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
quoteItem, err := CreateItem(exch, a, pair.Quote, elite, decimal.Zero)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
err = f.AddItem(quoteItem)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
|
||||
f.disableUSDTracking = true
|
||||
err = f.AddUSDTrackingData(dfk)
|
||||
if !errors.Is(err, ErrUSDTrackingDisabled) {
|
||||
t.Errorf("received '%v' expected '%v'", err, ErrUSDTrackingDisabled)
|
||||
}
|
||||
|
||||
f.disableUSDTracking = false
|
||||
err = f.AddUSDTrackingData(dfk)
|
||||
if !errors.Is(err, errCannotMatchTrackingToItem) {
|
||||
t.Errorf("received '%v' expected '%v'", err, errCannotMatchTrackingToItem)
|
||||
}
|
||||
|
||||
dfk = &kline.DataFromKline{
|
||||
Item: gctkline.Item{
|
||||
Exchange: exch,
|
||||
Pair: currency.NewPair(pair.Quote, currency.USD),
|
||||
Asset: a,
|
||||
Interval: gctkline.OneHour,
|
||||
Candles: []gctkline.Candle{
|
||||
{
|
||||
Time: time.Now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = dfk.Load()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
err = f.AddUSDTrackingData(dfk)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
|
||||
usdtItem, err := CreateItem(exch, a, currency.USDT, elite, decimal.Zero)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
err = f.AddItem(usdtItem)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
err = f.AddUSDTrackingData(dfk)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUSDTrackingDisabled(t *testing.T) {
|
||||
f := FundManager{}
|
||||
if f.USDTrackingDisabled() {
|
||||
t.Error("received true, expected false")
|
||||
}
|
||||
f.disableUSDTracking = true
|
||||
if !f.USDTrackingDisabled() {
|
||||
t.Error("received false, expected true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
@@ -14,32 +15,10 @@ import (
|
||||
// currencies used in the backtester
|
||||
type FundManager struct {
|
||||
usingExchangeLevelFunding bool
|
||||
disableUSDTracking bool
|
||||
items []*Item
|
||||
}
|
||||
|
||||
// Report holds all funding data for result reporting
|
||||
type Report struct {
|
||||
InitialTotalUSD decimal.Decimal
|
||||
FinalTotalUSD decimal.Decimal
|
||||
Difference decimal.Decimal
|
||||
Items []ReportItem
|
||||
}
|
||||
|
||||
// ReportItem holds reporting fields
|
||||
type ReportItem struct {
|
||||
Exchange string
|
||||
Asset asset.Item
|
||||
Currency currency.Code
|
||||
InitialFunds decimal.Decimal
|
||||
InitialFundsUSD decimal.Decimal
|
||||
TransferFee decimal.Decimal
|
||||
FinalFunds decimal.Decimal
|
||||
FinalFundsUSD decimal.Decimal
|
||||
Difference decimal.Decimal
|
||||
ShowInfinite bool
|
||||
PairedWith currency.Code
|
||||
}
|
||||
|
||||
// IFundingManager limits funding usage for portfolio event handling
|
||||
type IFundingManager interface {
|
||||
Reset()
|
||||
@@ -48,7 +27,10 @@ type IFundingManager interface {
|
||||
GetFundingForEvent(common.EventHandler) (*Pair, error)
|
||||
GetFundingForEAP(string, asset.Item, currency.Pair) (*Pair, error)
|
||||
Transfer(decimal.Decimal, *Item, *Item, bool) error
|
||||
GenerateReport(startDate, endDate time.Time) *Report
|
||||
GenerateReport() *Report
|
||||
AddUSDTrackingData(*kline.DataFromKline) error
|
||||
CreateSnapshot(time.Time)
|
||||
USDTrackingDisabled() bool
|
||||
}
|
||||
|
||||
// IFundTransferer allows for funding amounts to be transferred
|
||||
@@ -85,14 +67,16 @@ type IPairReleaser interface {
|
||||
|
||||
// Item holds funding data per currency item
|
||||
type Item struct {
|
||||
exchange string
|
||||
asset asset.Item
|
||||
currency currency.Code
|
||||
initialFunds decimal.Decimal
|
||||
available decimal.Decimal
|
||||
reserved decimal.Decimal
|
||||
transferFee decimal.Decimal
|
||||
pairedWith *Item
|
||||
exchange string
|
||||
asset asset.Item
|
||||
currency currency.Code
|
||||
initialFunds decimal.Decimal
|
||||
available decimal.Decimal
|
||||
reserved decimal.Decimal
|
||||
transferFee decimal.Decimal
|
||||
pairedWith *Item
|
||||
usdTrackingCandles *kline.DataFromKline
|
||||
snapshot map[time.Time]ItemSnapshot
|
||||
}
|
||||
|
||||
// Pair holds two currencies that are associated with each other
|
||||
@@ -100,3 +84,40 @@ type Pair struct {
|
||||
Base *Item
|
||||
Quote *Item
|
||||
}
|
||||
|
||||
// Report holds all funding data for result reporting
|
||||
type Report struct {
|
||||
DisableUSDTracking bool
|
||||
UsingExchangeLevelFunding bool
|
||||
Items []ReportItem
|
||||
USDTotalsOverTime map[time.Time]ItemSnapshot
|
||||
}
|
||||
|
||||
// ReportItem holds reporting fields
|
||||
type ReportItem struct {
|
||||
Exchange string
|
||||
Asset asset.Item
|
||||
Currency currency.Code
|
||||
TransferFee decimal.Decimal
|
||||
InitialFunds decimal.Decimal
|
||||
FinalFunds decimal.Decimal
|
||||
USDInitialFunds decimal.Decimal
|
||||
USDInitialCostForOne decimal.Decimal
|
||||
USDFinalFunds decimal.Decimal
|
||||
USDFinalCostForOne decimal.Decimal
|
||||
Snapshots []ItemSnapshot
|
||||
|
||||
USDPairCandle *kline.DataFromKline
|
||||
Difference decimal.Decimal
|
||||
ShowInfinite bool
|
||||
PairedWith currency.Code
|
||||
}
|
||||
|
||||
// ItemSnapshot holds USD values to allow for tracking
|
||||
// across backtesting results
|
||||
type ItemSnapshot struct {
|
||||
Time time.Time
|
||||
Available decimal.Decimal
|
||||
USDClosePrice decimal.Decimal
|
||||
USDValue decimal.Decimal
|
||||
}
|
||||
|
||||
65
backtester/funding/trackingcurrencies/README.md
Normal file
65
backtester/funding/trackingcurrencies/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# GoCryptoTrader Backtester: Trackingcurrencies package
|
||||
|
||||
<img src="/backtester/common/backtester.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/funding/trackingcurrencies)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
|
||||
|
||||
|
||||
This trackingcurrencies package is part of the GoCryptoTrader codebase.
|
||||
|
||||
## This is still in active development
|
||||
|
||||
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
|
||||
|
||||
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
|
||||
|
||||
## Trackingcurrencies package overview
|
||||
|
||||
### What does the tracking currencies package do?
|
||||
The tracking currencies package is responsible breaking up a user's strategy currencies into pairs with a USD equivalent pair in order to track strategy performance against a singular currency. For example, you are wanting to backtest on Binance using XRP/DOGE, the tracking currencies will also retrieve XRP/BUSD and DOGE/BUSD pair data for use in calculating how much a currency is worth at every candle point.
|
||||
|
||||
### What if the exchange does not support USD?
|
||||
The tracking currencies package will check supported currencies against a list of USD equivalent USD backed stablecoins. So if your select exchange only supports BUSD or USDT based pairs, then the GoCryptoTrader Backtester will break up config pairs into the equivalent. See below list for currently supported stablecoin equivalency
|
||||
|
||||
| Currency |
|
||||
|----------|
|
||||
|USD |
|
||||
|USDT |
|
||||
|BUSD |
|
||||
|USDC |
|
||||
|DAI |
|
||||
|TUSD |
|
||||
|ZUSD |
|
||||
|PAX |
|
||||
|
||||
### How do I disable this?
|
||||
If you need to disable this functionality, for example, you are using Live, Database or CSV based trade data, then under `strategy-settings` in your config, set `disable-usd-tracking` to `true`
|
||||
|
||||
### Can I supply my own list of equivalent currencies instead of USD?
|
||||
This is currently not supported. If this is a feature you would like to have, please raise an issue on GitHub or in our Slack channel
|
||||
|
||||
### Please click GoDocs chevron above to view current GoDoc information for this package
|
||||
|
||||
## Contribution
|
||||
|
||||
Please feel free to submit any pull requests or suggest any desired features to be added.
|
||||
|
||||
When submitting a PR, please abide by our coding guidelines:
|
||||
|
||||
+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
|
||||
+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines.
|
||||
+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md).
|
||||
+ Pull requests need to be based on and opened against the `master` branch.
|
||||
|
||||
## Donations
|
||||
|
||||
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
|
||||
|
||||
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
|
||||
|
||||
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***
|
||||
154
backtester/funding/trackingcurrencies/trackingcurrencies.go
Normal file
154
backtester/funding/trackingcurrencies/trackingcurrencies.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package trackingcurrencies
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/engine"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCurrencyContainsUSD is raised when the currency already contains a USD equivalent
|
||||
ErrCurrencyContainsUSD = errors.New("currency already contains a USD equivalent")
|
||||
// ErrCurrencyDoesNotContainsUSD is raised when the currency does not contain a USD equivalent
|
||||
ErrCurrencyDoesNotContainsUSD = errors.New("currency does not contains a USD equivalent")
|
||||
errNilPairs = errors.New("cannot assess with nil available pairs")
|
||||
errNoMatchingPairUSDFound = errors.New("currency pair has no USD backed equivalent, cannot track price")
|
||||
errCurrencyNotFoundInPairs = errors.New("currency does not exist in available pairs")
|
||||
errNoMatchingBaseUSDFound = errors.New("base currency has no USD back equivalent, cannot track price")
|
||||
errNoMatchingQuoteUSDFound = errors.New("quote currency has no USD back equivalent, cannot track price")
|
||||
errNilPairsReceived = errors.New("nil tracking pairs received")
|
||||
errExchangeManagerRequired = errors.New("exchange manager required")
|
||||
)
|
||||
|
||||
// rankedUSDs is a slice of USD tracked currencies
|
||||
// to allow for totals tracking across a backtesting run
|
||||
var rankedUSDs = []currency.Code{
|
||||
currency.USDT,
|
||||
currency.BUSD,
|
||||
currency.USDC,
|
||||
currency.DAI,
|
||||
currency.USD,
|
||||
currency.TUSD,
|
||||
currency.ZUSD,
|
||||
currency.PAX,
|
||||
}
|
||||
|
||||
// TrackingPair is basic pair data used
|
||||
// to create more pairs based whether they contain
|
||||
// a USD equivalent
|
||||
type TrackingPair struct {
|
||||
Exchange string
|
||||
Asset string
|
||||
Base string
|
||||
Quote string
|
||||
}
|
||||
|
||||
// CreateUSDTrackingPairs is responsible for loading exchanges,
|
||||
// ensuring the exchange have the latest currency pairs and
|
||||
// if a pair doesn't have a USD currency to track price, to add those settings
|
||||
func CreateUSDTrackingPairs(tp []TrackingPair, em *engine.ExchangeManager) ([]TrackingPair, error) {
|
||||
if len(tp) == 0 {
|
||||
return nil, errNilPairsReceived
|
||||
}
|
||||
if em == nil {
|
||||
return nil, errExchangeManagerRequired
|
||||
}
|
||||
|
||||
var resp []TrackingPair
|
||||
for i := range tp {
|
||||
exch, err := em.GetExchangeByName(tp[i].Exchange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pair, err := currency.NewPairFromStrings(tp[i].Base, tp[i].Quote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pairContainsUSD(pair) {
|
||||
resp = append(resp, tp[i])
|
||||
} else {
|
||||
b := exch.GetBase()
|
||||
a, err := asset.New(tp[i].Asset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pairs := b.CurrencyPairs.Pairs[a]
|
||||
basePair, quotePair, err := findMatchingUSDPairs(pair, pairs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = append(resp,
|
||||
tp[i],
|
||||
TrackingPair{
|
||||
Exchange: tp[i].Exchange,
|
||||
Asset: tp[i].Asset,
|
||||
Base: basePair.Base.String(),
|
||||
Quote: basePair.Quote.String(),
|
||||
},
|
||||
TrackingPair{
|
||||
Exchange: tp[i].Exchange,
|
||||
Asset: tp[i].Asset,
|
||||
Base: quotePair.Base.String(),
|
||||
Quote: quotePair.Quote.String(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CurrencyIsUSDTracked checks if the currency passed in
|
||||
// tracks against USD value, ie is in rankedUSDs
|
||||
func CurrencyIsUSDTracked(code currency.Code) bool {
|
||||
for i := range rankedUSDs {
|
||||
if code == rankedUSDs[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pairContainsUSD is a simple check to ensure that the currency pair
|
||||
// has some sort of matching USD currency
|
||||
func pairContainsUSD(pair currency.Pair) bool {
|
||||
return CurrencyIsUSDTracked(pair.Base) || CurrencyIsUSDTracked(pair.Quote)
|
||||
}
|
||||
|
||||
// findMatchingUSDPairs will return a USD pair for both the base and quote currency provided
|
||||
// this will allow for data retrieval and total tracking on backtesting runs
|
||||
func findMatchingUSDPairs(pair currency.Pair, pairs *currency.PairStore) (basePair, quotePair currency.Pair, err error) {
|
||||
if pairs == nil {
|
||||
return currency.Pair{}, currency.Pair{}, errNilPairs
|
||||
}
|
||||
if pairContainsUSD(pair) {
|
||||
return currency.Pair{}, currency.Pair{}, ErrCurrencyContainsUSD
|
||||
}
|
||||
if !pairs.Available.Contains(pair, true) {
|
||||
return currency.Pair{}, currency.Pair{}, fmt.Errorf("%v %w", pair, errCurrencyNotFoundInPairs)
|
||||
}
|
||||
var baseFound, quoteFound bool
|
||||
|
||||
for i := range rankedUSDs {
|
||||
if !baseFound && pairs.Available.Contains(currency.NewPair(pair.Base, rankedUSDs[i]), true) {
|
||||
baseFound = true
|
||||
basePair = currency.NewPair(pair.Base, rankedUSDs[i])
|
||||
}
|
||||
if !quoteFound && pairs.Available.Contains(currency.NewPair(pair.Quote, rankedUSDs[i]), true) {
|
||||
quoteFound = true
|
||||
quotePair = currency.NewPair(pair.Quote, rankedUSDs[i])
|
||||
}
|
||||
}
|
||||
if !baseFound {
|
||||
err = fmt.Errorf("%v %w", pair.Base, errNoMatchingBaseUSDFound)
|
||||
}
|
||||
if !quoteFound {
|
||||
err = fmt.Errorf("%v %w", pair.Quote, errNoMatchingQuoteUSDFound)
|
||||
}
|
||||
if !baseFound && !quoteFound {
|
||||
err = fmt.Errorf("%v %v %w", pair.Base, pair.Quote, errNoMatchingPairUSDFound)
|
||||
}
|
||||
return basePair, quotePair, err
|
||||
}
|
||||
222
backtester/funding/trackingcurrencies/trackingcurrencies_test.go
Normal file
222
backtester/funding/trackingcurrencies/trackingcurrencies_test.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package trackingcurrencies
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/engine"
|
||||
)
|
||||
|
||||
var (
|
||||
exch = "binance"
|
||||
a = "spot"
|
||||
b = "BTC"
|
||||
q = "USDT"
|
||||
)
|
||||
|
||||
func TestCreateUSDTrackingPairs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := CreateUSDTrackingPairs(nil, nil)
|
||||
if !errors.Is(err, errNilPairsReceived) {
|
||||
t.Errorf("received '%v' expected '%v'", err, errNilPairsReceived)
|
||||
}
|
||||
|
||||
_, err = CreateUSDTrackingPairs([]TrackingPair{{}}, nil)
|
||||
if !errors.Is(err, errExchangeManagerRequired) {
|
||||
t.Errorf("received '%v' expected '%v'", err, errExchangeManagerRequired)
|
||||
}
|
||||
|
||||
em := engine.SetupExchangeManager()
|
||||
_, err = CreateUSDTrackingPairs([]TrackingPair{{Exchange: exch}}, em)
|
||||
if !errors.Is(err, engine.ErrExchangeNotFound) {
|
||||
t.Errorf("received '%v' expected '%v'", err, engine.ErrExchangeNotFound)
|
||||
}
|
||||
|
||||
s1 := TrackingPair{
|
||||
Exchange: exch,
|
||||
Asset: a,
|
||||
Base: b,
|
||||
Quote: q,
|
||||
}
|
||||
excher, err := em.NewExchangeByName(exch)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = excher.GetDefaultConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
em.Add(excher)
|
||||
resp, err := CreateUSDTrackingPairs([]TrackingPair{s1}, em)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if len(resp) != 1 {
|
||||
t.Error("expected 1 currency setting as it contains a USD equiv")
|
||||
}
|
||||
s1.Base = "LTC"
|
||||
s1.Quote = "BTC"
|
||||
resp, err = CreateUSDTrackingPairs([]TrackingPair{s1}, em)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if len(resp) != 3 {
|
||||
t.Error("expected 3 currency settings as it did not contain a USD equiv")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindMatchingUSDPairs(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testPair struct {
|
||||
description string
|
||||
initialPair currency.Pair
|
||||
availablePairs *currency.PairStore
|
||||
basePair currency.Pair
|
||||
quotePair currency.Pair
|
||||
expectedErr error
|
||||
}
|
||||
tests := []testPair{
|
||||
{
|
||||
description: "already has USD",
|
||||
initialPair: currency.NewPair(currency.BTC, currency.USDT),
|
||||
availablePairs: ¤cy.PairStore{Available: currency.Pairs{currency.NewPair(currency.BTC, currency.USDT)}},
|
||||
basePair: currency.Pair{},
|
||||
quotePair: currency.Pair{},
|
||||
expectedErr: ErrCurrencyContainsUSD,
|
||||
},
|
||||
{
|
||||
description: "successful",
|
||||
initialPair: currency.NewPair(currency.BTC, currency.LTC),
|
||||
availablePairs: ¤cy.PairStore{Available: currency.Pairs{currency.NewPair(currency.BTC, currency.LTC), currency.NewPair(currency.BTC, currency.USDT), currency.NewPair(currency.LTC, currency.TUSD)}},
|
||||
basePair: currency.NewPair(currency.BTC, currency.USDT),
|
||||
quotePair: currency.NewPair(currency.LTC, currency.TUSD),
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
description: "quote currency has no matching USD pair",
|
||||
initialPair: currency.NewPair(currency.BTC, currency.LTC),
|
||||
availablePairs: ¤cy.PairStore{Available: currency.Pairs{currency.NewPair(currency.BTC, currency.LTC), currency.NewPair(currency.BTC, currency.DAI)}},
|
||||
basePair: currency.NewPair(currency.BTC, currency.DAI),
|
||||
quotePair: currency.Pair{},
|
||||
expectedErr: errNoMatchingQuoteUSDFound,
|
||||
},
|
||||
{
|
||||
description: "base currency has no matching USD pair",
|
||||
initialPair: currency.NewPair(currency.BTC, currency.LTC),
|
||||
availablePairs: ¤cy.PairStore{Available: currency.Pairs{currency.NewPair(currency.BTC, currency.LTC), currency.NewPair(currency.LTC, currency.USDT)}},
|
||||
basePair: currency.Pair{},
|
||||
quotePair: currency.NewPair(currency.LTC, currency.USDT),
|
||||
expectedErr: errNoMatchingBaseUSDFound,
|
||||
},
|
||||
{
|
||||
description: "both base and quote don't have USD pairs",
|
||||
initialPair: currency.NewPair(currency.BTC, currency.LTC),
|
||||
availablePairs: ¤cy.PairStore{Available: currency.Pairs{currency.NewPair(currency.BTC, currency.LTC)}},
|
||||
basePair: currency.Pair{},
|
||||
quotePair: currency.Pair{},
|
||||
expectedErr: errNoMatchingPairUSDFound,
|
||||
},
|
||||
{
|
||||
description: "currency doesnt exist in available pairs",
|
||||
initialPair: currency.NewPair(currency.BTC, currency.LTC),
|
||||
availablePairs: ¤cy.PairStore{Available: currency.Pairs{currency.NewPair(currency.BTC, currency.DOGE)}},
|
||||
basePair: currency.Pair{},
|
||||
quotePair: currency.Pair{},
|
||||
expectedErr: errCurrencyNotFoundInPairs,
|
||||
},
|
||||
}
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
basePair, quotePair, err := findMatchingUSDPairs(tt.initialPair, tt.availablePairs)
|
||||
if !errors.Is(err, tt.expectedErr) {
|
||||
t.Fatalf("'%v' received '%v' expected '%v'", tt.description, err, tt.expectedErr)
|
||||
}
|
||||
if basePair != tt.basePair {
|
||||
t.Fatalf("'%v' received '%v' expected '%v'", tt.description, basePair, tt.basePair)
|
||||
}
|
||||
if quotePair != tt.quotePair {
|
||||
t.Fatalf("'%v' received '%v' expected '%v'", tt.description, quotePair, tt.quotePair)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPairContainsUSD(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testPair struct {
|
||||
description string
|
||||
expected bool
|
||||
pair currency.Pair
|
||||
}
|
||||
pairs := []testPair{
|
||||
{
|
||||
"btcusdt",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.USDT),
|
||||
},
|
||||
{
|
||||
"btcdoge",
|
||||
false,
|
||||
currency.NewPair(currency.BTC, currency.DOGE),
|
||||
},
|
||||
{
|
||||
"usdltc",
|
||||
true,
|
||||
currency.NewPair(currency.USD, currency.LTC),
|
||||
},
|
||||
{
|
||||
"btcdai",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.DAI),
|
||||
},
|
||||
{
|
||||
"btcbusd",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.BUSD),
|
||||
},
|
||||
{
|
||||
"btcusd",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.USD),
|
||||
},
|
||||
{
|
||||
"btcaud",
|
||||
false,
|
||||
currency.NewPair(currency.BTC, currency.AUD),
|
||||
},
|
||||
{
|
||||
"btcusdc",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.USDC),
|
||||
},
|
||||
{
|
||||
"btctusd",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.TUSD),
|
||||
},
|
||||
{
|
||||
"btczusd",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.ZUSD),
|
||||
},
|
||||
{
|
||||
"btcpax",
|
||||
true,
|
||||
currency.NewPair(currency.BTC, currency.PAX),
|
||||
},
|
||||
}
|
||||
for i := range pairs {
|
||||
tt := pairs[i]
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
resp := pairContainsUSD(tt.pair)
|
||||
if resp != tt.expected {
|
||||
t.Errorf("expected %v received %v", tt, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/backtest"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
gctconfig "github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/engine"
|
||||
gctlog "github.com/thrasher-corp/gocryptotrader/log"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
"github.com/thrasher-corp/gocryptotrader/signaler"
|
||||
)
|
||||
|
||||
@@ -61,12 +59,15 @@ func main() {
|
||||
&darkReport,
|
||||
"darkreport",
|
||||
false,
|
||||
"sets the initial rerport to use a dark theme")
|
||||
"sets the output report to use a dark theme by default")
|
||||
flag.Parse()
|
||||
|
||||
var bt *backtest.BackTest
|
||||
var cfg *config.Config
|
||||
fmt.Println("reading config...")
|
||||
logConfig := log.GenDefaultSettings()
|
||||
log.GlobalLogConfig = &logConfig
|
||||
log.SetupGlobalLogger()
|
||||
|
||||
cfg, err = config.ReadConfigFromFile(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not read config. Error: %v.\n", err)
|
||||
@@ -76,36 +77,12 @@ func main() {
|
||||
fmt.Print(common.ASCIILogo)
|
||||
}
|
||||
|
||||
path := gctconfig.DefaultFilePath()
|
||||
if cfg.GoCryptoTraderConfigPath != "" {
|
||||
path = cfg.GoCryptoTraderConfigPath
|
||||
}
|
||||
|
||||
var bot *engine.Engine
|
||||
flags := map[string]bool{
|
||||
"tickersync": false,
|
||||
"orderbooksync": false,
|
||||
"tradesync": false,
|
||||
"ratelimiter": true,
|
||||
"ordermanager": false,
|
||||
}
|
||||
bot, err = engine.NewFromSettings(&engine.Settings{
|
||||
ConfigFile: path,
|
||||
EnableDryRun: true,
|
||||
EnableAllPairs: true,
|
||||
EnableExchangeHTTPRateLimiter: true,
|
||||
}, flags)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not load backtester. Error: %v.\n", err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
fmt.Printf("Could not read config. Error: %v.\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
bt, err = backtest.NewFromConfig(cfg, templatePath, reportOutput, bot)
|
||||
bt, err = backtest.NewFromConfig(cfg, templatePath, reportOutput)
|
||||
if err != nil {
|
||||
fmt.Printf("Could not setup backtester from config. Error: %v.\n", err)
|
||||
os.Exit(1)
|
||||
@@ -119,7 +96,7 @@ func main() {
|
||||
}
|
||||
}()
|
||||
interrupt := signaler.WaitForInterrupt()
|
||||
gctlog.Infof(gctlog.Global, "Captured %v, shutdown requested.\n", interrupt)
|
||||
log.Infof(log.Global, "Captured %v, shutdown requested.\n", interrupt)
|
||||
bt.Stop()
|
||||
} else {
|
||||
err = bt.Run()
|
||||
@@ -129,9 +106,9 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
err = bt.Statistic.CalculateAllResults(bt.Funding)
|
||||
err = bt.Statistic.CalculateAllResults()
|
||||
if err != nil {
|
||||
gctlog.Error(gctlog.BackTester, err)
|
||||
log.Error(log.BackTester, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -139,7 +116,7 @@ func main() {
|
||||
bt.Reports.UseDarkMode(darkReport)
|
||||
err = bt.Reports.GenerateReport()
|
||||
if err != nil {
|
||||
gctlog.Error(gctlog.BackTester, err)
|
||||
log.Error(log.BackTester, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ func (d *Data) GenerateReport() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range d.OriginalCandles {
|
||||
for j := range d.OriginalCandles[i].Candles {
|
||||
if d.OriginalCandles[i].Candles[j].ValidationIssues == "" {
|
||||
@@ -43,6 +42,8 @@ func (d *Data) GenerateReport() error {
|
||||
d.EnhancedCandles[i].Candles = d.EnhancedCandles[i].Candles[:maxChartLimit]
|
||||
}
|
||||
}
|
||||
d.USDTotalsChart = d.CreateUSDTotalsChart()
|
||||
d.HoldingsOverTimeChart = d.CreateHoldingsOverTimeChart()
|
||||
|
||||
tmpl := template.Must(
|
||||
template.ParseFiles(
|
||||
@@ -82,6 +83,66 @@ func (d *Data) GenerateReport() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateUSDTotalsChart used for creating a chart in the HTML report
|
||||
// to show how much the overall assets are worth over time
|
||||
func (d *Data) CreateUSDTotalsChart() []TotalsChart {
|
||||
if d.Statistics.FundingStatistics == nil || d.Statistics.FundingStatistics.Report.DisableUSDTracking {
|
||||
return nil
|
||||
}
|
||||
var response []TotalsChart
|
||||
var usdTotalChartPlot []ChartPlot
|
||||
for i := range d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues {
|
||||
usdTotalChartPlot = append(usdTotalChartPlot, ChartPlot{
|
||||
Value: d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues[i].Value.InexactFloat64(),
|
||||
UnixMilli: d.Statistics.FundingStatistics.TotalUSDStatistics.HoldingValues[i].Time.UTC().UnixMilli(),
|
||||
})
|
||||
}
|
||||
response = append(response, TotalsChart{
|
||||
Name: "Total USD value",
|
||||
DataPoints: usdTotalChartPlot,
|
||||
})
|
||||
|
||||
for i := range d.Statistics.FundingStatistics.Items {
|
||||
var plots []ChartPlot
|
||||
for j := range d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots {
|
||||
plots = append(plots, ChartPlot{
|
||||
Value: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].USDValue.InexactFloat64(),
|
||||
UnixMilli: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].Time.UTC().UnixMilli(),
|
||||
})
|
||||
}
|
||||
response = append(response, TotalsChart{
|
||||
Name: fmt.Sprintf("%v %v %v USD value", d.Statistics.FundingStatistics.Items[i].ReportItem.Exchange, d.Statistics.FundingStatistics.Items[i].ReportItem.Asset, d.Statistics.FundingStatistics.Items[i].ReportItem.Currency),
|
||||
DataPoints: plots,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// CreateHoldingsOverTimeChart used for creating a chart in the HTML report
|
||||
// to show how many holdings of each type was held over the time of backtesting
|
||||
func (d *Data) CreateHoldingsOverTimeChart() []TotalsChart {
|
||||
if d.Statistics.FundingStatistics == nil {
|
||||
return nil
|
||||
}
|
||||
var response []TotalsChart
|
||||
for i := range d.Statistics.FundingStatistics.Items {
|
||||
var plots []ChartPlot
|
||||
for j := range d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots {
|
||||
plots = append(plots, ChartPlot{
|
||||
Value: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].Available.InexactFloat64(),
|
||||
UnixMilli: d.Statistics.FundingStatistics.Items[i].ReportItem.Snapshots[j].Time.UTC().UnixMilli(),
|
||||
})
|
||||
}
|
||||
response = append(response, TotalsChart{
|
||||
Name: fmt.Sprintf("%v %v %v holdings", d.Statistics.FundingStatistics.Items[i].ReportItem.Exchange, d.Statistics.FundingStatistics.Items[i].ReportItem.Asset, d.Statistics.FundingStatistics.Items[i].ReportItem.Currency),
|
||||
DataPoints: plots,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// AddKlineItem appends a SET of candles for the report to enhance upon
|
||||
// generation
|
||||
func (d *Data) AddKlineItem(k *kline.Item) {
|
||||
@@ -133,12 +194,12 @@ func (d *Data) enhanceCandles() error {
|
||||
_, offset := time.Now().Zone()
|
||||
tt := d.OriginalCandles[intVal].Candles[j].Time.Add(time.Duration(offset) * time.Second)
|
||||
enhancedCandle := DetailedCandle{
|
||||
Time: tt.Unix(),
|
||||
Open: decimal.NewFromFloat(d.OriginalCandles[intVal].Candles[j].Open),
|
||||
High: decimal.NewFromFloat(d.OriginalCandles[intVal].Candles[j].High),
|
||||
Low: decimal.NewFromFloat(d.OriginalCandles[intVal].Candles[j].Low),
|
||||
Close: decimal.NewFromFloat(d.OriginalCandles[intVal].Candles[j].Close),
|
||||
Volume: decimal.NewFromFloat(d.OriginalCandles[intVal].Candles[j].Volume),
|
||||
UnixMilli: tt.UTC().UnixMilli(),
|
||||
Open: d.OriginalCandles[intVal].Candles[j].Open,
|
||||
High: d.OriginalCandles[intVal].Candles[j].High,
|
||||
Low: d.OriginalCandles[intVal].Candles[j].Low,
|
||||
Close: d.OriginalCandles[intVal].Candles[j].Close,
|
||||
Volume: d.OriginalCandles[intVal].Candles[j].Volume,
|
||||
VolumeColour: "rgba(50, 204, 30, 0.5)",
|
||||
}
|
||||
if j != 0 {
|
||||
@@ -169,7 +230,7 @@ func (d *Data) enhanceCandles() error {
|
||||
// an order was placed here, can enhance chart!
|
||||
enhancedCandle.MadeOrder = true
|
||||
enhancedCandle.OrderAmount = decimal.NewFromFloat(statsForCandles.FinalOrders.Orders[k].Amount)
|
||||
enhancedCandle.PurchasePrice = decimal.NewFromFloat(statsForCandles.FinalOrders.Orders[k].Price)
|
||||
enhancedCandle.PurchasePrice = statsForCandles.FinalOrders.Orders[k].Price
|
||||
enhancedCandle.OrderDirection = statsForCandles.FinalOrders.Orders[k].Side
|
||||
if enhancedCandle.OrderDirection == order.Buy {
|
||||
enhancedCandle.Colour = "green"
|
||||
@@ -197,7 +258,6 @@ func (d *DetailedCandle) copyCloseFromPreviousEvent(enhancedKline *DetailedKline
|
||||
d.High = enhancedKline.Candles[len(enhancedKline.Candles)-1].Close
|
||||
d.Low = enhancedKline.Candles[len(enhancedKline.Candles)-1].Close
|
||||
d.Close = enhancedKline.Candles[len(enhancedKline.Candles)-1].Close
|
||||
|
||||
d.Colour = "white"
|
||||
d.Position = "aboveBar"
|
||||
d.Shape = "arrowDown"
|
||||
|
||||
@@ -13,7 +13,6 @@ 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/eventhandlers/statistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics/currencystatistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
@@ -66,12 +65,13 @@ func TestGenerateReport(t *testing.T) {
|
||||
Watermark: "Binance - SPOT - BTC-USDT",
|
||||
Candles: []DetailedCandle{
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 5).Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
VolumeColour: "rgba(47, 194, 27, 0.8)",
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -79,16 +79,15 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
VolumeColour: "rgba(47, 194, 27, 0.8)",
|
||||
PurchasePrice: 50,
|
||||
},
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 4).Unix(),
|
||||
Open: decimal.NewFromInt(1332),
|
||||
High: decimal.NewFromInt(1332),
|
||||
Low: decimal.NewFromInt(1330),
|
||||
Close: decimal.NewFromInt(1331),
|
||||
Volume: decimal.NewFromInt(2),
|
||||
UnixMilli: time.Date(2020, 12, 12, 1, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1332,
|
||||
High: 1332,
|
||||
Low: 1330,
|
||||
Close: 1331,
|
||||
Volume: 2,
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -96,16 +95,16 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
PurchasePrice: 50,
|
||||
VolumeColour: "rgba(252, 3, 3, 0.8)",
|
||||
},
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 3).Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 2, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -113,16 +112,16 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
PurchasePrice: 50,
|
||||
VolumeColour: "rgba(47, 194, 27, 0.8)",
|
||||
},
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 2).Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 3, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -130,16 +129,16 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
PurchasePrice: 50,
|
||||
VolumeColour: "rgba(252, 3, 3, 0.8)",
|
||||
},
|
||||
{
|
||||
Time: time.Now().Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 4, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
VolumeColour: "rgba(47, 194, 27, 0.8)",
|
||||
},
|
||||
},
|
||||
@@ -152,12 +151,12 @@ func TestGenerateReport(t *testing.T) {
|
||||
Watermark: "BITTREX - SPOT - BTC-USD - 1d",
|
||||
Candles: []DetailedCandle{
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 5).Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -165,16 +164,16 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
PurchasePrice: 50,
|
||||
VolumeColour: "rgba(47, 194, 27, 0.8)",
|
||||
},
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 4).Unix(),
|
||||
Open: decimal.NewFromInt(1332),
|
||||
High: decimal.NewFromInt(1332),
|
||||
Low: decimal.NewFromInt(1330),
|
||||
Close: decimal.NewFromInt(1331),
|
||||
Volume: decimal.NewFromInt(2),
|
||||
UnixMilli: time.Date(2020, 12, 12, 1, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1332,
|
||||
High: 1332,
|
||||
Low: 1330,
|
||||
Close: 1331,
|
||||
Volume: 2,
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -182,16 +181,16 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
PurchasePrice: 50,
|
||||
VolumeColour: "rgba(252, 3, 3, 0.8)",
|
||||
},
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 3).Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 2, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -199,16 +198,16 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
PurchasePrice: 50,
|
||||
VolumeColour: "rgba(47, 194, 27, 0.8)",
|
||||
},
|
||||
{
|
||||
Time: time.Now().Add(-time.Hour * 2).Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 3, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
MadeOrder: true,
|
||||
OrderDirection: gctorder.Buy,
|
||||
OrderAmount: decimal.NewFromInt(1337),
|
||||
@@ -216,44 +215,44 @@ func TestGenerateReport(t *testing.T) {
|
||||
Text: "hi",
|
||||
Position: "aboveBar",
|
||||
Colour: "green",
|
||||
PurchasePrice: decimal.NewFromInt(50),
|
||||
PurchasePrice: 50,
|
||||
VolumeColour: "rgba(252, 3, 3, 0.8)",
|
||||
},
|
||||
{
|
||||
Time: time.Now().Unix(),
|
||||
Open: decimal.NewFromInt(1337),
|
||||
High: decimal.NewFromInt(1339),
|
||||
Low: decimal.NewFromInt(1336),
|
||||
Close: decimal.NewFromInt(1338),
|
||||
Volume: decimal.NewFromInt(3),
|
||||
UnixMilli: time.Date(2020, 12, 12, 4, 0, 0, 0, time.UTC).UnixMilli(),
|
||||
Open: 1337,
|
||||
High: 1339,
|
||||
Low: 1336,
|
||||
Close: 1338,
|
||||
Volume: 3,
|
||||
VolumeColour: "rgba(47, 194, 27, 0.8)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Statistics: &statistics.Statistic{
|
||||
Funding: &funding.Report{},
|
||||
StrategyName: "testStrat",
|
||||
ExchangeAssetPairStatistics: map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic{
|
||||
RiskFreeRate: decimal.NewFromFloat(0.03),
|
||||
ExchangeAssetPairStatistics: map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic{
|
||||
e: {
|
||||
a: {
|
||||
p: ¤cystatistics.CurrencyStatistic{
|
||||
MaxDrawdown: currencystatistics.Swing{},
|
||||
p: &statistics.CurrencyPairStatistic{
|
||||
MaxDrawdown: statistics.Swing{},
|
||||
LowestClosePrice: decimal.NewFromInt(100),
|
||||
HighestClosePrice: decimal.NewFromInt(200),
|
||||
MarketMovement: decimal.NewFromInt(100),
|
||||
StrategyMovement: decimal.NewFromInt(100),
|
||||
RiskFreeRate: decimal.NewFromInt(1),
|
||||
CompoundAnnualGrowthRate: decimal.NewFromInt(1),
|
||||
BuyOrders: 1,
|
||||
SellOrders: 1,
|
||||
FinalHoldings: holdings.Holding{},
|
||||
FinalOrders: compliance.Snapshot{},
|
||||
ArithmeticRatios: &statistics.Ratios{},
|
||||
GeometricRatios: &statistics.Ratios{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RiskFreeRate: decimal.NewFromFloat(0.03),
|
||||
TotalBuyOrders: 1337,
|
||||
TotalSellOrders: 1330,
|
||||
TotalOrders: 200,
|
||||
@@ -261,14 +260,14 @@ func TestGenerateReport(t *testing.T) {
|
||||
Exchange: e,
|
||||
Asset: a,
|
||||
Pair: p,
|
||||
MaxDrawdown: currencystatistics.Swing{
|
||||
Highest: currencystatistics.Iteration{
|
||||
MaxDrawdown: statistics.Swing{
|
||||
Highest: statistics.ValueAtTime{
|
||||
Time: time.Now(),
|
||||
Price: decimal.NewFromInt(1337),
|
||||
Value: decimal.NewFromInt(1337),
|
||||
},
|
||||
Lowest: currencystatistics.Iteration{
|
||||
Lowest: statistics.ValueAtTime{
|
||||
Time: time.Now(),
|
||||
Price: decimal.NewFromInt(137),
|
||||
Value: decimal.NewFromInt(137),
|
||||
},
|
||||
DrawdownPercent: decimal.NewFromInt(100),
|
||||
},
|
||||
@@ -279,14 +278,14 @@ func TestGenerateReport(t *testing.T) {
|
||||
Exchange: e,
|
||||
Asset: a,
|
||||
Pair: p,
|
||||
MaxDrawdown: currencystatistics.Swing{
|
||||
Highest: currencystatistics.Iteration{
|
||||
MaxDrawdown: statistics.Swing{
|
||||
Highest: statistics.ValueAtTime{
|
||||
Time: time.Now(),
|
||||
Price: decimal.NewFromInt(1337),
|
||||
Value: decimal.NewFromInt(1337),
|
||||
},
|
||||
Lowest: currencystatistics.Iteration{
|
||||
Lowest: statistics.ValueAtTime{
|
||||
Time: time.Now(),
|
||||
Price: decimal.NewFromInt(137),
|
||||
Value: decimal.NewFromInt(137),
|
||||
},
|
||||
DrawdownPercent: decimal.NewFromInt(100),
|
||||
},
|
||||
@@ -297,23 +296,32 @@ func TestGenerateReport(t *testing.T) {
|
||||
Exchange: e,
|
||||
Asset: a,
|
||||
Pair: p,
|
||||
MaxDrawdown: currencystatistics.Swing{
|
||||
Highest: currencystatistics.Iteration{
|
||||
MaxDrawdown: statistics.Swing{
|
||||
Highest: statistics.ValueAtTime{
|
||||
Time: time.Now(),
|
||||
Price: decimal.NewFromInt(1337),
|
||||
Value: decimal.NewFromInt(1337),
|
||||
},
|
||||
Lowest: currencystatistics.Iteration{
|
||||
Lowest: statistics.ValueAtTime{
|
||||
Time: time.Now(),
|
||||
Price: decimal.NewFromInt(137),
|
||||
Value: decimal.NewFromInt(137),
|
||||
},
|
||||
DrawdownPercent: decimal.NewFromInt(100),
|
||||
},
|
||||
MarketMovement: decimal.NewFromInt(1337),
|
||||
StrategyMovement: decimal.NewFromInt(1337),
|
||||
},
|
||||
CurrencyPairStatistics: nil,
|
||||
WasAnyDataMissing: false,
|
||||
FundingStatistics: nil,
|
||||
},
|
||||
}
|
||||
d.OutputPath = tempDir
|
||||
d.Config.StrategySettings.DisableUSDTracking = true
|
||||
d.Statistics.FundingStatistics = &statistics.FundingStatistics{
|
||||
Report: &funding.Report{
|
||||
DisableUSDTracking: true,
|
||||
},
|
||||
}
|
||||
err = d.GenerateReport()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -339,10 +347,10 @@ func TestEnhanceCandles(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
d.Statistics.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
d.Statistics.ExchangeAssetPairStatistics[testExchange] = make(map[asset.Item]map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot] = make(map[currency.Pair]*currencystatistics.CurrencyStatistic)
|
||||
d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.NewPair(currency.BTC, currency.USDT)] = ¤cystatistics.CurrencyStatistic{}
|
||||
d.Statistics.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
d.Statistics.ExchangeAssetPairStatistics[testExchange] = make(map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot] = make(map[currency.Pair]*statistics.CurrencyPairStatistic)
|
||||
d.Statistics.ExchangeAssetPairStatistics[testExchange][asset.Spot][currency.NewPair(currency.BTC, currency.USDT)] = &statistics.CurrencyPairStatistic{}
|
||||
|
||||
d.AddKlineItem(&gctkline.Item{
|
||||
Exchange: testExchange,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
@@ -30,14 +31,32 @@ type Handler interface {
|
||||
|
||||
// Data holds all statistical information required to output detailed backtesting results
|
||||
type Data struct {
|
||||
OriginalCandles []*kline.Item
|
||||
EnhancedCandles []DetailedKline
|
||||
Statistics *statistics.Statistic
|
||||
Config *config.Config
|
||||
TemplatePath string
|
||||
OutputPath string
|
||||
Warnings []Warning
|
||||
UseDarkTheme bool
|
||||
OriginalCandles []*kline.Item
|
||||
EnhancedCandles []DetailedKline
|
||||
Statistics *statistics.Statistic
|
||||
Config *config.Config
|
||||
TemplatePath string
|
||||
OutputPath string
|
||||
Warnings []Warning
|
||||
UseDarkTheme bool
|
||||
USDTotalsChart []TotalsChart
|
||||
HoldingsOverTimeChart []TotalsChart
|
||||
Prettify PrettyNumbers
|
||||
}
|
||||
|
||||
// TotalsChart holds chart plot data
|
||||
// to render charts in the report
|
||||
type TotalsChart struct {
|
||||
Name string
|
||||
DataPoints []ChartPlot
|
||||
}
|
||||
|
||||
// ChartPlot holds value data
|
||||
// for a chart
|
||||
type ChartPlot struct {
|
||||
Value float64
|
||||
UnixMilli int64
|
||||
Flag string
|
||||
}
|
||||
|
||||
// Warning holds any candle warnings
|
||||
@@ -61,12 +80,12 @@ type DetailedKline struct {
|
||||
|
||||
// DetailedCandle contains extra details to enable rich reporting results
|
||||
type DetailedCandle struct {
|
||||
Time int64
|
||||
Open decimal.Decimal
|
||||
High decimal.Decimal
|
||||
Low decimal.Decimal
|
||||
Close decimal.Decimal
|
||||
Volume decimal.Decimal
|
||||
UnixMilli int64
|
||||
Open float64
|
||||
High float64
|
||||
Low float64
|
||||
Close float64
|
||||
Volume float64
|
||||
VolumeColour string
|
||||
MadeOrder bool
|
||||
OrderDirection order.Side
|
||||
@@ -75,5 +94,36 @@ type DetailedCandle struct {
|
||||
Text string
|
||||
Position string
|
||||
Colour string
|
||||
PurchasePrice decimal.Decimal
|
||||
PurchasePrice float64
|
||||
}
|
||||
|
||||
// PrettyNumbers is used for report rendering
|
||||
// one cannot access packages when rendering data in a template
|
||||
// this struct exists purely to help make numbers look pretty
|
||||
type PrettyNumbers struct{}
|
||||
|
||||
// Decimal2 renders a decimal nicely with 2 decimal places
|
||||
func (p *PrettyNumbers) Decimal2(d decimal.Decimal) string {
|
||||
return convert.DecimalToHumanFriendlyString(d, 2, ".", ",")
|
||||
}
|
||||
|
||||
// Decimal8 renders a decimal nicely with 8 decimal places
|
||||
func (p *PrettyNumbers) Decimal8(d decimal.Decimal) string {
|
||||
return convert.DecimalToHumanFriendlyString(d, 8, ".", ",")
|
||||
}
|
||||
|
||||
// Decimal64 renders a decimal nicely with the idea not to limit decimal places
|
||||
// and to make you nostalgic for Nintendo
|
||||
func (p *PrettyNumbers) Decimal64(d decimal.Decimal) string {
|
||||
return convert.DecimalToHumanFriendlyString(d, 64, ".", ",")
|
||||
}
|
||||
|
||||
// Float8 renders a float nicely with 8 decimal places
|
||||
func (p *PrettyNumbers) Float8(f float64) string {
|
||||
return convert.FloatToHumanFriendlyString(f, 8, ".", ",")
|
||||
}
|
||||
|
||||
// Int renders an int nicely
|
||||
func (p *PrettyNumbers) Int(i int64) string {
|
||||
return convert.IntToHumanFriendlyString(i, ",")
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ See below for a set of tables and fields, expected values and what they can do
|
||||
| CustomSettings | This is a map where you can enter custom settings for a strategy. The RSI strategy allows for customisation of the upper, lower and length variables to allow you to change them from 70, 30 and 14 respectively to 69, 36, 12 | `"custom-settings": { "rsi-high": 70, "rsi-low": 30, "rsi-period": 14 } ` |
|
||||
| UseExchangeLevelFunding | Allows shared funding at an exchange asset level. You can set funding for `USDT` and all pairs that feature `USDT` will have access to those funds when making orders. See [this](/backtester/funding/README.md) for more information | `false` |
|
||||
| ExchangeLevelFunding | An array of exchange level funding settings. See below, or [this](/backtester/funding/README.md) for more information | `[]` |
|
||||
| DisableUSDTracking | If `false`, will track all currencies used in your strategy against USD equivalent candles. For example, if you are running a strategy for BTC/XRP, then the GoCryptoTrader Backtester will also retreive candles data for BTC/USD and XRP/USD to then track strategy performance against a single currency. This also tracks against USDT and other USD tracked stablecoins, so one exchange supporting USDT and another BUSD will still allow unified strategy performance analysis. If disabled, will not track against USD, this can be especially helpful when running strategies under live, database and CSV based data | `false` |
|
||||
|
||||
##### Funding Config Settings
|
||||
|
||||
@@ -114,9 +115,30 @@ See below for a set of tables and fields, expected values and what they can do
|
||||
| Interval | The candle interval in `time.Duration` format eg set as`15000000000` for a value of `time.Second * 15` | `15000000000` |
|
||||
| StartDate | The start date to retrieve data | `2021-01-23T11:00:00+11:00` |
|
||||
| EndDate | The end date to retrieve data | `2021-01-24T11:00:00+11:00` |
|
||||
| ConfigOverride | Override GoCryptoTrader's config database data with custom settings | `true` |
|
||||
| Config | This is the same struct used as your GoCryptoTrader database config. See below tables for breakdown | `see below` |
|
||||
| Path | If using SQLite, the path to the directory, not the file. Leaving blank will use GoCryptoTrader's default database path | `` |
|
||||
| InclusiveEndDate | When enabled, the end date's candle is included in the results. ie `2021-01-24T11:00:00+11:00` with a one hour candle, the final candle will be `2021-01-24T11:00:00+11:00` to `2021-01-24T12:00:00+11:00` | `false` |
|
||||
|
||||
##### database
|
||||
|
||||
| Config | Description | Example |
|
||||
| ------ | ----------- | ------- |
|
||||
| enabled | Enabled or disables the database connection subsystem | `true` |
|
||||
| verbose | Displays more information to the logger which can be helpful for debugging | `false` |
|
||||
| driver | The SQL driver to use. Can be `postgres` or `sqlite` | `sqlite` |
|
||||
| connectionDetails | See below | |
|
||||
|
||||
##### connectionDetails
|
||||
|
||||
| Config | Description | Example |
|
||||
| ------ | ----------- | ------- |
|
||||
| host | The host address of the database | `localhost` |
|
||||
| port | The port used to connect to the database | `5432` |
|
||||
| username | An optional username to connect to the database | `username` |
|
||||
| password | An optional password to connect to the database | `password` |
|
||||
| database | The name of the database | `database.db` |
|
||||
| sslmode | The connection type of the database for Postgres databases only | `disable` |
|
||||
|
||||
#### LiveData
|
||||
|
||||
| Key | Description | Example |
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{{define "backtester eventhandlers statistics currencystatistics" -}}
|
||||
{{template "backtester-header" .}}
|
||||
## {{.CapitalName}} package overview
|
||||
|
||||
Currency Statistics is an important package to verify the effectiveness of your strategies.
|
||||
It can calculate the following:
|
||||
- Calmar ratio
|
||||
- Information ratio
|
||||
- Sharpe ratio
|
||||
- Sortino ratio
|
||||
- CAGR
|
||||
- Drawdowns, both the biggest and longest
|
||||
- Whether the strategy outperformed the market
|
||||
- If the strategy made a profit
|
||||
|
||||
## Ratios
|
||||
|
||||
| Ratio | Description | A good range |
|
||||
| ----- | ----------- | ------------ |
|
||||
| Calmar ratio | It is a function of the fund's average compounded annual rate of return versus its maximum drawdown. The higher the Calmar ratio, the better it performed on a risk-adjusted basis during the given time frame, which is mostly commonly set at 36 months | 3.0 to 5.0 |
|
||||
| Information ratio| It is a measurement of portfolio returns beyond the returns of a benchmark, usually an index, compared to the volatility of those returns. The ratio is often used as a measure of a portfolio manager's level of skill and ability to generate excess returns relative to a benchmark | 0.40-0.60. Any positive number means that it has beaten the benchmark |
|
||||
| Sharpe ratio | The Sharpe Ratio is a financial metric often used by investors when assessing the performance of investment management products and professionals. It consists of taking the excess return of the portfolio, relative to the risk-free rate, and dividing it by the standard deviation of the portfolio's excess returns | Any Sharpe ratio greater than 1.0 is good. Higher than 2.0 is very good. 3.0 or higher is excellent. Under 1.0 is sub-optimal |
|
||||
| Sortino ratio | The Sortino ratio measures the risk-adjusted return of an investment asset, portfolio, or strategy. It is a modification of the Sharpe ratio but penalizes only those returns falling below a user-specified target or required rate of return, while the Sharpe ratio penalizes both upside and downside volatility equally | The higher the better, but > 2 is considered good |
|
||||
| Compound annual growth rate | Compound annual growth rate is the rate of return that would be required for an investment to grow from its beginning balance to its ending balance, assuming the profits were reinvested at the end of each year of the investment’s lifespan | Any positive number |
|
||||
|
||||
## Arithmetic or versus geometric?
|
||||
Both! We calculate ratios where an average is required using both types. The reasoning for using either is debated by finance and mathematicians. [This](https://www.investopedia.com/ask/answers/06/geometricmean.asp) is a good breakdown of both, but here is an extra simple table
|
||||
|
||||
| Average type | A reason to use it |
|
||||
| ------------ | ------------------ |
|
||||
| Arithmetic | The arithmetic mean is the average of a sum of numbers, which reflects the central tendency of the position of the numbers |
|
||||
| Geometric | The geometric mean differs from the arithmetic average, or arithmetic mean, in how it is calculated because it takes into account the compounding that occurs from period to period. Because of this, investors usually consider the geometric mean a more accurate measure of returns than the arithmetic mean |
|
||||
|
||||
|
||||
|
||||
### Please click GoDocs chevron above to view current GoDoc information for this package
|
||||
{{template "contributions"}}
|
||||
{{template "donations" .}}
|
||||
{{end}}
|
||||
@@ -5,6 +5,36 @@
|
||||
The statistics package is used for storing all relevant data over the course of a GoCryptoTrader Backtesting run. All types of events are tracked by exchange, asset and currency pair.
|
||||
When multiple currencies are included in your strategy, the statistics package will be able to calculate which exchange asset currency pair has performed the best, along with the biggest drop downs in the market.
|
||||
|
||||
It can calculate the following:
|
||||
- Calmar ratio
|
||||
- Information ratio
|
||||
- Sharpe ratio
|
||||
- Sortino ratio
|
||||
- CAGR
|
||||
- Drawdowns, both the biggest and longest
|
||||
- Whether the strategy outperformed the market
|
||||
- If the strategy made a profit
|
||||
|
||||
## Ratios
|
||||
|
||||
| Ratio | Description | A good range |
|
||||
| ----- | ----------- | ------------ |
|
||||
| Calmar ratio | It is a function of the fund's average compounded annual rate of return versus its maximum drawdown. The higher the Calmar ratio, the better it performed on a risk-adjusted basis during the given time frame, which is mostly commonly set at 36 months | 3.0 to 5.0 |
|
||||
| Information ratio| It is a measurement of portfolio returns beyond the returns of a benchmark, usually an index, compared to the volatility of those returns. The ratio is often used as a measure of a portfolio manager's level of skill and ability to generate excess returns relative to a benchmark | 0.40-0.60. Any positive number means that it has beaten the benchmark |
|
||||
| Sharpe ratio | The Sharpe Ratio is a financial metric often used by investors when assessing the performance of investment management products and professionals. It consists of taking the excess return of the portfolio, relative to the risk-free rate, and dividing it by the standard deviation of the portfolio's excess returns | Any Sharpe ratio greater than 1.0 is good. Higher than 2.0 is very good. 3.0 or higher is excellent. Under 1.0 is sub-optimal |
|
||||
| Sortino ratio | The Sortino ratio measures the risk-adjusted return of an investment asset, portfolio, or strategy. It is a modification of the Sharpe ratio but penalizes only those returns falling below a user-specified target or required rate of return, while the Sharpe ratio penalizes both upside and downside volatility equally | The higher the better, but > 2 is considered good |
|
||||
| Compound annual growth rate | Compound annual growth rate is the rate of return that would be required for an investment to grow from its beginning balance to its ending balance, assuming the profits were reinvested at the end of each year of the investment’s lifespan | Any positive number |
|
||||
|
||||
## Arithmetic or versus geometric?
|
||||
Both! We calculate ratios where an average is required using both types. The reasoning for using either is debated by finance and mathematicians. [This](https://www.investopedia.com/ask/answers/06/geometricmean.asp) is a good breakdown of both, but here is an extra simple table
|
||||
|
||||
| Average type | A reason to use it |
|
||||
| ------------ | ------------------ |
|
||||
| Arithmetic | The arithmetic mean is the average of a sum of numbers, which reflects the central tendency of the position of the numbers |
|
||||
| Geometric | The geometric mean differs from the arithmetic average, or arithmetic mean, in how it is calculated because it takes into account the compounding that occurs from period to period. Because of this, investors usually consider the geometric mean a more accurate measure of returns than the arithmetic mean |
|
||||
|
||||
## USD total tracking
|
||||
If the strategy config setting `DisableUSDTracking` is `false`, then the GoCryptoTrader Backtester will automatically retrieve USD data that matches your backtesting currencies, eg pair BTC/LTC will track BTC/USD and LTC/USD as well. This allows for tracking overall strategic performance against one currency. This can allow for much easier performance calculations and comparisons
|
||||
|
||||
|
||||
### Please click GoDocs chevron above to view current GoDoc information for this package
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{{define "backtester funding trackingcurrencies" -}}
|
||||
{{template "backtester-header" .}}
|
||||
## {{.CapitalName}} package overview
|
||||
|
||||
### What does the tracking currencies package do?
|
||||
The tracking currencies package is responsible breaking up a user's strategy currencies into pairs with a USD equivalent pair in order to track strategy performance against a singular currency. For example, you are wanting to backtest on Binance using XRP/DOGE, the tracking currencies will also retrieve XRP/BUSD and DOGE/BUSD pair data for use in calculating how much a currency is worth at every candle point.
|
||||
|
||||
### What if the exchange does not support USD?
|
||||
The tracking currencies package will check supported currencies against a list of USD equivalent USD backed stablecoins. So if your select exchange only supports BUSD or USDT based pairs, then the GoCryptoTrader Backtester will break up config pairs into the equivalent. See below list for currently supported stablecoin equivalency
|
||||
|
||||
| Currency |
|
||||
|----------|
|
||||
|USD |
|
||||
|USDT |
|
||||
|BUSD |
|
||||
|USDC |
|
||||
|DAI |
|
||||
|TUSD |
|
||||
|ZUSD |
|
||||
|PAX |
|
||||
|
||||
### How do I disable this?
|
||||
If you need to disable this functionality, for example, you are using Live, Database or CSV based trade data, then under `strategy-settings` in your config, set `disable-usd-tracking` to `true`
|
||||
|
||||
### Can I supply my own list of equivalent currencies instead of USD?
|
||||
This is currently not supported. If this is a feature you would like to have, please raise an issue on GitHub or in our Slack channel
|
||||
|
||||
### Please click GoDocs chevron above to view current GoDoc information for this package
|
||||
{{template "contributions"}}
|
||||
{{template "donations" .}}
|
||||
{{end}}
|
||||
@@ -4,7 +4,10 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// FloatFromString format
|
||||
@@ -81,3 +84,83 @@ func BoolPtr(condition bool) *bool {
|
||||
b := condition
|
||||
return &b
|
||||
}
|
||||
|
||||
// IntToHumanFriendlyString converts an int to a comma separated string at the thousand point
|
||||
// eg 1000 becomes 1,000
|
||||
func IntToHumanFriendlyString(number int64, thousandsSep string) string {
|
||||
neg := false
|
||||
if number < 0 {
|
||||
number = -number
|
||||
neg = true
|
||||
}
|
||||
str := fmt.Sprintf("%v", number)
|
||||
return numberToHumanFriendlyString(str, 0, "", thousandsSep, neg)
|
||||
}
|
||||
|
||||
// FloatToHumanFriendlyString converts a float to a comma separated string at the thousand point
|
||||
// eg 1000 becomes 1,000
|
||||
func FloatToHumanFriendlyString(number float64, decimals uint, decPoint, thousandsSep string) string {
|
||||
neg := false
|
||||
if number < 0 {
|
||||
number = -number
|
||||
neg = true
|
||||
}
|
||||
dec := int(decimals)
|
||||
str := fmt.Sprintf("%."+strconv.Itoa(dec)+"F", number)
|
||||
return numberToHumanFriendlyString(str, dec, decPoint, thousandsSep, neg)
|
||||
}
|
||||
|
||||
// DecimalToHumanFriendlyString converts a decimal number to a comma separated string at the thousand point
|
||||
// eg 1000 becomes 1,000
|
||||
func DecimalToHumanFriendlyString(number decimal.Decimal, rounding int, decPoint, thousandsSep string) string {
|
||||
neg := false
|
||||
if number.LessThan(decimal.Zero) {
|
||||
number = number.Abs()
|
||||
neg = true
|
||||
}
|
||||
str := number.String()
|
||||
rnd := strings.Split(str, ".")
|
||||
if len(rnd) == 1 {
|
||||
rounding = 0
|
||||
} else if len(rnd[1]) < rounding {
|
||||
rounding = len(rnd[1])
|
||||
}
|
||||
return numberToHumanFriendlyString(number.StringFixed(int32(rounding)), rounding, decPoint, thousandsSep, neg)
|
||||
}
|
||||
|
||||
func numberToHumanFriendlyString(str string, dec int, decPoint, thousandsSep string, neg bool) string {
|
||||
var prefix, suffix string
|
||||
if len(str)-(dec+1) < 0 {
|
||||
dec = 0
|
||||
}
|
||||
if dec > 0 {
|
||||
prefix = str[:len(str)-(dec+1)]
|
||||
suffix = str[len(str)-dec:]
|
||||
} else {
|
||||
prefix = str
|
||||
}
|
||||
sep := []byte(thousandsSep)
|
||||
n, l1, l2 := 0, len(prefix), len(sep)
|
||||
// thousands sep num
|
||||
c := (l1 - 1) / 3
|
||||
tmp := make([]byte, l2*c+l1)
|
||||
pos := len(tmp) - 1
|
||||
for i := l1 - 1; i >= 0; i, n, pos = i-1, n+1, pos-1 {
|
||||
if l2 > 0 && n > 0 && n%3 == 0 {
|
||||
for j := range sep {
|
||||
tmp[pos] = sep[l2-j-1]
|
||||
pos--
|
||||
}
|
||||
}
|
||||
tmp[pos] = prefix[i]
|
||||
}
|
||||
s := string(tmp)
|
||||
if dec > 0 {
|
||||
s += decPoint + suffix
|
||||
}
|
||||
if neg {
|
||||
s = "-" + s
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package convert
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestFloatFromString(t *testing.T) {
|
||||
@@ -150,3 +153,132 @@ func TestBoolPtr(t *testing.T) {
|
||||
t.Fatal("false expected received true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloatToHumanFriendlyString(t *testing.T) {
|
||||
t.Parallel()
|
||||
test := FloatToHumanFriendlyString(0, 3, ".", ",")
|
||||
if strings.Contains(test, ",") {
|
||||
t.Error("unexpected ','")
|
||||
}
|
||||
test = FloatToHumanFriendlyString(100, 3, ".", ",")
|
||||
if strings.Contains(test, ",") {
|
||||
t.Error("unexpected ','")
|
||||
}
|
||||
test = FloatToHumanFriendlyString(1000, 3, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = FloatToHumanFriendlyString(-1000, 3, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = FloatToHumanFriendlyString(-1000, 10, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = FloatToHumanFriendlyString(1000.1337, 1, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
dec := strings.Split(test, ".")
|
||||
if len(dec) == 1 {
|
||||
t.Error("expected decimal place")
|
||||
}
|
||||
if dec[1] != "1" {
|
||||
t.Error("expected decimal place")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecimalToHumanFriendlyString(t *testing.T) {
|
||||
t.Parallel()
|
||||
test := DecimalToHumanFriendlyString(decimal.Zero, 0, ".", ",")
|
||||
if strings.Contains(test, ",") {
|
||||
t.Log(test)
|
||||
t.Error("unexpected ','")
|
||||
}
|
||||
test = DecimalToHumanFriendlyString(decimal.NewFromInt(100), 0, ".", ",")
|
||||
if strings.Contains(test, ",") {
|
||||
t.Log(test)
|
||||
t.Error("unexpected ','")
|
||||
}
|
||||
test = DecimalToHumanFriendlyString(decimal.NewFromInt(1000), 0, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = DecimalToHumanFriendlyString(decimal.NewFromFloat(1000.1337), 1, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
dec := strings.Split(test, ".")
|
||||
if len(dec) == 1 {
|
||||
t.Error("expected decimal place")
|
||||
}
|
||||
if dec[1] != "1" {
|
||||
t.Error("expected decimal place")
|
||||
}
|
||||
|
||||
test = DecimalToHumanFriendlyString(decimal.NewFromFloat(-1000.1337), 1, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = DecimalToHumanFriendlyString(decimal.NewFromFloat(-1000.1337), 100000, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = DecimalToHumanFriendlyString(decimal.NewFromFloat(1000.1), 10, ".", ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
dec = strings.Split(test, ".")
|
||||
if len(dec) == 1 {
|
||||
t.Error("expected decimal place")
|
||||
}
|
||||
if dec[1] != "1" {
|
||||
t.Error("expected decimal place")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntToHumanFriendlyString(t *testing.T) {
|
||||
t.Parallel()
|
||||
test := IntToHumanFriendlyString(0, ",")
|
||||
if strings.Contains(test, ",") {
|
||||
t.Log(test)
|
||||
t.Error("unexpected ','")
|
||||
}
|
||||
test = IntToHumanFriendlyString(100, ",")
|
||||
if strings.Contains(test, ",") {
|
||||
t.Log(test)
|
||||
t.Error("unexpected ','")
|
||||
}
|
||||
test = IntToHumanFriendlyString(1000, ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = IntToHumanFriendlyString(-1000, ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
|
||||
test = IntToHumanFriendlyString(1000000, ",")
|
||||
if !strings.Contains(test, ",") {
|
||||
t.Error("expected ','")
|
||||
}
|
||||
dec := strings.Split(test, ",")
|
||||
if len(dec) <= 2 {
|
||||
t.Error("expected two commas place")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumberToHumanFriendlyString(t *testing.T) {
|
||||
resp := numberToHumanFriendlyString("1", 1337, ".", ",", false)
|
||||
if strings.Contains(resp, ".") {
|
||||
t.Error("expected no comma")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,13 +450,12 @@ func (m *OrderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder or
|
||||
|
||||
// GetOrdersSnapshot returns a snapshot of all orders in the orderstore. It optionally filters any orders that do not match the status
|
||||
// but a status of "" or ANY will include all
|
||||
// the time adds contexts for the when the snapshot is relevant for
|
||||
func (m *OrderManager) GetOrdersSnapshot(s order.Status) ([]order.Detail, time.Time) {
|
||||
// the time adds contexts for when the snapshot is relevant for
|
||||
func (m *OrderManager) GetOrdersSnapshot(s order.Status) []order.Detail {
|
||||
if m == nil || atomic.LoadInt32(&m.started) == 0 {
|
||||
return nil, time.Time{}
|
||||
return nil
|
||||
}
|
||||
var os []order.Detail
|
||||
var latestUpdate time.Time
|
||||
for _, v := range m.orderStore.Orders {
|
||||
for i := range v {
|
||||
if s != v[i].Status &&
|
||||
@@ -464,14 +463,11 @@ func (m *OrderManager) GetOrdersSnapshot(s order.Status) ([]order.Detail, time.T
|
||||
s != "" {
|
||||
continue
|
||||
}
|
||||
if v[i].LastUpdated.After(latestUpdate) {
|
||||
latestUpdate = v[i].LastUpdated
|
||||
}
|
||||
os = append(os, *v[i])
|
||||
}
|
||||
}
|
||||
|
||||
return os, latestUpdate
|
||||
return os
|
||||
}
|
||||
|
||||
// GetOrdersFiltered returns a snapshot of all orders in the order store.
|
||||
|
||||
@@ -327,6 +327,9 @@ func TotalCandlesPerInterval(start, end time.Time, interval Interval) (out float
|
||||
// IntervalsPerYear helps determine the number of intervals in a year
|
||||
// used in CAGR calculation to know the amount of time of an interval in a year
|
||||
func (i *Interval) IntervalsPerYear() float64 {
|
||||
if i.Duration() == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(OneYear.Duration().Nanoseconds()) / float64(i.Duration().Nanoseconds())
|
||||
}
|
||||
|
||||
@@ -471,6 +474,17 @@ func (h *IntervalRangeHolder) HasDataAtDate(t time.Time) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetClosePriceAtTime returns the close price of a candle
|
||||
// at a given time
|
||||
func (k *Item) GetClosePriceAtTime(t time.Time) (float64, error) {
|
||||
for i := range k.Candles {
|
||||
if k.Candles[i].Time.Equal(t) {
|
||||
return k.Candles[i].Close, nil
|
||||
}
|
||||
}
|
||||
return -1, fmt.Errorf("%w at %v", ErrNotFoundAtTime, t)
|
||||
}
|
||||
|
||||
// SetHasDataFromCandles will calculate whether there is data in each candle
|
||||
// allowing any missing data from an API request to be highlighted
|
||||
func (h *IntervalRangeHolder) SetHasDataFromCandles(c []Candle) {
|
||||
|
||||
@@ -844,7 +844,11 @@ func TestHasDataAtDate(t *testing.T) {
|
||||
|
||||
func TestIntervalsPerYear(t *testing.T) {
|
||||
t.Parallel()
|
||||
i := OneYear
|
||||
var i Interval
|
||||
if i.IntervalsPerYear() != 0 {
|
||||
t.Error("expected 0")
|
||||
}
|
||||
i = OneYear
|
||||
if i.IntervalsPerYear() != 1.0 {
|
||||
t.Error("expected 1")
|
||||
}
|
||||
@@ -898,7 +902,7 @@ func BenchmarkJustifyIntervalTimeStoringUnixValues2(b *testing.B) {
|
||||
func TestConvertToNewInterval(t *testing.T) {
|
||||
_, err := ConvertToNewInterval(nil, OneMin)
|
||||
if !errors.Is(err, errNilKline) {
|
||||
t.Errorf("received '%v' expectec '%v'", err, errNilKline)
|
||||
t.Errorf("received '%v' expected '%v'", err, errNilKline)
|
||||
}
|
||||
|
||||
old := &Item{
|
||||
@@ -936,23 +940,23 @@ func TestConvertToNewInterval(t *testing.T) {
|
||||
|
||||
_, err = ConvertToNewInterval(old, 0)
|
||||
if !errors.Is(err, ErrUnsetInterval) {
|
||||
t.Errorf("received '%v' expectec '%v'", err, ErrUnsetInterval)
|
||||
t.Errorf("received '%v' expected '%v'", err, ErrUnsetInterval)
|
||||
}
|
||||
_, err = ConvertToNewInterval(old, OneMin)
|
||||
if !errors.Is(err, ErrCanOnlyDownscaleCandles) {
|
||||
t.Errorf("received '%v' expectec '%v'", err, ErrCanOnlyDownscaleCandles)
|
||||
t.Errorf("received '%v' expected '%v'", err, ErrCanOnlyDownscaleCandles)
|
||||
}
|
||||
old.Interval = ThreeDay
|
||||
_, err = ConvertToNewInterval(old, OneWeek)
|
||||
if !errors.Is(err, ErrWholeNumberScaling) {
|
||||
t.Errorf("received '%v' expectec '%v'", err, ErrWholeNumberScaling)
|
||||
t.Errorf("received '%v' expected '%v'", err, ErrWholeNumberScaling)
|
||||
}
|
||||
|
||||
old.Interval = OneDay
|
||||
newInterval := ThreeDay
|
||||
newCandle, err := ConvertToNewInterval(old, newInterval)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expectec '%v'", err, nil)
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if len(newCandle.Candles) != 1 {
|
||||
t.Error("expected one candle")
|
||||
@@ -975,9 +979,36 @@ func TestConvertToNewInterval(t *testing.T) {
|
||||
})
|
||||
newCandle, err = ConvertToNewInterval(old, newInterval)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expectec '%v'", err, nil)
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if len(newCandle.Candles) != 1 {
|
||||
t.Error("expected one candle")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClosePriceAtTime(t *testing.T) {
|
||||
tt := time.Now()
|
||||
k := Item{
|
||||
Candles: []Candle{
|
||||
{
|
||||
Time: tt,
|
||||
Close: 1337,
|
||||
},
|
||||
{
|
||||
Time: tt.Add(time.Hour),
|
||||
Close: 1338,
|
||||
},
|
||||
},
|
||||
}
|
||||
price, err := k.GetClosePriceAtTime(tt)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if price != 1337 {
|
||||
t.Errorf("received '%v' expected '%v'", price, 1337)
|
||||
}
|
||||
_, err = k.GetClosePriceAtTime(tt.Add(time.Minute))
|
||||
if !errors.Is(err, ErrNotFoundAtTime) {
|
||||
t.Errorf("received '%v' expected '%v'", err, ErrNotFoundAtTime)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ var (
|
||||
// ErrWholeNumberScaling returns when old interval data cannot neatly fit into new interval size
|
||||
ErrWholeNumberScaling = errors.New("new interval must scale properly into new candle")
|
||||
errNilKline = errors.New("kline item is nil")
|
||||
// ErrNotFoundAtTime returned when looking up a candle at a specific time
|
||||
ErrNotFoundAtTime = errors.New("candle not found at time")
|
||||
|
||||
// SupportedIntervals is a list of all supported intervals
|
||||
SupportedIntervals = []Interval{
|
||||
|
||||
@@ -33,8 +33,8 @@ func getWriters(s *SubLoggerConfig) io.Writer {
|
||||
}
|
||||
|
||||
// GenDefaultSettings return struct with known sane/working logger settings
|
||||
func GenDefaultSettings() (log Config) {
|
||||
log = Config{
|
||||
func GenDefaultSettings() Config {
|
||||
return Config{
|
||||
Enabled: convert.BoolPtr(true),
|
||||
SubLoggerConfig: SubLoggerConfig{
|
||||
Level: "INFO|DEBUG|WARN|ERROR",
|
||||
@@ -57,7 +57,6 @@ func GenDefaultSettings() (log Config) {
|
||||
},
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func configureSubLogger(logger, levels string, output io.Writer) error {
|
||||
|
||||
Reference in New Issue
Block a user