Backtester: Live trading upgrades (#1023)

* Modifications for a smoother live run

* Fixes data appending

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

* Attempting to get cash and carry working

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

* =designs new live data handler

* Updates cash and carry strat to work

* adds test coverage. begins closeallpositions function

* Updates cash and carry to work live

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

* =Fixes USD funding issue

* =fixes tests

* fixes tests AGAIN

* adds coverage to close all orders

* crummy tests, should override

* more tests

* more tests

* more coverage

* removes scourge of currency.Pair maps. More tests

* missed currency stuff

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

* Now triggers updates on the very first data entry

* All my problems are solved now????

* fixes tests, extends coverage

* there is some really funky candle stuff going on

* my brain is melting

* better shutdown management, fixes freezing bug

* fixes data duplication issues, adds retries to requests

* reduces logging, adds verbose options

* expands coverage over all new functionality

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

* fixes setup issues and tests

* starts adding external wallet amounts for funding

* more setup for assets

* setup live fund calcs and placing orders

* successfully performs automated cash and carry

* merge fixes

* funding properly set at all times

* fixes some bugs, need to address currencystatistics still

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

* fixes stat bugs, adds cool new fetchfees feature

* fixes terrible processing bugs

* tightens realorder stats, sadly loses some live stats

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

* fix tests

* coverage

* beautiful new test coverage

* docs

* adds new fee getter delayer

* commits from the correct directory

* Lint

* adds verbose to fund manager

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

* go mod tidy

* update buf

* buf + test improvement

* Post merge fixes

* fixes surprise offset bug

* fix sizing restrictions for cash and carry

* fix server lints

* merge fixes

* test fixesss

* lintle fixles

* slowloris

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

* rpc lint and fixes

* bugfix: order manager not processing orders properly

* somewhat addresses nits

* absolutely broken end of day commit

* absolutely massive knockon effects from nits

* massive knockon effects continue

* fixes things

* address remaining nits

* jk now fixes things

* addresses the easier nits

* more nit fixers

* more niterinos addressederinos

* refactors holdings and does some nits

* so buf

* addresses some nits, fixes holdings bugs

* cleanup

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

* terrible code, will revert

* to be reviewed in detail tomorrow

* Fixes up channel system

* smashes those nits

* fixes extra candles, fixes collateral bug, tests

* fixes data races, introduces reflection

* more checks n tests

* Fixes cash and carry issues. Fixes more cool bugs

* fixes ~typer~ typo

* replace spot strats from ftx to binance

* fixes all the tests I just destroyed

* removes example path, rm verbose

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

* renamed, non-working strategies

* Removes FTX references almost as fast as sbf removes funds

* regen docs, add contrib names,sort contrib names

* fixes merge renamings

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

* Fixes live order bugs with real orders and without

* Apply suggestions from code review

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

* Update backtester/engine/live.go

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

* Update backtester/engine/live.go

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

* Update backtester/config/strategyconfigbuilder/main.go

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

* updates docs

* even better docs

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
Scott
2023-01-05 13:03:17 +11:00
committed by GitHub
parent d92ffe6e9e
commit 017cdf1384
195 changed files with 13783 additions and 8048 deletions

1
.gitignore vendored
View File

@@ -15,6 +15,7 @@ vendor/
# Binaries for programs and plugins
gocryptotrader
/backtester/backtester
cmd/gctcli/gctcli
backtester/backtester
backtester/btcli/btcli

View File

@@ -22,6 +22,7 @@ khcchiu | https://github.com/khcchiu
woshidama323 | https://github.com/woshidama323
yangrq1018 | https://github.com/yangrq1018
TaltaM | https://github.com/TaltaM
samuael | https://github.com/samuael
crackcomm | https://github.com/crackcomm
azhang | https://github.com/azhang
andreygrehov | https://github.com/andreygrehov
@@ -30,27 +31,26 @@ Christian-Achilli | https://github.com/Christian-Achilli
MarkDzulko | https://github.com/MarkDzulko
gam-phon | https://github.com/gam-phon
cornelk | https://github.com/cornelk
if1live | https://github.com/if1live
herenow | https://github.com/herenow
if1live | https://github.com/if1live
lozdog245 | https://github.com/lozdog245
mshogin | https://github.com/mshogin
soxipy | https://github.com/soxipy
tk42 | https://github.com/tk42
blombard | https://github.com/blombard
cavapoo2 | https://github.com/cavapoo2
CodeLingoTeam | https://github.com/CodeLingoTeam
CodeLingoBot | https://github.com/CodeLingoBot
Daanikus | https://github.com/Daanikus
daniel-cohen | https://github.com/daniel-cohen
DirectX | https://github.com/DirectX
frankzougc | https://github.com/frankzougc
idoall | https://github.com/idoall
mattkanwisher | https://github.com/mattkanwisher
mKurrels | https://github.com/mKurrels
m1kola | https://github.com/m1kola
cavapoo2 | https://github.com/cavapoo2
zeldrinn | https://github.com/zeldrinn
starit | https://github.com/starit
Jimexist | https://github.com/Jimexist
lookfirst | https://github.com/lookfirst
m1kola | https://github.com/m1kola
mattkanwisher | https://github.com/mattkanwisher
merkeld | https://github.com/merkeld
CodeLingoTeam | https://github.com/CodeLingoTeam
Daanikus | https://github.com/Daanikus
CodeLingoBot | https://github.com/CodeLingoBot
blombard | https://github.com/blombard
soxipy | https://github.com/soxipy
lozdog245 | https://github.com/lozdog245
mKurrels | https://github.com/mKurrels
starit | https://github.com/starit
zeldrinn | https://github.com/zeldrinn

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014-2022 The GoCryptoTrader Developers
Copyright (c) 2014-2023 The GoCryptoTrader Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -146,9 +146,9 @@ Binaries will be published once the codebase reaches a stable condition.
|User|Contribution Amount|
|--|--|
| [thrasher-](https://github.com/thrasher-) | 670 |
| [shazbert](https://github.com/shazbert) | 268 |
| [gloriousCode](https://github.com/gloriousCode) | 202 |
| [dependabot[bot]](https://github.com/apps/dependabot) | 130 |
| [shazbert](https://github.com/shazbert) | 269 |
| [gloriousCode](https://github.com/gloriousCode) | 205 |
| [dependabot[bot]](https://github.com/apps/dependabot) | 139 |
| [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 |
| [xtda](https://github.com/xtda) | 47 |
| [lrascao](https://github.com/lrascao) | 27 |
@@ -160,13 +160,14 @@ Binaries will be published once the codebase reaches a stable condition.
| [vadimzhukck](https://github.com/vadimzhukck) | 10 |
| [140am](https://github.com/140am) | 8 |
| [marcofranssen](https://github.com/marcofranssen) | 8 |
| [geseq](https://github.com/geseq) | 7 |
| [geseq](https://github.com/geseq) | 8 |
| [dackroyd](https://github.com/dackroyd) | 5 |
| [cranktakular](https://github.com/cranktakular) | 5 |
| [khcchiu](https://github.com/khcchiu) | 5 |
| [woshidama323](https://github.com/woshidama323) | 3 |
| [yangrq1018](https://github.com/yangrq1018) | 3 |
| [TaltaM](https://github.com/TaltaM) | 3 |
| [samuael](https://github.com/samuael) | 3 |
| [crackcomm](https://github.com/crackcomm) | 3 |
| [azhang](https://github.com/azhang) | 2 |
| [andreygrehov](https://github.com/andreygrehov) | 2 |
@@ -175,27 +176,26 @@ Binaries will be published once the codebase reaches a stable condition.
| [MarkDzulko](https://github.com/MarkDzulko) | 2 |
| [gam-phon](https://github.com/gam-phon) | 2 |
| [cornelk](https://github.com/cornelk) | 2 |
| [if1live](https://github.com/if1live) | 2 |
| [herenow](https://github.com/herenow) | 2 |
| [if1live](https://github.com/if1live) | 2 |
| [lozdog245](https://github.com/lozdog245) | 2 |
| [mshogin](https://github.com/mshogin) | 2 |
| [soxipy](https://github.com/soxipy) | 2 |
| [tk42](https://github.com/tk42) | 2 |
| [blombard](https://github.com/blombard) | 1 |
| [cavapoo2](https://github.com/cavapoo2) | 1 |
| [CodeLingoTeam](https://github.com/CodeLingoTeam) | 1 |
| [CodeLingoBot](https://github.com/CodeLingoBot) | 1 |
| [Daanikus](https://github.com/Daanikus) | 1 |
| [daniel-cohen](https://github.com/daniel-cohen) | 1 |
| [DirectX](https://github.com/DirectX) | 1 |
| [frankzougc](https://github.com/frankzougc) | 1 |
| [idoall](https://github.com/idoall) | 1 |
| [mattkanwisher](https://github.com/mattkanwisher) | 1 |
| [mKurrels](https://github.com/mKurrels) | 1 |
| [m1kola](https://github.com/m1kola) | 1 |
| [cavapoo2](https://github.com/cavapoo2) | 1 |
| [zeldrinn](https://github.com/zeldrinn) | 1 |
| [starit](https://github.com/starit) | 1 |
| [Jimexist](https://github.com/Jimexist) | 1 |
| [lookfirst](https://github.com/lookfirst) | 1 |
| [m1kola](https://github.com/m1kola) | 1 |
| [mattkanwisher](https://github.com/mattkanwisher) | 1 |
| [merkeld](https://github.com/merkeld) | 1 |
| [CodeLingoTeam](https://github.com/CodeLingoTeam) | 1 |
| [Daanikus](https://github.com/Daanikus) | 1 |
| [CodeLingoBot](https://github.com/CodeLingoBot) | 1 |
| [blombard](https://github.com/blombard) | 1 |
| [soxipy](https://github.com/soxipy) | 2 |
| [lozdog245](https://github.com/lozdog245) | 2 |
| [mKurrels](https://github.com/mKurrels) | 1 |
| [starit](https://github.com/starit) | 1 |
| [zeldrinn](https://github.com/zeldrinn) | 1 |

View File

@@ -45,20 +45,23 @@ An event-driven backtesting tool to test and iterate trading strategies using hi
- Fund transfer. At a strategy level, transfer funds between exchanges to allow for complex strategy design
- Backtesting support for futures asset types
- Example cash and carry spot futures strategy
- Long-running application
- GRPC server implementation
- Long-running application as a GRPC server
- Custom strategy plugins
- Live data source trading. Traders can move their back tested strategies and use them against current live data
## Planned Features
We welcome pull requests on any feature for the Backtester! We will be especially appreciative of any contribution towards the following planned features:
| Feature | Description |
|---------|-------------|
| Perpetual futures support | Accounting for hourly funding rates in user's overall positions allows for much greater strategic depth |
| Margin borrowing support | Allowing strategies to utilise margin borrowing to have larger positions and handling borrow rate payments |
| Leverage support | Leverage is a good way to enhance profit and loss and is important to include in strategies |
| Live ticker data | A potential feature as live trading works off candle data which is only processed at intervals. Adding ticker data as a strategic source allows for faster decision making |
| Live orderbook data | Processing orders based off the latest orderbook data allows for much more accurate order placement and reduces surprise slippage |
| Enhance config-builder | Create an application that can create strategy configs in a more visual manner and execute them via GRPC to allow for faster customisation of strategies |
| Save Backtester results to database | This will allow for easier comparison of results over time |
| Backtester result comparison report | Providing an executive summary of Backtester database results |
| Currency correlation | Compare multiple exchange, asset, currencies for a candle interval against indicators to highlight correlated pairs for use in pairs trading |
| Improve live trading functionality | Live trading is currently only a proof Of concept. Adding live support for running multiple currencies and running off orderbook data will allow for esteemed traders to use their backtested strategies |
## How does it work?

View File

@@ -40,16 +40,16 @@ var executeStrategyFromFileCommand = &cli.Command{
}
func executeStrategyFromFile(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var path string
if c.IsSet("path") {
path = c.String("path")
@@ -84,13 +84,13 @@ func executeStrategyFromFile(c *cli.Context) error {
return nil
}
var listAllRunsCommand = &cli.Command{
Name: "listallruns",
Usage: "returns a list of all loaded backtest/livestrategy runs",
Action: listAllRuns,
var listAllTasksCommand = &cli.Command{
Name: "listalltasks",
Usage: "returns a list of all loaded strategy tasks",
Action: listAllTasks,
}
func listAllRuns(c *cli.Context) error {
func listAllTasks(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
@@ -98,9 +98,9 @@ func listAllRuns(c *cli.Context) error {
defer closeConn(conn, cancel)
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.ListAllRuns(
result, err := client.ListAllTasks(
c.Context,
&btrpc.ListAllRunsRequest{},
&btrpc.ListAllTasksRequest{},
)
if err != nil {
@@ -111,26 +111,20 @@ func listAllRuns(c *cli.Context) error {
return nil
}
var startRunCommand = &cli.Command{
Name: "startrun",
Usage: "executes a strategy loaded into the server",
var startTaskCommand = &cli.Command{
Name: "starttask",
Usage: "executes a strategy task loaded into the server",
ArgsUsage: "<id>",
Action: startRun,
Action: startTask,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "id",
Usage: "the id of the backtest/livestrategy run",
Usage: "the id of the strategy task",
},
},
}
func startRun(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
func startTask(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
@@ -142,10 +136,15 @@ func startRun(c *cli.Context) error {
id = c.Args().First()
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.StartRun(
result, err := client.StartTask(
c.Context,
&btrpc.StartRunRequest{
&btrpc.StartTaskRequest{
Id: id,
},
)
@@ -158,13 +157,13 @@ func startRun(c *cli.Context) error {
return nil
}
var startAllRunsCommand = &cli.Command{
Name: "startallruns",
var startAllTasksCommand = &cli.Command{
Name: "startalltasks",
Usage: "executes all strategies loaded into the server that have not been run",
Action: startAllRuns,
Action: startAllTasks,
}
func startAllRuns(c *cli.Context) error {
func startAllTasks(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
@@ -172,9 +171,9 @@ func startAllRuns(c *cli.Context) error {
defer closeConn(conn, cancel)
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.StartAllRuns(
result, err := client.StartAllTasks(
c.Context,
&btrpc.StartAllRunsRequest{},
&btrpc.StartAllTasksRequest{},
)
if err != nil {
@@ -185,30 +184,30 @@ func startAllRuns(c *cli.Context) error {
return nil
}
var stopRunCommand = &cli.Command{
Name: "stoprun",
var stopTaskCommand = &cli.Command{
Name: "stoptask",
Usage: "stops a strategy loaded into the server",
ArgsUsage: "<id>",
Action: stopRun,
Action: stopTask,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "id",
Usage: "the id of the backtest/livestrategy run",
Usage: "the id of the strategy task",
},
},
}
func stopRun(c *cli.Context) error {
func stopTask(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var id string
if c.IsSet("id") {
id = c.String("id")
@@ -217,9 +216,9 @@ func stopRun(c *cli.Context) error {
}
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.StopRun(
result, err := client.StopTask(
c.Context,
&btrpc.StopRunRequest{
&btrpc.StopTaskRequest{
Id: id,
},
)
@@ -232,13 +231,13 @@ func stopRun(c *cli.Context) error {
return nil
}
var stopAllRunsCommand = &cli.Command{
Name: "stopallruns",
var stopAllTasksCommand = &cli.Command{
Name: "stopalltasks",
Usage: "stops all strategies loaded into the server",
Action: stopAllRuns,
Action: stopAllTasks,
}
func stopAllRuns(c *cli.Context) error {
func stopAllTasks(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
@@ -246,9 +245,9 @@ func stopAllRuns(c *cli.Context) error {
defer closeConn(conn, cancel)
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.StopAllRuns(
result, err := client.StopAllTasks(
c.Context,
&btrpc.StopAllRunsRequest{},
&btrpc.StopAllTasksRequest{},
)
if err != nil {
@@ -259,30 +258,30 @@ func stopAllRuns(c *cli.Context) error {
return nil
}
var clearRunCommand = &cli.Command{
Name: "clearrun",
var clearTaskCommand = &cli.Command{
Name: "cleartask",
Usage: "clears/deletes a strategy loaded into the server - if it is not running",
ArgsUsage: "<id>",
Action: clearRun,
Action: clearTask,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "id",
Usage: "the id of the backtest/livestrategy run",
Usage: "the id of the strategy task",
},
},
}
func clearRun(c *cli.Context) error {
func clearTask(c *cli.Context) error {
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowSubcommandHelp(c)
}
var id string
if c.IsSet("id") {
id = c.String("id")
@@ -291,9 +290,9 @@ func clearRun(c *cli.Context) error {
}
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.ClearRun(
result, err := client.ClearTask(
c.Context,
&btrpc.ClearRunRequest{
&btrpc.ClearTaskRequest{
Id: id,
},
)
@@ -306,13 +305,13 @@ func clearRun(c *cli.Context) error {
return nil
}
var clearAllRunsCommand = &cli.Command{
Name: "clearallruns",
Usage: "clears all strategies loaded into the server. Only runs not actively running will be cleared",
Action: clearAllRuns,
var clearAllTasksCommand = &cli.Command{
Name: "clearalltasks",
Usage: "clears all strategies loaded into the server. Only tasks not actively running will be cleared",
Action: clearAllTasks,
}
func clearAllRuns(c *cli.Context) error {
func clearAllTasks(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
@@ -320,9 +319,9 @@ func clearAllRuns(c *cli.Context) error {
defer closeConn(conn, cancel)
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.ClearAllRuns(
result, err := client.ClearAllTasks(
c.Context,
&btrpc.ClearAllRunsRequest{},
&btrpc.ClearAllTasksRequest{},
)
if err != nil {
@@ -335,7 +334,7 @@ func clearAllRuns(c *cli.Context) error {
var executeStrategyFromConfigCommand = &cli.Command{
Name: "executestrategyfromconfig",
Usage: "runs the default strategy config but via passing in as a struct instead of a filepath - this is a proof-of-concept implementation",
Usage: fmt.Sprintf("runs the default strategy config but via passing in as a struct instead of a filepath - this is a proof-of-concept implementation using %v", filepath.Join("..", "config", "strategyexamples", "dca-api-candles.strat")),
Description: "the cli is not a good place to manage this type of command with n variables to pass in from a command line",
Action: executeStrategyFromConfig,
Flags: []cli.Flag{
@@ -359,7 +358,7 @@ func executeStrategyFromConfig(c *cli.Context) error {
"..",
"config",
"strategyexamples",
"ftx-cash-carry.strat")
"dca-api-candles.strat")
defaultConfig, err := config.ReadStrategyConfigFromFile(defaultPath)
if err != nil {
return err
@@ -376,12 +375,16 @@ func executeStrategyFromConfig(c *cli.Context) error {
currencySettings := make([]*btrpc.CurrencySettings, len(defaultConfig.CurrencySettings))
for i := range defaultConfig.CurrencySettings {
var sd *btrpc.SpotDetails
var sd btrpc.SpotDetails
if defaultConfig.CurrencySettings[i].SpotDetails != nil {
sd.InitialBaseFunds = defaultConfig.CurrencySettings[i].SpotDetails.InitialBaseFunds.String()
sd.InitialQuoteFunds = defaultConfig.CurrencySettings[i].SpotDetails.InitialQuoteFunds.String()
if defaultConfig.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
sd.InitialBaseFunds = defaultConfig.CurrencySettings[i].SpotDetails.InitialBaseFunds.String()
}
if defaultConfig.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil {
sd.InitialQuoteFunds = defaultConfig.CurrencySettings[i].SpotDetails.InitialQuoteFunds.String()
}
}
var fd *btrpc.FuturesDetails
var fd btrpc.FuturesDetails
if defaultConfig.CurrencySettings[i].FuturesDetails != nil {
fd.Leverage = &btrpc.Leverage{
CanUseLeverage: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.CanUseLeverage,
@@ -413,8 +416,12 @@ func executeStrategyFromConfig(c *cli.Context) error {
SkipCandleVolumeFitting: defaultConfig.CurrencySettings[i].SkipCandleVolumeFitting,
UseExchangeOrderLimits: defaultConfig.CurrencySettings[i].CanUseExchangeLimits,
UseExchangePnlCalculation: defaultConfig.CurrencySettings[i].UseExchangePNLCalculation,
SpotDetails: sd,
FuturesDetails: fd,
}
if sd.InitialQuoteFunds != "" || sd.InitialBaseFunds != "" {
currencySettings[i].SpotDetails = &sd
}
if fd.Leverage != nil {
currencySettings[i].FuturesDetails = &fd
}
}
@@ -441,13 +448,28 @@ func executeStrategyFromConfig(c *cli.Context) error {
}
}
if defaultConfig.DataSettings.LiveData != nil {
creds := make([]*btrpc.ExchangeCredentials, len(defaultConfig.DataSettings.LiveData.ExchangeCredentials))
for i := range defaultConfig.DataSettings.LiveData.ExchangeCredentials {
creds[i] = &btrpc.ExchangeCredentials{
Exchange: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Exchange,
Keys: &btrpc.ExchangeKeys{
Key: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.Key,
Secret: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.Secret,
ClientId: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.ClientID,
PemKey: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.PEMKey,
SubAccount: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.SubAccount,
OneTimePassword: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.OneTimePassword,
},
}
}
dataSettings.LiveData = &btrpc.LiveData{
ApiKeyOverride: defaultConfig.DataSettings.LiveData.APIKeyOverride,
ApiSecretOverride: defaultConfig.DataSettings.LiveData.APISecretOverride,
ApiClientIdOverride: defaultConfig.DataSettings.LiveData.APIClientIDOverride,
Api_2FaOverride: defaultConfig.DataSettings.LiveData.API2FAOverride,
ApiSubAccountOverride: defaultConfig.DataSettings.LiveData.APISubAccountOverride,
UseRealOrders: defaultConfig.DataSettings.LiveData.RealOrders,
NewEventTimeout: defaultConfig.DataSettings.LiveData.NewEventTimeout.Nanoseconds(),
DataCheckTimer: defaultConfig.DataSettings.LiveData.DataCheckTimer.Nanoseconds(),
RealOrders: defaultConfig.DataSettings.LiveData.RealOrders,
ClosePositionsOnStop: defaultConfig.DataSettings.LiveData.ClosePositionsOnStop,
DataRequestRetryTolerance: defaultConfig.DataSettings.LiveData.DataRequestRetryTolerance,
DataRequestRetryWaitTime: defaultConfig.DataSettings.LiveData.DataRequestRetryWaitTime.Nanoseconds(),
Credentials: creds,
}
}
if defaultConfig.DataSettings.CSVData != nil {

View File

@@ -112,13 +112,13 @@ func main() {
app.Commands = []*cli.Command{
executeStrategyFromFileCommand,
executeStrategyFromConfigCommand,
listAllRunsCommand,
startRunCommand,
startAllRunsCommand,
stopRunCommand,
stopAllRunsCommand,
clearRunCommand,
clearAllRunsCommand,
listAllTasksCommand,
startTaskCommand,
startAllTasksCommand,
stopTaskCommand,
stopAllTasksCommand,
clearTaskCommand,
clearAllTasksCommand,
}
ctx, cancel := context.WithCancel(context.Background())

File diff suppressed because it is too large Load Diff

View File

@@ -103,182 +103,182 @@ func local_request_BacktesterService_ExecuteStrategyFromConfig_0(ctx context.Con
}
func request_BacktesterService_ListAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListAllRunsRequest
func request_BacktesterService_ListAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := client.ListAllRuns(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.ListAllTasks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_ListAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListAllRunsRequest
func local_request_BacktesterService_ListAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := server.ListAllRuns(ctx, &protoReq)
msg, err := server.ListAllTasks(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_BacktesterService_StartRun_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
filter_BacktesterService_StartTask_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_BacktesterService_StartRun_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartRunRequest
func request_BacktesterService_StartTask_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartTaskRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StartRun_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StartTask_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.StartRun(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.StartTask(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_StartRun_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartRunRequest
func local_request_BacktesterService_StartTask_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartTaskRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StartRun_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StartTask_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.StartRun(ctx, &protoReq)
msg, err := server.StartTask(ctx, &protoReq)
return msg, metadata, err
}
func request_BacktesterService_StartAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartAllRunsRequest
func request_BacktesterService_StartAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := client.StartAllRuns(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.StartAllTasks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_StartAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartAllRunsRequest
func local_request_BacktesterService_StartAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StartAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := server.StartAllRuns(ctx, &protoReq)
msg, err := server.StartAllTasks(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_BacktesterService_StopRun_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
filter_BacktesterService_StopTask_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_BacktesterService_StopRun_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopRunRequest
func request_BacktesterService_StopTask_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopTaskRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StopRun_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StopTask_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.StopRun(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.StopTask(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_StopRun_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopRunRequest
func local_request_BacktesterService_StopTask_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopTaskRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StopRun_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_StopTask_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.StopRun(ctx, &protoReq)
msg, err := server.StopTask(ctx, &protoReq)
return msg, metadata, err
}
func request_BacktesterService_StopAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopAllRunsRequest
func request_BacktesterService_StopAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := client.StopAllRuns(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.StopAllTasks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_StopAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopAllRunsRequest
func local_request_BacktesterService_StopAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StopAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := server.StopAllRuns(ctx, &protoReq)
msg, err := server.StopAllTasks(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_BacktesterService_ClearRun_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
filter_BacktesterService_ClearTask_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_BacktesterService_ClearRun_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearRunRequest
func request_BacktesterService_ClearTask_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearTaskRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_ClearRun_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_ClearTask_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.ClearRun(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.ClearTask(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_ClearRun_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearRunRequest
func local_request_BacktesterService_ClearTask_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearTaskRequest
var metadata runtime.ServerMetadata
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_ClearRun_0); err != nil {
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_BacktesterService_ClearTask_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.ClearRun(ctx, &protoReq)
msg, err := server.ClearTask(ctx, &protoReq)
return msg, metadata, err
}
func request_BacktesterService_ClearAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearAllRunsRequest
func request_BacktesterService_ClearAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := client.ClearAllRuns(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
msg, err := client.ClearAllTasks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_ClearAllRuns_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearAllRunsRequest
func local_request_BacktesterService_ClearAllTasks_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ClearAllTasksRequest
var metadata runtime.ServerMetadata
msg, err := server.ClearAllRuns(ctx, &protoReq)
msg, err := server.ClearAllTasks(ctx, &protoReq)
return msg, metadata, err
}
@@ -339,7 +339,7 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
})
mux.Handle("GET", pattern_BacktesterService_ListAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_BacktesterService_ListAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -347,12 +347,12 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ListAllRuns", runtime.WithHTTPPathPattern("/v1/listallruns"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ListAllTasks", runtime.WithHTTPPathPattern("/v1/listalltasks"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_ListAllRuns_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_BacktesterService_ListAllTasks_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -360,11 +360,11 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
return
}
forward_BacktesterService_ListAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_ListAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StartRun_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StartTask_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -372,12 +372,12 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StartRun", runtime.WithHTTPPathPattern("/v1/startrun"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StartTask", runtime.WithHTTPPathPattern("/v1/starttask"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_StartRun_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_BacktesterService_StartTask_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -385,11 +385,11 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
return
}
forward_BacktesterService_StartRun_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StartTask_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StartAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StartAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -397,12 +397,12 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StartAllRuns", runtime.WithHTTPPathPattern("/v1/startallruns"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StartAllTasks", runtime.WithHTTPPathPattern("/v1/startall"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_StartAllRuns_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_BacktesterService_StartAllTasks_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -410,11 +410,11 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
return
}
forward_BacktesterService_StartAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StartAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StopRun_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StopTask_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -422,12 +422,12 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StopRun", runtime.WithHTTPPathPattern("/v1/stoprun"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StopTask", runtime.WithHTTPPathPattern("/v1/stoptask"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_StopRun_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_BacktesterService_StopTask_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -435,11 +435,11 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
return
}
forward_BacktesterService_StopRun_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StopTask_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StopAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StopAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -447,12 +447,12 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StopAllRuns", runtime.WithHTTPPathPattern("/v1/stopallruns"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/StopAllTasks", runtime.WithHTTPPathPattern("/v1/stopalltasks"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_StopAllRuns_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_BacktesterService_StopAllTasks_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -460,11 +460,11 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
return
}
forward_BacktesterService_StopAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StopAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("DELETE", pattern_BacktesterService_ClearRun_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("DELETE", pattern_BacktesterService_ClearTask_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -472,12 +472,12 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ClearRun", runtime.WithHTTPPathPattern("/v1/clearrun"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ClearTask", runtime.WithHTTPPathPattern("/v1/cleartask"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_ClearRun_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_BacktesterService_ClearTask_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -485,11 +485,11 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
return
}
forward_BacktesterService_ClearRun_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_ClearTask_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("DELETE", pattern_BacktesterService_ClearAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("DELETE", pattern_BacktesterService_ClearAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
@@ -497,12 +497,12 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ClearAllRuns", runtime.WithHTTPPathPattern("/v1/clearallruns"))
annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ClearAllTasks", runtime.WithHTTPPathPattern("/v1/clearalltasks"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_ClearAllRuns_0(annotatedContext, inboundMarshaler, server, req, pathParams)
resp, md, err := local_request_BacktesterService_ClearAllTasks_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
@@ -510,7 +510,7 @@ func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.Se
return
}
forward_BacktesterService_ClearAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_ClearAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
@@ -599,157 +599,157 @@ func RegisterBacktesterServiceHandlerClient(ctx context.Context, mux *runtime.Se
})
mux.Handle("GET", pattern_BacktesterService_ListAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("GET", pattern_BacktesterService_ListAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ListAllRuns", runtime.WithHTTPPathPattern("/v1/listallruns"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ListAllTasks", runtime.WithHTTPPathPattern("/v1/listalltasks"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_ListAllRuns_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_BacktesterService_ListAllTasks_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_ListAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_ListAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StartRun_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StartTask_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StartRun", runtime.WithHTTPPathPattern("/v1/startrun"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StartTask", runtime.WithHTTPPathPattern("/v1/starttask"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_StartRun_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_BacktesterService_StartTask_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_StartRun_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StartTask_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StartAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StartAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StartAllRuns", runtime.WithHTTPPathPattern("/v1/startallruns"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StartAllTasks", runtime.WithHTTPPathPattern("/v1/startall"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_StartAllRuns_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_BacktesterService_StartAllTasks_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_StartAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StartAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StopRun_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StopTask_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StopRun", runtime.WithHTTPPathPattern("/v1/stoprun"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StopTask", runtime.WithHTTPPathPattern("/v1/stoptask"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_StopRun_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_BacktesterService_StopTask_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_StopRun_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StopTask_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_BacktesterService_StopAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("POST", pattern_BacktesterService_StopAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StopAllRuns", runtime.WithHTTPPathPattern("/v1/stopallruns"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/StopAllTasks", runtime.WithHTTPPathPattern("/v1/stopalltasks"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_StopAllRuns_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_BacktesterService_StopAllTasks_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_StopAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_StopAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("DELETE", pattern_BacktesterService_ClearRun_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("DELETE", pattern_BacktesterService_ClearTask_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ClearRun", runtime.WithHTTPPathPattern("/v1/clearrun"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ClearTask", runtime.WithHTTPPathPattern("/v1/cleartask"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_ClearRun_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_BacktesterService_ClearTask_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_ClearRun_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_ClearTask_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("DELETE", pattern_BacktesterService_ClearAllRuns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
mux.Handle("DELETE", pattern_BacktesterService_ClearAllTasks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
var annotatedContext context.Context
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ClearAllRuns", runtime.WithHTTPPathPattern("/v1/clearallruns"))
annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ClearAllTasks", runtime.WithHTTPPathPattern("/v1/clearalltasks"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_ClearAllRuns_0(annotatedContext, inboundMarshaler, client, req, pathParams)
resp, md, err := request_BacktesterService_ClearAllTasks_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_ClearAllRuns_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
forward_BacktesterService_ClearAllTasks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
@@ -761,19 +761,19 @@ var (
pattern_BacktesterService_ExecuteStrategyFromConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "executestrategyfromconfig"}, ""))
pattern_BacktesterService_ListAllRuns_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "listallruns"}, ""))
pattern_BacktesterService_ListAllTasks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "listalltasks"}, ""))
pattern_BacktesterService_StartRun_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "startrun"}, ""))
pattern_BacktesterService_StartTask_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "starttask"}, ""))
pattern_BacktesterService_StartAllRuns_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "startallruns"}, ""))
pattern_BacktesterService_StartAllTasks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "startall"}, ""))
pattern_BacktesterService_StopRun_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "stoprun"}, ""))
pattern_BacktesterService_StopTask_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "stoptask"}, ""))
pattern_BacktesterService_StopAllRuns_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "stopallruns"}, ""))
pattern_BacktesterService_StopAllTasks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "stopalltasks"}, ""))
pattern_BacktesterService_ClearRun_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "clearrun"}, ""))
pattern_BacktesterService_ClearTask_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "cleartask"}, ""))
pattern_BacktesterService_ClearAllRuns_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "clearallruns"}, ""))
pattern_BacktesterService_ClearAllTasks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "clearalltasks"}, ""))
)
var (
@@ -781,17 +781,17 @@ var (
forward_BacktesterService_ExecuteStrategyFromConfig_0 = runtime.ForwardResponseMessage
forward_BacktesterService_ListAllRuns_0 = runtime.ForwardResponseMessage
forward_BacktesterService_ListAllTasks_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StartRun_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StartTask_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StartAllRuns_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StartAllTasks_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StopRun_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StopTask_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StopAllRuns_0 = runtime.ForwardResponseMessage
forward_BacktesterService_StopAllTasks_0 = runtime.ForwardResponseMessage
forward_BacktesterService_ClearRun_0 = runtime.ForwardResponseMessage
forward_BacktesterService_ClearTask_0 = runtime.ForwardResponseMessage
forward_BacktesterService_ClearAllRuns_0 = runtime.ForwardResponseMessage
forward_BacktesterService_ClearAllTasks_0 = runtime.ForwardResponseMessage
)

View File

@@ -16,14 +16,14 @@
"application/json"
],
"paths": {
"/v1/clearallruns": {
"/v1/clearalltasks": {
"delete": {
"operationId": "BacktesterService_ClearAllRuns",
"operationId": "BacktesterService_ClearAllTasks",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcClearAllRunsResponse"
"$ref": "#/definitions/btrpcClearAllTasksResponse"
}
},
"default": {
@@ -38,14 +38,14 @@
]
}
},
"/v1/clearrun": {
"/v1/cleartask": {
"delete": {
"operationId": "BacktesterService_ClearRun",
"operationId": "BacktesterService_ClearTask",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcClearRunResponse"
"$ref": "#/definitions/btrpcClearTaskResponse"
}
},
"default": {
@@ -255,41 +255,45 @@
"type": "string"
},
{
"name": "config.dataSettings.liveData.apiKeyOverride",
"name": "config.dataSettings.liveData.newEventTimeout",
"in": "query",
"required": false,
"type": "string"
"type": "string",
"format": "int64"
},
{
"name": "config.dataSettings.liveData.apiSecretOverride",
"name": "config.dataSettings.liveData.dataCheckTimer",
"in": "query",
"required": false,
"type": "string"
"type": "string",
"format": "int64"
},
{
"name": "config.dataSettings.liveData.apiClientIdOverride",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.liveData.api2faOverride",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.liveData.apiSubAccountOverride",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.liveData.useRealOrders",
"name": "config.dataSettings.liveData.realOrders",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.dataSettings.liveData.closePositionsOnStop",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.dataSettings.liveData.dataRequestRetryTolerance",
"in": "query",
"required": false,
"type": "string",
"format": "int64"
},
{
"name": "config.dataSettings.liveData.dataRequestRetryWaitTime",
"in": "query",
"required": false,
"type": "string",
"format": "int64"
},
{
"name": "config.portfolioSettings.leverage.canUseLeverage",
"in": "query",
@@ -404,14 +408,14 @@
]
}
},
"/v1/listallruns": {
"/v1/listalltasks": {
"get": {
"operationId": "BacktesterService_ListAllRuns",
"operationId": "BacktesterService_ListAllTasks",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcListAllRunsResponse"
"$ref": "#/definitions/btrpcListAllTasksResponse"
}
},
"default": {
@@ -426,14 +430,14 @@
]
}
},
"/v1/startallruns": {
"/v1/startall": {
"post": {
"operationId": "BacktesterService_StartAllRuns",
"operationId": "BacktesterService_StartAllTasks",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcStartAllRunsResponse"
"$ref": "#/definitions/btrpcStartAllTasksResponse"
}
},
"default": {
@@ -448,14 +452,14 @@
]
}
},
"/v1/startrun": {
"/v1/starttask": {
"post": {
"operationId": "BacktesterService_StartRun",
"operationId": "BacktesterService_StartTask",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcStartRunResponse"
"$ref": "#/definitions/btrpcStartTaskResponse"
}
},
"default": {
@@ -478,14 +482,14 @@
]
}
},
"/v1/stopallruns": {
"/v1/stopalltasks": {
"post": {
"operationId": "BacktesterService_StopAllRuns",
"operationId": "BacktesterService_StopAllTasks",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcStopAllRunsResponse"
"$ref": "#/definitions/btrpcStopAllTasksResponse"
}
},
"default": {
@@ -500,14 +504,14 @@
]
}
},
"/v1/stoprun": {
"/v1/stoptask": {
"post": {
"operationId": "BacktesterService_StopRun",
"operationId": "BacktesterService_StopTask",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcStopRunResponse"
"$ref": "#/definitions/btrpcStopTaskResponse"
}
},
"default": {
@@ -556,28 +560,28 @@
}
}
},
"btrpcClearAllRunsResponse": {
"btrpcClearAllTasksResponse": {
"type": "object",
"properties": {
"clearedRuns": {
"clearedTasks": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcRunSummary"
"$ref": "#/definitions/btrpcTaskSummary"
}
},
"remainingRuns": {
"remainingTasks": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcRunSummary"
"$ref": "#/definitions/btrpcTaskSummary"
}
}
}
},
"btrpcClearRunResponse": {
"btrpcClearTaskResponse": {
"type": "object",
"properties": {
"clearedRun": {
"$ref": "#/definitions/btrpcRunSummary"
"clearedTask": {
"$ref": "#/definitions/btrpcTaskSummary"
}
}
},
@@ -764,6 +768,40 @@
}
}
},
"btrpcExchangeCredentials": {
"type": "object",
"properties": {
"exchange": {
"type": "string"
},
"keys": {
"$ref": "#/definitions/btrpcExchangeKeys"
}
}
},
"btrpcExchangeKeys": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"secret": {
"type": "string"
},
"clientId": {
"type": "string"
},
"pemKey": {
"type": "string"
},
"subAccount": {
"type": "string"
},
"oneTimePassword": {
"type": "string"
}
}
},
"btrpcExchangeLevelFunding": {
"type": "object",
"properties": {
@@ -787,8 +825,8 @@
"btrpcExecuteStrategyResponse": {
"type": "object",
"properties": {
"run": {
"$ref": "#/definitions/btrpcRunSummary"
"task": {
"$ref": "#/definitions/btrpcTaskSummary"
}
}
},
@@ -831,13 +869,13 @@
}
}
},
"btrpcListAllRunsResponse": {
"btrpcListAllTasksResponse": {
"type": "object",
"properties": {
"runs": {
"tasks": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcRunSummary"
"$ref": "#/definitions/btrpcTaskSummary"
}
}
}
@@ -845,23 +883,33 @@
"btrpcLiveData": {
"type": "object",
"properties": {
"apiKeyOverride": {
"type": "string"
"newEventTimeout": {
"type": "string",
"format": "int64"
},
"apiSecretOverride": {
"type": "string"
"dataCheckTimer": {
"type": "string",
"format": "int64"
},
"apiClientIdOverride": {
"type": "string"
},
"api2faOverride": {
"type": "string"
},
"apiSubAccountOverride": {
"type": "string"
},
"useRealOrders": {
"realOrders": {
"type": "boolean"
},
"closePositionsOnStop": {
"type": "boolean"
},
"dataRequestRetryTolerance": {
"type": "string",
"format": "int64"
},
"dataRequestRetryWaitTime": {
"type": "string",
"format": "int64"
},
"credentials": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcExchangeCredentials"
}
}
}
},
@@ -893,7 +941,85 @@
}
}
},
"btrpcRunSummary": {
"btrpcSpotDetails": {
"type": "object",
"properties": {
"initialBaseFunds": {
"type": "string"
},
"initialQuoteFunds": {
"type": "string"
}
}
},
"btrpcStartAllTasksResponse": {
"type": "object",
"properties": {
"tasksStarted": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"btrpcStartTaskResponse": {
"type": "object",
"properties": {
"started": {
"type": "boolean"
}
}
},
"btrpcStatisticSettings": {
"type": "object",
"properties": {
"riskFreeRate": {
"type": "string"
}
}
},
"btrpcStopAllTasksResponse": {
"type": "object",
"properties": {
"tasksStopped": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcTaskSummary"
}
}
}
},
"btrpcStopTaskResponse": {
"type": "object",
"properties": {
"stoppedTask": {
"$ref": "#/definitions/btrpcTaskSummary"
}
}
},
"btrpcStrategySettings": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"useSimultaneousSignalProcessing": {
"type": "boolean"
},
"disableUsdTracking": {
"type": "boolean"
},
"customSettings": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcCustomSettings"
}
}
},
"title": "struct definitions"
},
"btrpcTaskSummary": {
"type": "object",
"properties": {
"id": {
@@ -922,84 +1048,6 @@
}
}
},
"btrpcSpotDetails": {
"type": "object",
"properties": {
"initialBaseFunds": {
"type": "string"
},
"initialQuoteFunds": {
"type": "string"
}
}
},
"btrpcStartAllRunsResponse": {
"type": "object",
"properties": {
"runsStarted": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"btrpcStartRunResponse": {
"type": "object",
"properties": {
"started": {
"type": "boolean"
}
}
},
"btrpcStatisticSettings": {
"type": "object",
"properties": {
"riskFreeRate": {
"type": "string"
}
}
},
"btrpcStopAllRunsResponse": {
"type": "object",
"properties": {
"runsStopped": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcRunSummary"
}
}
}
},
"btrpcStopRunResponse": {
"type": "object",
"properties": {
"stoppedRun": {
"$ref": "#/definitions/btrpcRunSummary"
}
}
},
"btrpcStrategySettings": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"useSimultaneousSignalProcessing": {
"type": "boolean"
},
"disableUsdTracking": {
"type": "boolean"
},
"customSettings": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcCustomSettings"
}
}
},
"title": "struct definitions"
},
"protobufAny": {
"type": "object",
"properties": {

View File

@@ -24,13 +24,13 @@ const _ = grpc.SupportPackageIsVersion7
type BacktesterServiceClient interface {
ExecuteStrategyFromFile(ctx context.Context, in *ExecuteStrategyFromFileRequest, opts ...grpc.CallOption) (*ExecuteStrategyResponse, error)
ExecuteStrategyFromConfig(ctx context.Context, in *ExecuteStrategyFromConfigRequest, opts ...grpc.CallOption) (*ExecuteStrategyResponse, error)
ListAllRuns(ctx context.Context, in *ListAllRunsRequest, opts ...grpc.CallOption) (*ListAllRunsResponse, error)
StartRun(ctx context.Context, in *StartRunRequest, opts ...grpc.CallOption) (*StartRunResponse, error)
StartAllRuns(ctx context.Context, in *StartAllRunsRequest, opts ...grpc.CallOption) (*StartAllRunsResponse, error)
StopRun(ctx context.Context, in *StopRunRequest, opts ...grpc.CallOption) (*StopRunResponse, error)
StopAllRuns(ctx context.Context, in *StopAllRunsRequest, opts ...grpc.CallOption) (*StopAllRunsResponse, error)
ClearRun(ctx context.Context, in *ClearRunRequest, opts ...grpc.CallOption) (*ClearRunResponse, error)
ClearAllRuns(ctx context.Context, in *ClearAllRunsRequest, opts ...grpc.CallOption) (*ClearAllRunsResponse, error)
ListAllTasks(ctx context.Context, in *ListAllTasksRequest, opts ...grpc.CallOption) (*ListAllTasksResponse, error)
StartTask(ctx context.Context, in *StartTaskRequest, opts ...grpc.CallOption) (*StartTaskResponse, error)
StartAllTasks(ctx context.Context, in *StartAllTasksRequest, opts ...grpc.CallOption) (*StartAllTasksResponse, error)
StopTask(ctx context.Context, in *StopTaskRequest, opts ...grpc.CallOption) (*StopTaskResponse, error)
StopAllTasks(ctx context.Context, in *StopAllTasksRequest, opts ...grpc.CallOption) (*StopAllTasksResponse, error)
ClearTask(ctx context.Context, in *ClearTaskRequest, opts ...grpc.CallOption) (*ClearTaskResponse, error)
ClearAllTasks(ctx context.Context, in *ClearAllTasksRequest, opts ...grpc.CallOption) (*ClearAllTasksResponse, error)
}
type backtesterServiceClient struct {
@@ -59,63 +59,63 @@ func (c *backtesterServiceClient) ExecuteStrategyFromConfig(ctx context.Context,
return out, nil
}
func (c *backtesterServiceClient) ListAllRuns(ctx context.Context, in *ListAllRunsRequest, opts ...grpc.CallOption) (*ListAllRunsResponse, error) {
out := new(ListAllRunsResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ListAllRuns", in, out, opts...)
func (c *backtesterServiceClient) ListAllTasks(ctx context.Context, in *ListAllTasksRequest, opts ...grpc.CallOption) (*ListAllTasksResponse, error) {
out := new(ListAllTasksResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ListAllTasks", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backtesterServiceClient) StartRun(ctx context.Context, in *StartRunRequest, opts ...grpc.CallOption) (*StartRunResponse, error) {
out := new(StartRunResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StartRun", in, out, opts...)
func (c *backtesterServiceClient) StartTask(ctx context.Context, in *StartTaskRequest, opts ...grpc.CallOption) (*StartTaskResponse, error) {
out := new(StartTaskResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StartTask", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backtesterServiceClient) StartAllRuns(ctx context.Context, in *StartAllRunsRequest, opts ...grpc.CallOption) (*StartAllRunsResponse, error) {
out := new(StartAllRunsResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StartAllRuns", in, out, opts...)
func (c *backtesterServiceClient) StartAllTasks(ctx context.Context, in *StartAllTasksRequest, opts ...grpc.CallOption) (*StartAllTasksResponse, error) {
out := new(StartAllTasksResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StartAllTasks", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backtesterServiceClient) StopRun(ctx context.Context, in *StopRunRequest, opts ...grpc.CallOption) (*StopRunResponse, error) {
out := new(StopRunResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StopRun", in, out, opts...)
func (c *backtesterServiceClient) StopTask(ctx context.Context, in *StopTaskRequest, opts ...grpc.CallOption) (*StopTaskResponse, error) {
out := new(StopTaskResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StopTask", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backtesterServiceClient) StopAllRuns(ctx context.Context, in *StopAllRunsRequest, opts ...grpc.CallOption) (*StopAllRunsResponse, error) {
out := new(StopAllRunsResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StopAllRuns", in, out, opts...)
func (c *backtesterServiceClient) StopAllTasks(ctx context.Context, in *StopAllTasksRequest, opts ...grpc.CallOption) (*StopAllTasksResponse, error) {
out := new(StopAllTasksResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/StopAllTasks", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backtesterServiceClient) ClearRun(ctx context.Context, in *ClearRunRequest, opts ...grpc.CallOption) (*ClearRunResponse, error) {
out := new(ClearRunResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ClearRun", in, out, opts...)
func (c *backtesterServiceClient) ClearTask(ctx context.Context, in *ClearTaskRequest, opts ...grpc.CallOption) (*ClearTaskResponse, error) {
out := new(ClearTaskResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ClearTask", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backtesterServiceClient) ClearAllRuns(ctx context.Context, in *ClearAllRunsRequest, opts ...grpc.CallOption) (*ClearAllRunsResponse, error) {
out := new(ClearAllRunsResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ClearAllRuns", in, out, opts...)
func (c *backtesterServiceClient) ClearAllTasks(ctx context.Context, in *ClearAllTasksRequest, opts ...grpc.CallOption) (*ClearAllTasksResponse, error) {
out := new(ClearAllTasksResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ClearAllTasks", in, out, opts...)
if err != nil {
return nil, err
}
@@ -128,13 +128,13 @@ func (c *backtesterServiceClient) ClearAllRuns(ctx context.Context, in *ClearAll
type BacktesterServiceServer interface {
ExecuteStrategyFromFile(context.Context, *ExecuteStrategyFromFileRequest) (*ExecuteStrategyResponse, error)
ExecuteStrategyFromConfig(context.Context, *ExecuteStrategyFromConfigRequest) (*ExecuteStrategyResponse, error)
ListAllRuns(context.Context, *ListAllRunsRequest) (*ListAllRunsResponse, error)
StartRun(context.Context, *StartRunRequest) (*StartRunResponse, error)
StartAllRuns(context.Context, *StartAllRunsRequest) (*StartAllRunsResponse, error)
StopRun(context.Context, *StopRunRequest) (*StopRunResponse, error)
StopAllRuns(context.Context, *StopAllRunsRequest) (*StopAllRunsResponse, error)
ClearRun(context.Context, *ClearRunRequest) (*ClearRunResponse, error)
ClearAllRuns(context.Context, *ClearAllRunsRequest) (*ClearAllRunsResponse, error)
ListAllTasks(context.Context, *ListAllTasksRequest) (*ListAllTasksResponse, error)
StartTask(context.Context, *StartTaskRequest) (*StartTaskResponse, error)
StartAllTasks(context.Context, *StartAllTasksRequest) (*StartAllTasksResponse, error)
StopTask(context.Context, *StopTaskRequest) (*StopTaskResponse, error)
StopAllTasks(context.Context, *StopAllTasksRequest) (*StopAllTasksResponse, error)
ClearTask(context.Context, *ClearTaskRequest) (*ClearTaskResponse, error)
ClearAllTasks(context.Context, *ClearAllTasksRequest) (*ClearAllTasksResponse, error)
mustEmbedUnimplementedBacktesterServiceServer()
}
@@ -148,26 +148,26 @@ func (UnimplementedBacktesterServiceServer) ExecuteStrategyFromFile(context.Cont
func (UnimplementedBacktesterServiceServer) ExecuteStrategyFromConfig(context.Context, *ExecuteStrategyFromConfigRequest) (*ExecuteStrategyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExecuteStrategyFromConfig not implemented")
}
func (UnimplementedBacktesterServiceServer) ListAllRuns(context.Context, *ListAllRunsRequest) (*ListAllRunsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListAllRuns not implemented")
func (UnimplementedBacktesterServiceServer) ListAllTasks(context.Context, *ListAllTasksRequest) (*ListAllTasksResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListAllTasks not implemented")
}
func (UnimplementedBacktesterServiceServer) StartRun(context.Context, *StartRunRequest) (*StartRunResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StartRun not implemented")
func (UnimplementedBacktesterServiceServer) StartTask(context.Context, *StartTaskRequest) (*StartTaskResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StartTask not implemented")
}
func (UnimplementedBacktesterServiceServer) StartAllRuns(context.Context, *StartAllRunsRequest) (*StartAllRunsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StartAllRuns not implemented")
func (UnimplementedBacktesterServiceServer) StartAllTasks(context.Context, *StartAllTasksRequest) (*StartAllTasksResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StartAllTasks not implemented")
}
func (UnimplementedBacktesterServiceServer) StopRun(context.Context, *StopRunRequest) (*StopRunResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopRun not implemented")
func (UnimplementedBacktesterServiceServer) StopTask(context.Context, *StopTaskRequest) (*StopTaskResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopTask not implemented")
}
func (UnimplementedBacktesterServiceServer) StopAllRuns(context.Context, *StopAllRunsRequest) (*StopAllRunsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopAllRuns not implemented")
func (UnimplementedBacktesterServiceServer) StopAllTasks(context.Context, *StopAllTasksRequest) (*StopAllTasksResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method StopAllTasks not implemented")
}
func (UnimplementedBacktesterServiceServer) ClearRun(context.Context, *ClearRunRequest) (*ClearRunResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearRun not implemented")
func (UnimplementedBacktesterServiceServer) ClearTask(context.Context, *ClearTaskRequest) (*ClearTaskResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearTask not implemented")
}
func (UnimplementedBacktesterServiceServer) ClearAllRuns(context.Context, *ClearAllRunsRequest) (*ClearAllRunsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearAllRuns not implemented")
func (UnimplementedBacktesterServiceServer) ClearAllTasks(context.Context, *ClearAllTasksRequest) (*ClearAllTasksResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ClearAllTasks not implemented")
}
func (UnimplementedBacktesterServiceServer) mustEmbedUnimplementedBacktesterServiceServer() {}
@@ -218,128 +218,128 @@ func _BacktesterService_ExecuteStrategyFromConfig_Handler(srv interface{}, ctx c
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_ListAllRuns_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAllRunsRequest)
func _BacktesterService_ListAllTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAllTasksRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).ListAllRuns(ctx, in)
return srv.(BacktesterServiceServer).ListAllTasks(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/ListAllRuns",
FullMethod: "/btrpc.BacktesterService/ListAllTasks",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).ListAllRuns(ctx, req.(*ListAllRunsRequest))
return srv.(BacktesterServiceServer).ListAllTasks(ctx, req.(*ListAllTasksRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_StartRun_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartRunRequest)
func _BacktesterService_StartTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartTaskRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).StartRun(ctx, in)
return srv.(BacktesterServiceServer).StartTask(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/StartRun",
FullMethod: "/btrpc.BacktesterService/StartTask",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).StartRun(ctx, req.(*StartRunRequest))
return srv.(BacktesterServiceServer).StartTask(ctx, req.(*StartTaskRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_StartAllRuns_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartAllRunsRequest)
func _BacktesterService_StartAllTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StartAllTasksRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).StartAllRuns(ctx, in)
return srv.(BacktesterServiceServer).StartAllTasks(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/StartAllRuns",
FullMethod: "/btrpc.BacktesterService/StartAllTasks",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).StartAllRuns(ctx, req.(*StartAllRunsRequest))
return srv.(BacktesterServiceServer).StartAllTasks(ctx, req.(*StartAllTasksRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_StopRun_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StopRunRequest)
func _BacktesterService_StopTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StopTaskRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).StopRun(ctx, in)
return srv.(BacktesterServiceServer).StopTask(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/StopRun",
FullMethod: "/btrpc.BacktesterService/StopTask",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).StopRun(ctx, req.(*StopRunRequest))
return srv.(BacktesterServiceServer).StopTask(ctx, req.(*StopTaskRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_StopAllRuns_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StopAllRunsRequest)
func _BacktesterService_StopAllTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(StopAllTasksRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).StopAllRuns(ctx, in)
return srv.(BacktesterServiceServer).StopAllTasks(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/StopAllRuns",
FullMethod: "/btrpc.BacktesterService/StopAllTasks",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).StopAllRuns(ctx, req.(*StopAllRunsRequest))
return srv.(BacktesterServiceServer).StopAllTasks(ctx, req.(*StopAllTasksRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_ClearRun_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearRunRequest)
func _BacktesterService_ClearTask_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearTaskRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).ClearRun(ctx, in)
return srv.(BacktesterServiceServer).ClearTask(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/ClearRun",
FullMethod: "/btrpc.BacktesterService/ClearTask",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).ClearRun(ctx, req.(*ClearRunRequest))
return srv.(BacktesterServiceServer).ClearTask(ctx, req.(*ClearTaskRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_ClearAllRuns_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearAllRunsRequest)
func _BacktesterService_ClearAllTasks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ClearAllTasksRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).ClearAllRuns(ctx, in)
return srv.(BacktesterServiceServer).ClearAllTasks(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/ClearAllRuns",
FullMethod: "/btrpc.BacktesterService/ClearAllTasks",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).ClearAllRuns(ctx, req.(*ClearAllRunsRequest))
return srv.(BacktesterServiceServer).ClearAllTasks(ctx, req.(*ClearAllTasksRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -360,32 +360,32 @@ var BacktesterService_ServiceDesc = grpc.ServiceDesc{
Handler: _BacktesterService_ExecuteStrategyFromConfig_Handler,
},
{
MethodName: "ListAllRuns",
Handler: _BacktesterService_ListAllRuns_Handler,
MethodName: "ListAllTasks",
Handler: _BacktesterService_ListAllTasks_Handler,
},
{
MethodName: "StartRun",
Handler: _BacktesterService_StartRun_Handler,
MethodName: "StartTask",
Handler: _BacktesterService_StartTask_Handler,
},
{
MethodName: "StartAllRuns",
Handler: _BacktesterService_StartAllRuns_Handler,
MethodName: "StartAllTasks",
Handler: _BacktesterService_StartAllTasks_Handler,
},
{
MethodName: "StopRun",
Handler: _BacktesterService_StopRun_Handler,
MethodName: "StopTask",
Handler: _BacktesterService_StopTask_Handler,
},
{
MethodName: "StopAllRuns",
Handler: _BacktesterService_StopAllRuns_Handler,
MethodName: "StopAllTasks",
Handler: _BacktesterService_StopAllTasks_Handler,
},
{
MethodName: "ClearRun",
Handler: _BacktesterService_ClearRun_Handler,
MethodName: "ClearTask",
Handler: _BacktesterService_ClearTask_Handler,
},
{
MethodName: "ClearAllRuns",
Handler: _BacktesterService_ClearAllRuns_Handler,
MethodName: "ClearAllTasks",
Handler: _BacktesterService_ClearAllTasks_Handler,
},
},
Streams: []grpc.StreamDesc{},

View File

@@ -86,6 +86,10 @@ func RegisterBacktesterSubLoggers() error {
if err != nil {
return err
}
LiveStrategy, err = log.NewSubLogger("LiveStrategy")
if err != nil {
return err
}
Setup, err = log.NewSubLogger("Setup")
if err != nil {
return err
@@ -110,10 +114,6 @@ func RegisterBacktesterSubLoggers() error {
if err != nil {
return err
}
Backtester, err = log.NewSubLogger("Sizing")
if err != nil {
return err
}
Holdings, err = log.NewSubLogger("Holdings")
if err != nil {
return err
@@ -122,6 +122,10 @@ func RegisterBacktesterSubLoggers() error {
if err != nil {
return err
}
FundManager, err = log.NewSubLogger("FundManager")
if err != nil {
return err
}
// Set to existing registered sub-loggers
Config = log.ConfigMgr

View File

@@ -6,6 +6,7 @@ import (
"testing"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
func TestCanTransact(t *testing.T) {
@@ -244,3 +245,16 @@ func TestGenerateFileName(t *testing.T) {
t.Errorf("received '%v' expected '%v'", name, "hell0_.moto")
}
}
func TestRegisterBacktesterSubLoggers(t *testing.T) {
t.Parallel()
err := RegisterBacktesterSubLoggers()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = RegisterBacktesterSubLoggers()
if !errors.Is(err, log.ErrSubLoggerAlreadyRegistered) {
t.Errorf("received '%v' expected '%v'", err, log.ErrSubLoggerAlreadyRegistered)
}
}

View File

@@ -26,9 +26,6 @@ const (
)
var (
// ErrNilArguments is a common error response to highlight that nils were passed in
// when they should not have been
ErrNilArguments = errors.New("received nil argument(s)")
// ErrNilEvent is a common error for whenever a nil event occurs when it shouldn't have
ErrNilEvent = errors.New("nil event received")
// ErrInvalidDataType occurs when an invalid data type is defined in the config
@@ -39,8 +36,8 @@ var (
errCannotGenerateFileName = errors.New("cannot generate filename")
)
// EventHandler interface implements required GetTime() & Pair() return
type EventHandler interface {
// Event interface implements required GetTime() & Pair() return
type Event interface {
GetBase() *event.Base
GetOffset() int64
SetOffset(int64)
@@ -61,6 +58,7 @@ type EventHandler interface {
// custom subloggers for backtester use
var (
Backtester *log.SubLogger
LiveStrategy *log.SubLogger
Setup *log.SubLogger
Strategy *log.SubLogger
Config *log.SubLogger
@@ -73,18 +71,9 @@ var (
FundingStatistics *log.SubLogger
Holdings *log.SubLogger
Data *log.SubLogger
FundManager *log.SubLogger
)
// DataEventHandler interface used for loading and interacting with Data
type DataEventHandler interface {
EventHandler
GetUnderlyingPair() currency.Pair
GetClosePrice() decimal.Decimal
GetHighPrice() decimal.Decimal
GetLowPrice() decimal.Decimal
GetOpenPrice() decimal.Decimal
}
// Directioner dictates the side of an order
type Directioner interface {
SetDirection(side order.Side)

View File

@@ -19,42 +19,43 @@ You can track ideas, planned features and what's in progress on this Trello boar
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Config package overview
This readme contains details for both the GoCryptoTrader Backtester config structure along with the strategy config structure
## Backtester Config overview
## GoCryptoTrader Backtester Config overview
Below are the details for the GoCryptoTrader Backtester _application_ config. Strategy config overview is below this section
| Key | Description | Example |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------|
| PrintLogo | Whether to print the GoCryptoTrader Backtester logo on startup. Recommended because it looks good | `true` |
| Verbose | Whether to receive verbose output. If running a GRPC server, it outputs to the server, not to the client | `false` |
| LogSubheaders | Whether log output contains a descriptor of what area the log is coming from, for example `STRATEGY`. Helpful for debugging | `true` |
| SingleRun | Whether or not to run the GoCryptoTrader Backtester to read the `SingleRunStrategyConfig` strategy and exit afterwards. If false, will run a GRPC server | `false` |
| SingleRunStrategyConfig | The path to the strategy to run when `SingleRun` is `true` | `path\to\strategy\example.strat` |
| Report | Contains details on the output report after a successful backtesting run | See Report table below |
| GRPC | Contains GRPC server details | See GRPC table below |
| UseCMDColours | If enabled, will output pretty colours of your choosing when running the application | `true` |
| Colours | Contains details on what the colour definitions are | See Colours table below |
| print-logo | Whether to print the GoCryptoTrader Backtester logo on startup. Recommended because it looks good | `true` |
| verbose | Whether to receive verbose output. If running a GRPC server, it outputs to the server, not to the client | `false` |
| log-subheaders | Whether log output contains a descriptor of what area the log is coming from, for example `STRATEGY`. Helpful for debugging | `true` |
| stop-all-tasks-on-close | When closing the application, the Backtester will attempt to stop all active tasks | `true` |
| plugin-path | When using custom strategy plugins, you can enter the path here to automatically load the plugin | `true` |
| report | Contains details on the output report after a successful backtesting run | See Report table below |
| grpc | Contains GRPC server details | See GRPC table below |
| use-cmd-colours | If enabled, will output pretty colours of your choosing when running the application | `true` |
| cmd-colours | Contains details on what the colour definitions are | See Colours table below |
### Backtester Config Report overview
| Key | Description | Example |
|----------------|----------------------------------------------------------------------|---------------------------------|
| GenerateReport | Whether or not to output a report after a successful backtesting run | `true` |
| TemplatePath | The path for the template to use when generating a report | `/backtester/report/tpl.gohtml` |
| OutputPath | The path where report output is saved | `/backtester/results` |
| DarkMode | Whether or not the report defaults to using dark mode | `true` |
| output-report | Whether or not to output a report after a successful backtesting run | `true` |
| template-path | The path for the template to use when generating a report | `/backtester/report/tpl.gohtml` |
| output-path | The path where report output is saved | `/backtester/results` |
| dark-mode | Whether or not the report defaults to using dark mode | `true` |
### Backtester Config GRPC overview
| Key | Description | Example |
|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| Username | Your username to negotiate a successful connection with the server | `rpcuser` |
| Password | Your password to negotiate a successful connection with the server | `helloImTheDefaultPassword` |
| Enabled | Whether the server is enabled. Setting this to `false` and `SingleRun` to `false` would be inadvisable | `true` |
| ListenAddress | The listen address for the GRPC server | `localhost:42069` |
| GRPCProxyEnabled | If enabled, creates a proxy server to interact with the GRPC server via HTTP commands | `true` |
| GRPCProxyListenAddress | The address for the proxy to listen on | `localhost:9053` |
| TLSDir | The directory for holding your TLS certifications to make connections to the server. Will be generated by default on startup if not present | `/backtester/config/location/` |
| username | Your username to negotiate a successful connection with the server | `rpcuser` |
| password | Your password to negotiate a successful connection with the server | `helloImTheDefaultPassword` |
| enabled | Whether the server is enabled. Setting this to `false` and `SingleRun` to `false` would be inadvisable | `true` |
| listenAddress | The listen address for the GRPC server | `localhost:9054` |
| grpcProxyEnabled | If enabled, creates a proxy server to interact with the GRPC server via HTTP commands | `true` |
| grpcProxyListenAddress | The address for the proxy to listen on | `localhost:9053` |
| tls-dir | The directory for holding your TLS certifications to make connections to the server. Will be generated by default on startup if not present | `/backtester/config/location/` |
### Backtester Config Colours overview
@@ -97,123 +98,111 @@ See below for a set of tables and fields, expected values and what they can do
#### Config
| Key | Description |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Nickname | A nickname for the specific config. When running multiple variants of the same strategy, use the nickname to help differentiate between runs |
| Goal | A description of what you would hope the outcome to be. When verifying output, you can review and confirm whether the strategy met that goal |
| CurrencySettings | Currency settings is an array of settings for each individual currency you wish to run the strategy against |
| StrategySettings | Select which strategy to run, what custom settings to load and whether the strategy can assess multiple currencies at once to make more in-depth decisions |
| FundingSettings | Defines whether individual funding settings can be used. Defines the funding exchange, asset, currencies at an individual level |
| PortfolioSettings | Contains a list of global rules for the portfolio manager. CurrencySettings contain their own rules on things like how big a position is allowable, the portfolio manager rules are the same, but override any individual currency's settings |
| StatisticSettings | Contains settings that impact statistics calculation. Such as the risk-free rate for the sharpe ratio |
| Key | Description |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| nickname | A nickname for the specific config. When running multiple variants of the same strategy, use the nickname to help differentiate between runs |
| goal | A description of what you would hope the outcome to be. When verifying output, you can review and confirm whether the strategy met that goal |
| strategy-settings | Select which strategy to run, what custom settings to load and whether the strategy can assess multiple currencies at once to make more in-depth decisions |
| funding-settings | Defines whether individual funding settings can be used. Defines the funding exchange, asset, currencies at an individual level |
| currency-settings | Currency settings is an array of settings for each individual currency you wish to run the strategy against |
| data-settings | Holds data retrieval settings. Determines how the GoCryptoTraderBacktester will fetch data and in what format |
| portfolio-settings | Contains a list of global rules for the portfolio manager. CurrencySettings contain their own rules on things like how big a position is allowable, the portfolio manager rules are the same, but override any individual currency's settings |
| statistic-settings | Contains settings that impact statistics calculation. Such as the risk-free rate for the sharpe ratio |
#### Strategy Settings
| Key | Description | Example |
|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| Name | The strategy to use | `rsi` |
| UsesSimultaneousProcessing | This denotes whether multiple currencies are processed simultaneously with the strategy function `OnSimultaneousSignals`. Eg If you have multiple CurrencySettings and only wish to purchase BTC-USDT when XRP-DOGE is 1337, this setting is useful as you can analyse both signal events to output a purchase call for BTC | `true` |
| CustomSettings | This is a map where you can enter custom settings for a strategy. The RSI strategy allows for customisation of the upper, lower and length variables to allow you to change them from 70, 30 and 14 respectively to 69, 36, 12 | `"custom-settings": { "rsi-high": 70, "rsi-low": 30, "rsi-period": 14 } ` |
| DisableUSDTracking | If `false`, will track all currencies used in your strategy against USD equivalent candles. For example, if you are running a strategy for BTC/XRP, then the GoCryptoTrader Backtester will also retreive candles data for BTC/USD and XRP/USD to then track strategy performance against a single currency. This also tracks against USDT and other USD tracked stablecoins, so one exchange supporting USDT and another BUSD will still allow unified strategy performance analysis. If disabled, will not track against USD, this can be especially helpful when running strategies under live, database and CSV based data | `false` |
| Key | Description | Example |
|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
| name | The strategy to use | `rsi` |
| use-simultaneous-signal-processing | This denotes whether multiple currencies are processed simultaneously with the strategy function `OnSimultaneousSignals`. Eg If you have multiple CurrencySettings and only wish to purchase BTC-USDT when XRP-DOGE is 1337, this setting is useful as you can analyse both signal events to output a purchase call for BTC | `true` |
| disable-usd-tracking | 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` |
| custom-settings | This is a map where you can enter custom settings for a strategy. The RSI strategy allows for customisation of the upper, lower and length variables to allow you to change them from 70, 30 and 14 respectively to 69, 36, 12 | `"custom-settings": { "rsi-high": 70, "rsi-low": 30, "rsi-period": 14 } ` |
#### Funding Config Settings
| Key | Description | Example |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| UseExchangeLevelFunding | Allows shared funding at an exchange asset level. You can set funding for `USDT` and all pairs that feature `USDT` will have access to those funds when making orders. See [this](/backtester/funding/README.md) for more information | `false` |
| ExchangeLevelFunding | An array of exchange level funding settings. See below, or [this](/backtester/funding/README.md) for more information | `[]` |
| Key | Description | Example |
|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| use-exchange-level-funding | 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` |
| exchange-level-funding | An array of exchange level funding settings. See below, or [this](/backtester/funding/README.md) for more information | `[]` |
##### Funding Item Config Settings
| Key | Description | Example |
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|
| ExchangeName | The exchange to set funds. See [here](https://github.com/thrasher-corp/gocryptotrader/blob/master/README.md) for a list of supported exchanges | `Binance` |
| Asset | The asset type to set funds. Typically, this will be `spot`, however, see [this package](https://github.com/thrasher-corp/gocryptotrader/blob/master/exchanges/asset/asset.go) for the various asset types GoCryptoTrader supports | `spot` |
| Currency | The currency to set funds | `BTC` |
| InitialFunds | The initial funding for the currency | `1337` |
| TransferFee | If your strategy utilises transferring of funds via the Funding Manager, this is deducted upon doing so | `0.005` |
| Key | Description | Example |
|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|
| exchange-name | The exchange to set funds. See [here](https://github.com/thrasher-corp/gocryptotrader/blob/master/README.md) for a list of supported exchanges | `Binance` |
| asset | The asset type to set funds. Typically, this will be `spot`, however, see [this package](https://github.com/thrasher-corp/gocryptotrader/blob/master/exchanges/asset/asset.go) for the various asset types GoCryptoTrader supports | `spot` |
| currency | The currency to set funds | `BTC` |
| initial-funds | The initial funding for the currency | `1337` |
| transfer-fee | If your strategy utilises transferring of funds via the Funding Manager, this is deducted upon doing so | `0.005` |
#### Currency Settings
| Key | Description | Example |
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|
| ExchangeName | The exchange to load. See [here](https://github.com/thrasher-corp/gocryptotrader/blob/master/README.md) for a list of supported exchanges | `Binance` |
| Asset | The asset type. Typically, this will be `spot`, however, see [this package](https://github.com/thrasher-corp/gocryptotrader/blob/master/exchanges/asset/asset.go) for the various asset types GoCryptoTrader supports | `spot` |
| Base | The base of a currency | `BTC` |
| Quote | The quote of a currency | `USDT` |
| InitialFunds | A legacy field, will be temporarily migrated to `InitialQuoteFunds` if present in your strat config | `` |
| BuySide | This struct defines the buying side rules this specific currency setting must abide by such as maximum purchase amount | - |
| SellSide | This struct defines the selling side rules this specific currency setting must abide by such as maximum selling amount | - |
| MinimumSlippagePercent | Is the lower bounds in a random number generated that make purchases more expensive, or sell events less valuable. If this value is 90, then the most a price can be affected is 10% | `90` |
| MaximumSlippagePercent | Is the upper bounds in a random number generated that make purchases more expensive, or sell events less valuable. If this value is 99, then the least a price can be affected is 1%. Set both upper and lower to 100 to have no randomness applied to purchase events | `100` |
| MakerFee | The fee to use when sizing and purchasing currency. If `nil`, will lookup an exchange's fee details | `0.001` |
| TakerFee | Unused fee for when an order is placed in the orderbook, rather than taken from the orderbook. If `nil`, will lookup an exchange's fee details | `0.002` |
| MaximumHoldingsRatio | When multiple currency settings are used, you may set a maximum holdings ratio to prevent having too large a stake in a single currency | `0.5` |
| CanUseExchangeLimits | Will lookup exchange rules around purchase sizing eg minimum order increments of 0.0005. Note: Will retrieve up-to-date rules which may not have existed for the data you are using. Best to use this when considering to use this strategy live | `false` |
| SkipCandleVolumeFitting | When placing orders, by default the BackTester will shrink an order's size to fit the candle data's volume so as to not rewrite history. Set this to `true` to ignore this and to set order size at what the portfolio manager prescribes | `false` |
| SpotSettings | An optional field which contains initial funding data for SPOT currency pairs | See SpotSettings table below |
| FuturesSettings | An optional field which contains leverage data for FUTURES currency pairs | See FuturesSettings table below |
| Key | Description | Example |
|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|
| exchange-name | The exchange to load. See [here](https://github.com/thrasher-corp/gocryptotrader/blob/master/README.md) for a list of supported exchanges | `Binance` |
| asset | The asset type. Typically, this will be `spot`, however, see [this package](https://github.com/thrasher-corp/gocryptotrader/blob/master/exchanges/asset/asset.go) for the various asset types GoCryptoTrader supports | `spot` |
| base | The base of a currency | `BTC` |
| quote | The quote of a currency | `USDT` |
| spot-details | An optional field which contains initial funding data for SPOT currency pairs | See SpotSettings table below |
| future-detailss | An optional field which contains leverage data for FUTURES currency pairs | See FuturesSettings table below |
| buy-side | This struct defines the buying side rules this specific currency setting must abide by such as maximum purchase amount |-- |
| sell-side | This struct defines the selling side rules this specific currency setting must abide by such as maximum selling amount |-- |
| min-slippage-percent | Is the lower bounds in a random number generated that make purchases more expensive, or sell events less valuable. If this value is 90, then the most a price can be affected is 10% | `90` |
| max-slippage-percent | Is the upper bounds in a random number generated that make purchases more expensive, or sell events less valuable. If this value is 99, then the least a price can be affected is 1%. Set both upper and lower to 100 to have no randomness applied to purchase events | `100` |
| maker-fee-override | The fee to use when sizing and purchasing currency. If `nil`, will lookup an exchange's fee details | `0.001` |
| taker-fee-override | Unused fee for when an order is placed in the orderbook, rather than taken from the orderbook. If `nil`, will lookup an exchange's fee details | `0.002` |
| maximum-holdings-ratio | When multiple currency settings are used, you may set a maximum holdings ratio to prevent having too large a stake in a single currency | `0.5` |
| skip-candle-volume-fitting | When placing orders, by default the BackTester will shrink an order's size to fit the candle data's volume so as to not rewrite history. Set this to `true` to ignore this and to set order size at what the portfolio manager prescribes | `false` |
| use-exchange-order-limits | Will lookup exchange rules around purchase sizing eg minimum order increments of 0.0005. Note: Will retrieve up-to-date rules which may not have existed for the data you are using. Best to use this when considering to use this strategy live | `false` |
| use-exchange-pnl-calculation | Instead of simulating the exchange's own way of calculating PNL, use a default method which calculates the value of an asset | `false` |
##### SpotSettings
| Key | Description | Example |
|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| InitialBaseFunds | The funds that the GoCryptoTraderBacktester has for the base currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `2` |
| InitialQuoteFunds | The funds that the GoCryptoTraderBacktester has for the quote currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `10000` |
| Key | Description | Example |
|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| initial-base-funds | The funds that the GoCryptoTraderBacktester has for the base currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `2` |
| initial-quote-funds | The funds that the GoCryptoTraderBacktester has for the quote currency. This is only required if the strategy setting `UseExchangeLevelFunding` is `false` | `10000` |
##### FuturesSettings
| Key | Description | Example |
|----------|------------------------------------------------------------------------------------------|---------|
| Leverage | This struct defines the leverage rules that this specific currency setting must abide by | `1` |
| leverage | This struct defines the leverage rules that this specific currency setting must abide by | `1` |
#### PortfolioSettings
| Key | Description |
|----------|------------------------------------------------------------------------------------------------------------------------|
| Leverage | This struct defines the leverage rules that this specific currency setting must abide by |
| BuySide | This struct defines the buying side rules this specific currency setting must abide by such as maximum purchase amount |
| SellSide | This struct defines the selling side rules this specific currency setting must abide by such as maximum selling amount |
#### StatisticsSettings
| Key | Description | Example |
|--------------|-------------------------------------------------------------------------|---------|
| RiskFreeRate | The risk free rate used in the calculation of sharpe and sortino ratios | `0.03` |
### DataSettings
| Key | Description | Example |
|---------------------------|--------------------------------------------------------------------------------------------------------|---------------|
| interval | The candle interval in `time.Duration` format eg set as`15000000000` for a value of `time.Second * 15` | `15000000000` |
| data-type | Choose whether `candle` or `trade` data is used. If trades are used, they will be converted to candles | `trade` |
| verbose-exchange-requests | When retrieving candle data from an exchange, print verbose request/response details | `false` |
| api-data | Holds API data settings. See table `APIData` | |
| database-data | Holds database data settings. See table `DatabaseData` | |
| live-data | Holds API data settings. See table `LiveData` | |
| csv-data | Holds CSV data settings. See table `CSVData` | |
#### APIData
| Key | Description | Example |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| DataType | Choose whether `candle` or `trade` data is used. If trades are used, they will be converted to candles | `trade` |
| 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` |
| 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` |
| Key | Description | Example |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| start-date | The start date to retrieve data | `2021-01-23T11:00:00+11:00` |
| end-date | The end date to retrieve data | `2021-01-24T11:00:00+11:00` |
| inclusive-end-date | 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` |
#### CSVData
| Key | Description | Example |
|----------|--------------------------------------------------------------------------------------------------------|--------------------------|
| DataType | Choose whether `candle` or `trade` data is used. If trades are used, they will be converted to candles | `candle` |
| Interval | The candle interval in `time.Duration` format eg set as`15000000000` for a value of `time.Second * 15` | `15000000000` |
| FullPath | The file to load | `/data/exchangelist.csv` |
| Key | Description | Example |
|-----------|------------------|--------------------------|
| full-path | The file to load | `/data/exchangelist.csv` |
#### DatabaseData
| Key | Description | Example |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| DataType | Choose whether `candle` or `trade` data is used. If trades are used, they will be converted to candles | `trade` |
| 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` |
| 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` |
| Key | Description | Example |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| start-date | The start date to retrieve data | `2021-01-23T11:00:00+11:00` |
| end-date | The end date to retrieve data | `2021-01-24T11:00:00+11:00` |
| 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 | `` |
| inclusive-end-date | 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
@@ -237,32 +226,65 @@ See below for a set of tables and fields, expected values and what they can do
#### LiveData
| Key | Description | Example |
|-----------------------|--------------------------------------------------------------------------------------------------------|---------------|
| DataType | Choose whether `candle` or `trade` data is used. If trades are used, they will be converted to candles | `candle` |
| Interval | The candle interval in `time.Duration` format eg set as`15000000000` for a value of `time.Second * 15` | `15000000000` |
| APIKeyOverride | Will set the GoCryptoTrader exchange to use the following API Key | `1234` |
| APISecretOverride | Will set the GoCryptoTrader exchange to use the following API Secret | `5678` |
| APIClientIDOverride | Will set the GoCryptoTrader exchange to use the following API Client ID | `9012` |
| API2FAOverride | Will set the GoCryptoTrader exchange to use the following 2FA seed | `hello-moto` |
| APISubaccountOverride | Will set the GoCryptoTrader exchange to use the following subaccount on supported exchanges | `subzero` |
| RealOrders | Whether to place real orders. You really should never consider using this. Ever ever | `true` |
| Key | Description | Example |
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
| new-event-timeout | The time allowed to wait for new data before exiting the strategy. Ensures new data is always coming in | `60000000000` |
| data-check-timer | The interval in which to check exchange API's for new data | `1000000000` |
| real-orders | Whether to place real orders with real money. Its likely you should never want to set this to true | `false` |
| close-positions-on-stop | As live trading doesn't stop until you tell it to, you can trigger a close of your position(s) when you stop the strategy | `true` |
| data-request-retry-tolerance | Rather than immediately closing a strategy on failure to retreive candle data, having a retry tolerance allows multiple attempts to return data | `3` |
| data-request-retry-wait-time | How long to wait in between request retries | `500000000` |
| exchange-credentials | A list of exchange credentials. See table named `ExchangeCredentials` | |
##### ExchangeCredentials Settings
| Key | Description | Example |
|-------------|-----------------------------------------------------------|-----------|
| exchange | The exchange to apply credentials to | `binance` |
| credentials | The API credentials to use. See table named `Credentials` | |
##### Credentials Settings
| Key | Description | Example |
|-----------------|---------------------------------------------------------------------------------------------|--------------|
| Key | Will set the GoCryptoTrader exchange to use the following API Key | `1234` |
| Secret | Will set the GoCryptoTrader exchange to use the following API Secret | `5678` |
| ClientID | Will set the GoCryptoTrader exchange to use the following API Client ID | `9012` |
| PEMKey | Private key for certain API requests. If you don't know it, you probably don't need it | `hello-moto` |
| SubAccount | Will set the GoCryptoTrader exchange to use the following subaccount on supported exchanges | `subzero` |
| OneTimePassword | Will set the GoCryptoTrader exchange to use the following 2FA seed | `subzero` |
#### PortfolioSettings
| Key | Description |
|-----------|------------------------------------------------------------------------------------------------------------------------|
| leverage | This struct defines the leverage rules that this specific currency setting must abide by |
| buy-side | This struct defines the buying side rules this specific currency setting must abide by such as maximum purchase amount |
| sell-side | This struct defines the selling side rules this specific currency setting must abide by such as maximum selling amount |
##### Leverage Settings
| Key | Description | Example |
|--------------------------------|------------------------------------------------------------------------------------------|---------|
| CanUseLeverage | Allows the use of leverage | `false` |
| MaximumOrdersWithLeverageRatio | If the ratio of leveraged orders for a currency exceeds this, the order cannot be placed | `0.5` |
| MaximumLeverageRate | Orders cannot be placed with leverage over this amount | `100` |
| Key | Description | Example |
|------------------------------------|-------------------------------|---------|
| can-use-leverage | Allows the use of leverage | `false` |
| maximum-orders-with-leverage-ratio | currently unused | `0.5` |
| maximum-leverage-rate | currently unused | `100` |
| maximum-collateral-leverage-rate | currently unused | `100` |
##### Buy/Sell Settings
| Key | Description | Example |
|--------------|------------------------------------------------------------------------------------------------------------------|---------|
| MinimumSize | If the order's quantity is below this, the order cannot be placed | `0.1` |
| MaximumSize | If the order's quantity is over this amount, it cannot be placed and will be reduced to the maximum amount | `10` |
| MaximumTotal | If the order's price * amount exceeds this number, the order cannot be placed and will be reduced to this figure | `1337` |
| Key | Description | Example |
|---------------|------------------------------------------------------------------------------------------------------------------|---------|
| minimum-size | If the order's quantity is below this, the order cannot be placed | `0.1` |
| maximum-size | If the order's quantity is over this amount, it cannot be placed and will be reduced to the maximum amount | `10` |
| maximum-total | If the order's price * amount exceeds this number, the order cannot be placed and will be reduced to this figure | `1337` |
#### StatisticsSettings
| Key | Description | Example |
|----------------|-------------------------------------------------------------------------|---------|
| risk-free-rate | The risk free rate used in the calculation of sharpe and sortino ratios | `0.03` |
### Please click GoDocs chevron above to view current GoDoc information for this package

View File

@@ -67,5 +67,6 @@ func GenerateDefaultConfig() (*BacktesterConfig, error) {
Warn: common.CMDColours.Warn,
Error: common.CMDColours.Error,
},
StopAllTasksOnClose: true,
}, nil
}

View File

@@ -18,14 +18,15 @@ var (
// BacktesterConfig contains the configuration for the backtester
type BacktesterConfig struct {
PluginPath string `json:"plugin-path"`
PrintLogo bool `json:"print-logo"`
Verbose bool `json:"verbose"`
LogSubheaders bool `json:"log-subheaders"`
Report Report `json:"report"`
GRPC GRPC `json:"grpc"`
UseCMDColours bool `json:"use-cmd-colours"`
Colours common.Colours `json:"cmd-colours"`
PrintLogo bool `json:"print-logo"`
LogSubheaders bool `json:"log-subheaders"`
Verbose bool `json:"verbose"`
StopAllTasksOnClose bool `json:"stop-all-tasks-on-close"`
PluginPath string `json:"plugin-path"`
Report Report `json:"report"`
GRPC GRPC `json:"grpc"`
UseCMDColours bool `json:"use-cmd-colours"`
Colours common.Colours `json:"cmd-colours"`
}
// Report contains the report settings

View File

@@ -35,7 +35,7 @@ func ReadStrategyConfigFromFile(path string) (*Config, error) {
// Validate checks all config settings
func (c *Config) Validate() error {
if c == nil {
return fmt.Errorf("%w nil config", common.ErrNilArguments)
return fmt.Errorf("%w config", gctcommon.ErrNilPointer)
}
err := c.validateDate()
if err != nil {
@@ -122,7 +122,7 @@ func (c *Config) validateStrategySettings() error {
}
}
}
strats := strategies.GetStrategies()
strats := strategies.GetSupportedStrategies()
for i := range strats {
if strings.EqualFold(strats[i].Name(), c.StrategySettings.Name) {
return nil
@@ -158,12 +158,11 @@ func (c *Config) validateCurrencySettings() error {
c.CurrencySettings[i].Asset == asset.PerpetualContract {
return errPerpetualsUnsupported
}
if c.CurrencySettings[i].Asset == asset.Futures &&
(c.CurrencySettings[i].Quote.String() == "PERP" || c.CurrencySettings[i].Base.String() == "PI") {
return errPerpetualsUnsupported
}
if c.CurrencySettings[i].Asset.IsFutures() {
hasFutures = true
if c.CurrencySettings[i].Quote.String() == "PERP" || c.CurrencySettings[i].Base.String() == "PI" {
return errPerpetualsUnsupported
}
}
if c.CurrencySettings[i].SpotDetails != nil {
if c.FundingSettings.UseExchangeLevelFunding {
@@ -239,12 +238,16 @@ func (c *Config) PrintSetting() {
if c.FundingSettings.UseExchangeLevelFunding && c.StrategySettings.SimultaneousSignalProcessing {
log.Info(common.Config, common.CMDColours.H2+"------------------Funding Settings---------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Use Exchange Level Funding: %v", c.FundingSettings.UseExchangeLevelFunding)
for i := range c.FundingSettings.ExchangeLevelFunding {
log.Infof(common.Config, "Initial funds for %v %v %v: %v",
c.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
c.FundingSettings.ExchangeLevelFunding[i].Asset,
c.FundingSettings.ExchangeLevelFunding[i].Currency,
c.FundingSettings.ExchangeLevelFunding[i].InitialFunds.Round(8))
if c.DataSettings.LiveData != nil && c.DataSettings.LiveData.RealOrders {
log.Infof(common.Config, "Funding levels will be set by the exchange")
} else {
for i := range c.FundingSettings.ExchangeLevelFunding {
log.Infof(common.Config, "Initial funds for %v %v %v: %v",
c.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
c.FundingSettings.ExchangeLevelFunding[i].Asset,
c.FundingSettings.ExchangeLevelFunding[i].Currency,
c.FundingSettings.ExchangeLevelFunding[i].InitialFunds.Round(8))
}
}
}
@@ -255,7 +258,10 @@ func (c *Config) PrintSetting() {
c.CurrencySettings[i].Quote)
log.Infof(common.Config, currStr[:61])
log.Infof(common.Config, "Exchange: %v", c.CurrencySettings[i].ExchangeName)
if !c.FundingSettings.UseExchangeLevelFunding && c.CurrencySettings[i].SpotDetails != nil {
switch {
case c.DataSettings.LiveData != nil && c.DataSettings.LiveData.RealOrders:
log.Infof(common.Config, "Funding levels will be set by the exchange")
case !c.FundingSettings.UseExchangeLevelFunding && c.CurrencySettings[i].SpotDetails != nil:
if c.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
log.Infof(common.Config, "Initial base funds: %v %v",
c.CurrencySettings[i].SpotDetails.InitialBaseFunds.Round(8),
@@ -299,8 +305,12 @@ func (c *Config) PrintSetting() {
log.Info(common.Config, common.CMDColours.H2+"------------------Live Settings------------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "REAL ORDERS: %v", c.DataSettings.LiveData.RealOrders)
log.Infof(common.Config, "Overriding GCT API settings: %v", c.DataSettings.LiveData.APIClientIDOverride != "")
log.Infof(common.Config, "Using real orders: %v", c.DataSettings.LiveData.RealOrders)
log.Infof(common.Config, "Data check timer: %v", c.DataSettings.LiveData.DataCheckTimer)
log.Infof(common.Config, "New event timeout: %v", c.DataSettings.LiveData.NewEventTimeout)
for i := range c.DataSettings.LiveData.ExchangeCredentials {
log.Infof(common.Config, "%s credentials: %s", c.DataSettings.LiveData.ExchangeCredentials[i].Exchange, c.DataSettings.LiveData.ExchangeCredentials[i].Keys.String())
}
}
if c.DataSettings.APIData != nil {
log.Info(common.Config, common.CMDColours.H2+"------------------API Settings-------------------------------"+common.CMDColours.Default)

View File

@@ -17,12 +17,13 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/database/drivers"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
const (
testExchange = "ftx"
mainExchange = "binance"
dca = "dollarcostaverage"
// change this if you modify a config and want it to save to the example folder
saveConfig = false
@@ -39,9 +40,17 @@ var (
MaximumSize: decimal.NewFromInt(2),
MaximumTotal: decimal.NewFromInt(40000),
}
// strictMinMax used for live order restrictions
strictMinMax = MinMax{
MinimumSize: decimal.NewFromFloat(0.001),
MaximumSize: decimal.NewFromFloat(0.05),
MaximumTotal: decimal.NewFromInt(100),
}
initialFunds1000000 *decimal.Decimal
initialFunds100000 *decimal.Decimal
initialFunds10 *decimal.Decimal
mainCurrencyPair = currency.NewPair(currency.BTC, currency.USDT)
)
func TestMain(m *testing.M) {
@@ -58,8 +67,8 @@ func TestValidateDate(t *testing.T) {
t.Parallel()
c := Config{}
err := c.validateDate()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
c.DataSettings = DataSettings{
DatabaseData: &DatabaseData{},
@@ -76,8 +85,8 @@ func TestValidateDate(t *testing.T) {
}
c.DataSettings.DatabaseData.EndDate = c.DataSettings.DatabaseData.StartDate.Add(time.Minute)
err = c.validateDate()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
c.DataSettings.APIData = &APIData{}
err = c.validateDate()
@@ -92,8 +101,8 @@ func TestValidateDate(t *testing.T) {
}
c.DataSettings.APIData.EndDate = c.DataSettings.APIData.StartDate.Add(time.Minute)
err = c.validateDate()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -127,9 +136,88 @@ func TestValidateCurrencySettings(t *testing.T) {
}
c.CurrencySettings[0].ExchangeName = "lol"
err = c.validateCurrencySettings()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
c.CurrencySettings[0].Asset = asset.PerpetualSwap
err = c.validateCurrencySettings()
if !errors.Is(err, errPerpetualsUnsupported) {
t.Errorf("received: %v, expected: %v", err, errPerpetualsUnsupported)
}
c.CurrencySettings[0].Asset = asset.USDTMarginedFutures
c.CurrencySettings[0].Quote = currency.NewCode("PERP")
err = c.validateCurrencySettings()
if !errors.Is(err, errPerpetualsUnsupported) {
t.Errorf("received: %v, expected: %v", err, errPerpetualsUnsupported)
}
c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(3)
c.CurrencySettings[0].Quote = currency.NewCode("USD")
err = c.validateCurrencySettings()
if !errors.Is(err, errFeatureIncompatible) {
t.Errorf("received: %v, expected: %v", err, errFeatureIncompatible)
}
c.CurrencySettings[0].Asset = asset.Spot
c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(-1)
err = c.validateCurrencySettings()
if !errors.Is(err, errBadSlippageRates) {
t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
}
c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(-1)
err = c.validateCurrencySettings()
if !errors.Is(err, errBadSlippageRates) {
t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
}
c.CurrencySettings[0].MinimumSlippagePercent = decimal.NewFromInt(2)
c.CurrencySettings[0].MaximumSlippagePercent = decimal.NewFromInt(1)
err = c.validateCurrencySettings()
if !errors.Is(err, errBadSlippageRates) {
t.Errorf("received: %v, expected: %v", err, errBadSlippageRates)
}
c.CurrencySettings[0].SpotDetails = &SpotDetails{}
err = c.validateCurrencySettings()
if !errors.Is(err, errBadInitialFunds) {
t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
}
z := decimal.Zero
c.CurrencySettings[0].SpotDetails.InitialQuoteFunds = &z
c.CurrencySettings[0].SpotDetails.InitialBaseFunds = &z
err = c.validateCurrencySettings()
if !errors.Is(err, errBadInitialFunds) {
t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
}
c.CurrencySettings[0].SpotDetails.InitialQuoteFunds = &leet
c.FundingSettings.UseExchangeLevelFunding = true
err = c.validateCurrencySettings()
if !errors.Is(err, errBadInitialFunds) {
t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
}
c.CurrencySettings[0].SpotDetails.InitialQuoteFunds = &z
c.CurrencySettings[0].SpotDetails.InitialBaseFunds = &leet
c.FundingSettings.UseExchangeLevelFunding = true
err = c.validateCurrencySettings()
if !errors.Is(err, errBadInitialFunds) {
t.Errorf("received: %v, expected: %v", err, errBadInitialFunds)
}
}
func TestValidateMinMaxes(t *testing.T) {
t.Parallel()
c := &Config{}
err := c.validateMinMaxes()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
c.CurrencySettings = []CurrencySettings{
{
SellSide: MinMax{
@@ -282,10 +370,10 @@ func TestPrintSettings(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds1000000,
InitialBaseFunds: initialFunds1000000,
@@ -308,27 +396,15 @@ func TestPrintSettings(t *testing.T) {
CSVData: &CSVData{
FullPath: "fake",
},
LiveData: &LiveData{
APIKeyOverride: "",
APISecretOverride: "",
APIClientIDOverride: "",
API2FAOverride: "",
APISubAccountOverride: "",
RealOrders: false,
},
LiveData: &LiveData{},
DatabaseData: &DatabaseData{
StartDate: startDate,
EndDate: endDate,
Config: database.Config{},
InclusiveEndDate: false,
StartDate: startDate,
EndDate: endDate,
},
},
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -348,10 +424,10 @@ func TestValidate(t *testing.T) {
StrategySettings: StrategySettings{Name: dca},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialBaseFunds: initialFunds10,
InitialQuoteFunds: initialFunds100000,
@@ -371,8 +447,8 @@ func TestValidate(t *testing.T) {
c = nil
err = c.Validate()
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received %v expected %v", err, gctcommon.ErrNilPointer)
}
}
@@ -383,16 +459,16 @@ func TestReadStrategyConfigFromFile(t *testing.T) {
t.Fatalf("Problem creating temp file at %v: %s\n", passFile, err)
}
_, err = passFile.WriteString("{}")
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = passFile.Close()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = ReadStrategyConfigFromFile(passFile.Name())
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = ReadStrategyConfigFromFile("test")
@@ -413,10 +489,10 @@ func TestGenerateConfigForDCAAPICandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -438,9 +514,6 @@ func TestGenerateConfigForDCAAPICandles(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -455,7 +528,7 @@ func TestGenerateConfigForDCAAPICandles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-api-candles.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-api-candles.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -474,10 +547,10 @@ func TestGenerateConfigForPluginStrategy(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds1000000,
},
@@ -516,7 +589,7 @@ func TestGenerateConfigForPluginStrategy(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "custom-plugin-strategy.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "custom-plugin-strategy.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -539,29 +612,29 @@ func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) {
UseExchangeLevelFunding: true,
ExchangeLevelFunding: []ExchangeLevelFunding{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Currency: currency.USDT,
Currency: mainCurrencyPair.Quote,
InitialFunds: decimal.NewFromInt(100000),
},
},
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.ETH,
Quote: currency.USDT,
Quote: mainCurrencyPair.Quote,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
@@ -580,9 +653,6 @@ func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -597,7 +667,7 @@ func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-api-candles-exchange-level-funding.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-api-candles-exchange-level-funding.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -616,10 +686,10 @@ func TestGenerateConfigForDCAAPITrades(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: "ftx",
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -650,9 +720,6 @@ func TestGenerateConfigForDCAAPITrades(t *testing.T) {
MaximumSize: decimal.NewFromInt(1),
MaximumTotal: decimal.NewFromInt(10000),
},
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -667,7 +734,7 @@ func TestGenerateConfigForDCAAPITrades(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-api-trades.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-api-trades.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -686,10 +753,10 @@ func TestGenerateConfigForDCAAPICandlesMultipleCurrencies(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -699,10 +766,10 @@ func TestGenerateConfigForDCAAPICandlesMultipleCurrencies(t *testing.T) {
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.ETH,
Quote: currency.USDT,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -724,9 +791,6 @@ func TestGenerateConfigForDCAAPICandlesMultipleCurrencies(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -741,7 +805,7 @@ func TestGenerateConfigForDCAAPICandlesMultipleCurrencies(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-api-candles-multiple-currencies.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-api-candles-multiple-currencies.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -761,10 +825,10 @@ func TestGenerateConfigForDCAAPICandlesSimultaneousProcessing(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds1000000,
},
@@ -774,10 +838,10 @@ func TestGenerateConfigForDCAAPICandlesSimultaneousProcessing(t *testing.T) {
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.ETH,
Quote: currency.USDT,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -799,9 +863,6 @@ func TestGenerateConfigForDCAAPICandlesSimultaneousProcessing(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -816,7 +877,7 @@ func TestGenerateConfigForDCAAPICandlesSimultaneousProcessing(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-api-candles-simultaneous-processing.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-api-candles-simultaneous-processing.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -836,15 +897,15 @@ func TestGenerateConfigForDCALiveCandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
BuySide: minMax,
SellSide: minMax,
BuySide: strictMinMax,
SellSide: strictMinMax,
MakerFee: &makerFee,
TakerFee: &takerFee,
},
@@ -853,20 +914,25 @@ func TestGenerateConfigForDCALiveCandles(t *testing.T) {
Interval: kline.OneMin,
DataType: common.CandleStr,
LiveData: &LiveData{
APIKeyOverride: "",
APISecretOverride: "",
APIClientIDOverride: "",
API2FAOverride: "",
APISubAccountOverride: "",
RealOrders: false,
NewEventTimeout: time.Minute * 2,
DataCheckTimer: time.Second,
RealOrders: false,
DataRequestRetryTolerance: 3,
DataRequestRetryWaitTime: time.Millisecond * 500,
ExchangeCredentials: []Credentials{
{
Exchange: mainExchange,
Keys: account.Credentials{
Key: "",
Secret: "",
},
},
},
},
},
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
BuySide: strictMinMax,
SellSide: strictMinMax,
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -881,7 +947,7 @@ func TestGenerateConfigForDCALiveCandles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-candles-live.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-candles-live.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -905,10 +971,10 @@ func TestGenerateConfigForRSIAPICustomSettings(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -930,9 +996,6 @@ func TestGenerateConfigForRSIAPICustomSettings(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -947,7 +1010,7 @@ func TestGenerateConfigForRSIAPICustomSettings(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "rsi-api-candles.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "rsi-api-candles.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -968,10 +1031,10 @@ func TestGenerateConfigForDCACSVCandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -991,9 +1054,6 @@ func TestGenerateConfigForDCACSVCandles(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -1008,7 +1068,7 @@ func TestGenerateConfigForDCACSVCandles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-csv-candles.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-csv-candles.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -1029,10 +1089,10 @@ func TestGenerateConfigForDCACSVTrades(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -1047,11 +1107,7 @@ func TestGenerateConfigForDCACSVTrades(t *testing.T) {
FullPath: fp,
},
},
PortfolioSettings: PortfolioSettings{
Leverage: Leverage{
CanUseLeverage: false,
},
},
PortfolioSettings: PortfolioSettings{},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
},
@@ -1065,7 +1121,7 @@ func TestGenerateConfigForDCACSVTrades(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-csv-trades.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-csv-trades.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -1084,10 +1140,10 @@ func TestGenerateConfigForDCADatabaseCandles(t *testing.T) {
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
SpotDetails: &SpotDetails{
InitialQuoteFunds: initialFunds100000,
},
@@ -1118,9 +1174,6 @@ func TestGenerateConfigForDCADatabaseCandles(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{
CanUseLeverage: false,
},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -1135,7 +1188,7 @@ func TestGenerateConfigForDCADatabaseCandles(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "dca-database-candles.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "dca-database-candles.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
@@ -1163,75 +1216,75 @@ func TestGenerateConfigForTop2Bottom2(t *testing.T) {
UseExchangeLevelFunding: true,
ExchangeLevelFunding: []ExchangeLevelFunding{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Currency: currency.BTC,
Currency: mainCurrencyPair.Base,
InitialFunds: decimal.NewFromFloat(3),
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Currency: currency.USDT,
Currency: mainCurrencyPair.Quote,
InitialFunds: decimal.NewFromInt(10000),
},
},
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USDT,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.DOGE,
Quote: currency.USDT,
Quote: mainCurrencyPair.Quote,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.ETH,
Quote: currency.BTC,
Quote: mainCurrencyPair.Base,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.LTC,
Quote: currency.BTC,
Quote: mainCurrencyPair.Base,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.XRP,
Quote: currency.USDT,
Quote: mainCurrencyPair.Quote,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
TakerFee: &takerFee,
},
{
ExchangeName: testExchange,
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BNB,
Quote: currency.BTC,
Quote: mainCurrencyPair.Base,
BuySide: minMax,
SellSide: minMax,
MakerFee: &makerFee,
@@ -1249,7 +1302,6 @@ func TestGenerateConfigForTop2Bottom2(t *testing.T) {
PortfolioSettings: PortfolioSettings{
BuySide: minMax,
SellSide: minMax,
Leverage: Leverage{},
},
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
@@ -1264,14 +1316,14 @@ func TestGenerateConfigForTop2Bottom2(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "t2b2-api-candles-exchange-funding.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "t2b2-api-candles-exchange-funding.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
}
}
func TestGenerateFTXCashAndCarryStrategy(t *testing.T) {
func TestGenerateBinanceCashAndCarryStrategy(t *testing.T) {
if !saveConfig {
t.Skip()
}
@@ -1279,36 +1331,40 @@ func TestGenerateFTXCashAndCarryStrategy(t *testing.T) {
Nickname: "ExampleCashAndCarry",
Goal: "To demonstrate a cash and carry strategy",
StrategySettings: StrategySettings{
Name: "ftx-cash-carry",
Name: "binance-cash-carry",
SimultaneousSignalProcessing: true,
},
FundingSettings: FundingSettings{
UseExchangeLevelFunding: true,
ExchangeLevelFunding: []ExchangeLevelFunding{
{
ExchangeName: "ftx",
ExchangeName: mainExchange,
Asset: asset.Spot,
Currency: currency.USD,
Currency: mainCurrencyPair.Quote,
InitialFunds: *initialFunds100000,
},
},
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: "ftx",
Asset: asset.Futures,
Base: currency.BTC,
Quote: currency.NewCode("20210924"),
ExchangeName: mainExchange,
Asset: asset.USDTMarginedFutures,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
MakerFee: &makerFee,
TakerFee: &takerFee,
BuySide: minMax,
SellSide: minMax,
},
{
ExchangeName: "ftx",
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: currency.BTC,
Quote: currency.USD,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
MakerFee: &makerFee,
TakerFee: &takerFee,
BuySide: minMax,
SellSide: minMax,
},
},
DataSettings: DataSettings{
@@ -1320,9 +1376,92 @@ func TestGenerateFTXCashAndCarryStrategy(t *testing.T) {
InclusiveEndDate: false,
},
},
PortfolioSettings: PortfolioSettings{
Leverage: Leverage{
CanUseLeverage: true,
StatisticSettings: StatisticSettings{
RiskFreeRate: decimal.NewFromFloat(0.03),
},
}
if saveConfig {
result, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
t.Fatal(err)
}
p, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "strategyexamples", "binance-cash-and-carry.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}
}
}
func TestGenerateConfigForLiveCashAndCarry(t *testing.T) {
if !saveConfig {
t.Skip()
}
cfg := Config{
Nickname: "ExampleBinanceLiveCashAndCarry",
Goal: "To demonstrate a cash and carry strategy using a live data source",
StrategySettings: StrategySettings{
Name: "binance-cash-carry",
SimultaneousSignalProcessing: true,
},
FundingSettings: FundingSettings{
UseExchangeLevelFunding: true,
ExchangeLevelFunding: []ExchangeLevelFunding{
{
ExchangeName: mainExchange,
Asset: asset.Spot,
Currency: mainCurrencyPair.Quote,
InitialFunds: *initialFunds100000,
},
},
},
CurrencySettings: []CurrencySettings{
{
ExchangeName: mainExchange,
Asset: asset.USDTMarginedFutures,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
MakerFee: &makerFee,
TakerFee: &takerFee,
SkipCandleVolumeFitting: true,
BuySide: strictMinMax,
SellSide: strictMinMax,
},
{
ExchangeName: mainExchange,
Asset: asset.Spot,
Base: mainCurrencyPair.Base,
Quote: mainCurrencyPair.Quote,
MakerFee: &makerFee,
TakerFee: &takerFee,
SkipCandleVolumeFitting: true,
BuySide: strictMinMax,
SellSide: strictMinMax,
},
},
DataSettings: DataSettings{
Interval: kline.FifteenSecond,
DataType: common.CandleStr,
LiveData: &LiveData{
NewEventTimeout: time.Minute,
DataCheckTimer: time.Second,
RealOrders: false,
DataRequestRetryTolerance: 3,
ClosePositionsOnStop: true,
DataRequestRetryWaitTime: time.Millisecond * 500,
ExchangeCredentials: []Credentials{
{
Exchange: mainExchange,
Keys: account.Credentials{
Key: "",
Secret: "",
SubAccount: "",
},
},
},
},
},
StatisticSettings: StatisticSettings{
@@ -1338,7 +1477,7 @@ func TestGenerateFTXCashAndCarryStrategy(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(p, "examples", "ftx-cash-carry.strat"), result, file.DefaultPermissionOctal)
err = os.WriteFile(filepath.Join(p, "strategyexamples", "binance-live-cash-and-carry.strat"), result, file.DefaultPermissionOctal)
if err != nil {
t.Error(err)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
@@ -42,12 +43,13 @@ type Config struct {
// DataSettings is a container for each type of data retrieval setting.
// Only ONE can be populated per config
type DataSettings struct {
Interval kline.Interval `json:"interval"`
DataType string `json:"data-type"`
APIData *APIData `json:"api-data,omitempty"`
DatabaseData *DatabaseData `json:"database-data,omitempty"`
LiveData *LiveData `json:"live-data,omitempty"`
CSVData *CSVData `json:"csv-data,omitempty"`
Interval kline.Interval `json:"interval"`
DataType string `json:"data-type"`
VerboseExchangeRequests bool `json:"verbose-exchange-requests"`
APIData *APIData `json:"api-data,omitempty"`
DatabaseData *DatabaseData `json:"database-data,omitempty"`
LiveData *LiveData `json:"live-data,omitempty"`
CSVData *CSVData `json:"csv-data,omitempty"`
}
// FundingSettings contains funding details for individual currencies
@@ -188,10 +190,17 @@ type DatabaseData struct {
// LiveData defines all fields to configure live data
type LiveData struct {
APIKeyOverride string `json:"api-key-override"`
APISecretOverride string `json:"api-secret-override"`
APIClientIDOverride string `json:"api-client-id-override"`
API2FAOverride string `json:"api-2fa-override"`
APISubAccountOverride string `json:"api-sub-account-override"`
RealOrders bool `json:"real-orders"`
NewEventTimeout time.Duration `json:"new-event-timeout"`
DataCheckTimer time.Duration `json:"data-check-timer"`
RealOrders bool `json:"real-orders"`
ClosePositionsOnStop bool `json:"close-positions-on-stop"`
DataRequestRetryTolerance int64 `json:"data-request-retry-tolerance"`
DataRequestRetryWaitTime time.Duration `json:"data-request-retry-wait-time"`
ExchangeCredentials []Credentials `json:"exchange-credentials"`
}
// Credentials holds each exchanges credentials
type Credentials struct {
Exchange string `json:"exchange"`
Keys account.Credentials `json:"credentials"`
}

View File

@@ -236,7 +236,7 @@ func parseExchangeSettings(reader *bufio.Reader, cfg *config.Config) error {
func parseStrategySettings(cfg *config.Config, reader *bufio.Reader) error {
fmt.Println("Firstly, please select which strategy you wish to use")
strats := strategies.GetStrategies()
strats := strategies.GetSupportedStrategies()
strategiesToUse := make([]string, len(strats))
for i := range strats {
fmt.Printf("%v. %s\n", i+1, strats[i].Name())
@@ -460,19 +460,32 @@ func parseLive(reader *bufio.Reader, cfg *config.Config) {
input := quickParse(reader)
cfg.DataSettings.LiveData.RealOrders = input == y || input == yes
if cfg.DataSettings.LiveData.RealOrders {
fmt.Printf("Do you want to override GoCryptoTrader's API credentials for %s? y/n\n", cfg.CurrencySettings[0].ExchangeName)
fmt.Printf("Do you want to set credentials for exchanges? y/n\n")
input = quickParse(reader)
if input == y || input == yes {
if input != yes && input != y {
return
}
for {
var creds config.Credentials
fmt.Printf("What is the exchange name? y/n\n")
creds.Exchange = quickParse(reader)
fmt.Println("What is the API key?")
cfg.DataSettings.LiveData.APIKeyOverride = quickParse(reader)
creds.Keys.Key = quickParse(reader)
fmt.Println("What is the API secret?")
cfg.DataSettings.LiveData.APISecretOverride = quickParse(reader)
fmt.Println("What is the Client ID?")
cfg.DataSettings.LiveData.APIClientIDOverride = quickParse(reader)
fmt.Println("What is the 2FA seed?")
cfg.DataSettings.LiveData.API2FAOverride = quickParse(reader)
fmt.Println("What is the subaccount to use?")
cfg.DataSettings.LiveData.APISubAccountOverride = quickParse(reader)
creds.Keys.Secret = quickParse(reader)
fmt.Println("What is the Client ID? (leave blank if not applicable)")
creds.Keys.ClientID = quickParse(reader)
fmt.Println("What is the 2FA seed? (leave blank if not applicable)")
creds.Keys.OneTimePassword = quickParse(reader)
fmt.Println("What is the subaccount to use? (leave blank if not applicable)")
creds.Keys.SubAccount = quickParse(reader)
fmt.Println("What is the PEM key? (leave blank if not applicable)")
creds.Keys.PEMKey = quickParse(reader)
cfg.DataSettings.LiveData.ExchangeCredentials = append(cfg.DataSettings.LiveData.ExchangeCredentials, creds)
fmt.Printf("Do you want to add another? y/n\n")
if input != yes && input != y {
break
}
}
}
}
@@ -556,10 +569,7 @@ func customSettingsLoop(reader *bufio.Reader) map[string]interface{} {
}
func addCurrencySetting(reader *bufio.Reader, usingExchangeLevelFunding bool) (*config.CurrencySettings, error) {
setting := config.CurrencySettings{
BuySide: config.MinMax{},
SellSide: config.MinMax{},
}
setting := config.CurrencySettings{}
fmt.Println("Enter the exchange name. eg Binance")
setting.ExchangeName = quickParse(reader)

View File

@@ -1,16 +1,16 @@
# GoCryptoTrader Backtester: Examples package
# GoCryptoTrader Backtester: Strategyexamples package
<img src="/backtester/common/backtester.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/config/examples)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/config/strategyexamples)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This examples package is part of the GoCryptoTrader codebase.
This strategyexamples package is part of the GoCryptoTrader codebase.
## This is still in active development
@@ -18,7 +18,7 @@ You can track ideas, planned features and what's in progress on this Trello boar
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Examples package overview
## Strategyexamples package overview
### Current Config Examples
@@ -34,7 +34,8 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| dca-database-candles.strat | The same DCA strategy, but uses a database to retrieve candle data |
| rsi-api-candles.strat | Runs a strategy using rsi figures to make buy or sell orders based on market figures |
| t2b2-api-candles-exchange-funding.strat | Runs a more complex strategy using simultaneous signal processing, exchange level funding and MFI values to make buy or sell signals based on the two strongest and weakest MFI values |
| ftx-cash-carry.strat | Executes a cash and carry trade on FTX, buying BTC-USD while shorting the long dated futures contract BTC-20210924 |
| binance-cash-and-carry.strat | Executes a cash and carry trade on Binance, buying BTC-USD while shorting the long dated futures contract. Is not currently implemented |
| binance-live-cash-and-carry.strat | Executes a cash and carry trade on Binance using realtime 15 second candles, buying BTC-USD while shorting the long dated futures contract. Is not currently implemented |
### Want to make your own configs?
Use the provided config builder under `/backtester/config/configbuilder` or modify tests under `/backtester/config/config_test.go` to generates strategy files quickly

View File

@@ -2,7 +2,7 @@
"nickname": "ExampleCashAndCarry",
"goal": "To demonstrate a cash and carry strategy",
"strategy-settings": {
"name": "ftx-cash-carry",
"name": "binance-cash-carry",
"use-simultaneous-signal-processing": true,
"disable-usd-tracking": false
},
@@ -10,9 +10,9 @@
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"currency": "USD",
"currency": "USDT",
"initial-funds": "100000",
"transfer-fee": "0"
}
@@ -20,19 +20,19 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"asset": "futures",
"exchange-name": "binance",
"asset": "usdtmarginedfutures",
"base": "BTC",
"quote": "20210924",
"quote": "USDT",
"buy-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
},
"sell-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
@@ -44,19 +44,19 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USD",
"quote": "USDT",
"buy-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
},
"sell-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
@@ -71,6 +71,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-01-14T00:00:00Z",
"end-date": "2021-09-24T00:00:00Z",
@@ -79,7 +80,7 @@
},
"portfolio-settings": {
"leverage": {
"can-use-leverage": true,
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"

View File

@@ -0,0 +1,118 @@
{
"nickname": "ExampleBinanceLiveCashAndCarry",
"goal": "To demonstrate a cash and carry strategy using a live data source",
"strategy-settings": {
"name": "binance-cash-carry",
"use-simultaneous-signal-processing": true,
"disable-usd-tracking": false
},
"funding-settings": {
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "binance",
"asset": "spot",
"currency": "USDT",
"initial-funds": "100000",
"transfer-fee": "0"
}
]
},
"currency-settings": [
{
"exchange-name": "binance",
"asset": "usdtmarginedfutures",
"base": "BTC",
"quote": "USDT",
"buy-side": {
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
},
"sell-side": {
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": true,
"use-exchange-order-limits": false,
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
"buy-side": {
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
},
"sell-side": {
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
"maker-fee-override": "0.0002",
"taker-fee-override": "0.0007",
"maximum-holdings-ratio": "0",
"skip-candle-volume-fitting": true,
"use-exchange-order-limits": false,
"use-exchange-pnl-calculation": false
}
],
"data-settings": {
"interval": 15000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"live-data": {
"new-event-timeout": 60000000000,
"data-check-timer": 1000000000,
"real-orders": false,
"close-positions-on-stop": true,
"data-request-retry-tolerance": 3,
"data-request-retry-wait-time": 500000000,
"exchange-credentials": [
{
"exchange": "binance",
"credentials": {
"Key": "",
"Secret": "",
"ClientID": "",
"PEMKey": "",
"SubAccount": "",
"OneTimePassword": ""
}
}
]
}
},
"portfolio-settings": {
"leverage": {
"can-use-leverage": false,
"maximum-orders-with-leverage-ratio": "0",
"maximum-leverage-rate": "0",
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
},
"sell-side": {
"minimum-size": "0",
"maximum-size": "0",
"maximum-total": "0"
}
},
"statistic-settings": {
"risk-free-rate": "0.03"
}
}

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -41,6 +41,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -10,7 +10,7 @@
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"currency": "USDT",
"initial-funds": "100000",
@@ -20,7 +20,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -44,7 +44,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "ETH",
"quote": "USDT",
@@ -71,6 +71,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -38,7 +38,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "ETH",
"quote": "USDT",
@@ -68,6 +68,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -38,7 +38,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "ETH",
"quote": "USDT",
@@ -68,6 +68,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -41,6 +41,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -41,6 +41,7 @@
"data-settings": {
"interval": 3600000000000,
"data-type": "trade",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-08-04T00:00:00+10:00",

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -19,14 +19,14 @@
"initial-quote-funds": "100000"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
},
"sell-side": {
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
},
"min-slippage-percent": "0",
"max-slippage-percent": "0",
@@ -41,13 +41,27 @@
"data-settings": {
"interval": 60000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"live-data": {
"api-key-override": "",
"api-secret-override": "",
"api-client-id-override": "",
"api-2fa-override": "",
"api-sub-account-override": "",
"real-orders": false
"new-event-timeout": 120000000000,
"data-check-timer": 1000000000,
"real-orders": false,
"close-positions-on-stop": false,
"data-request-retry-tolerance": 3,
"data-request-retry-wait-time": 500000000,
"exchange-credentials": [
{
"exchange": "binance",
"credentials": {
"Key": "",
"Secret": "",
"ClientID": "",
"PEMKey": "",
"SubAccount": "",
"OneTimePassword": ""
}
}
]
}
},
"portfolio-settings": {
@@ -58,14 +72,14 @@
"maximum-collateral-leverage-rate": "0"
},
"buy-side": {
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
},
"sell-side": {
"minimum-size": "0.005",
"maximum-size": "2",
"maximum-total": "40000"
"minimum-size": "0.001",
"maximum-size": "0.05",
"maximum-total": "100"
}
},
"statistic-settings": {

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -41,6 +41,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"csv-data": {
"full-path": "..\\testdata\\binance_BTCUSDT_24h_2019_01_01_2020_01_01.csv"
}

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -41,6 +41,7 @@
"data-settings": {
"interval": 60000000000,
"data-type": "trade",
"verbose-exchange-requests": false,
"csv-data": {
"full-path": "..\\testdata\\binance_BTCUSDT_24h-trades_2020_11_16.csv"
}

View File

@@ -11,7 +11,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -41,6 +41,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"database-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -16,7 +16,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -46,6 +46,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-05-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -15,14 +15,14 @@
"use-exchange-level-funding": true,
"exchange-level-funding": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"currency": "BTC",
"initial-funds": "3",
"transfer-fee": "0"
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"currency": "USDT",
"initial-funds": "10000",
@@ -32,7 +32,7 @@
},
"currency-settings": [
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BTC",
"quote": "USDT",
@@ -56,7 +56,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "DOGE",
"quote": "USDT",
@@ -80,7 +80,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "ETH",
"quote": "BTC",
@@ -104,7 +104,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "LTC",
"quote": "BTC",
@@ -128,7 +128,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "XRP",
"quote": "USDT",
@@ -152,7 +152,7 @@
"use-exchange-pnl-calculation": false
},
{
"exchange-name": "ftx",
"exchange-name": "binance",
"asset": "spot",
"base": "BNB",
"quote": "BTC",
@@ -179,6 +179,7 @@
"data-settings": {
"interval": 86400000000000,
"data-type": "candle",
"verbose-exchange-requests": false,
"api-data": {
"start-date": "2021-08-01T00:00:00+10:00",
"end-date": "2021-12-01T00:00:00+11:00",

View File

@@ -6,129 +6,338 @@ import (
"strings"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// Setup creates a basic map
func (h *HandlerPerCurrency) Setup() {
if h.data == nil {
h.data = make(map[string]map[asset.Item]map[currency.Pair]Handler)
// NewHandlerHolder returns a new HandlerHolder
func NewHandlerHolder() *HandlerHolder {
return &HandlerHolder{
data: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler),
}
}
// SetDataForCurrency assigns a data Handler to the data map by exchange, asset and currency
func (h *HandlerPerCurrency) SetDataForCurrency(e string, a asset.Item, p currency.Pair, k Handler) {
// SetDataForCurrency assigns a Data Handler to the Data map by exchange, asset and currency
func (h *HandlerHolder) SetDataForCurrency(e string, a asset.Item, p currency.Pair, k Handler) error {
if h == nil {
return fmt.Errorf("%w handler holder", gctcommon.ErrNilPointer)
}
h.m.Lock()
defer h.m.Unlock()
if h.data == nil {
h.Setup()
h.data = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler)
}
e = strings.ToLower(e)
if h.data[e] == nil {
h.data[e] = make(map[asset.Item]map[currency.Pair]Handler)
m1, ok := h.data[e]
if !ok {
m1 = make(map[asset.Item]map[*currency.Item]map[*currency.Item]Handler)
h.data[e] = m1
}
if h.data[e][a] == nil {
h.data[e][a] = make(map[currency.Pair]Handler)
m2, ok := m1[a]
if !ok {
m2 = make(map[*currency.Item]map[*currency.Item]Handler)
m1[a] = m2
}
h.data[e][a][p] = k
m3, ok := m2[p.Base.Item]
if !ok {
m3 = make(map[*currency.Item]Handler)
m2[p.Base.Item] = m3
}
m3[p.Quote.Item] = k
return nil
}
// GetAllData returns all set data in the data map
func (h *HandlerPerCurrency) GetAllData() map[string]map[asset.Item]map[currency.Pair]Handler {
return h.data
// GetAllData returns all set Data in the Data map
func (h *HandlerHolder) GetAllData() ([]Handler, error) {
if h == nil {
return nil, fmt.Errorf("%w handler holder", gctcommon.ErrNilPointer)
}
h.m.Lock()
defer h.m.Unlock()
var resp []Handler
for _, exchMap := range h.data {
for _, assetMap := range exchMap {
for _, baseMap := range assetMap {
for _, handler := range baseMap {
resp = append(resp, handler)
}
}
}
}
return resp, nil
}
// GetDataForCurrency returns the Handler for a specific exchange, asset, currency
func (h *HandlerPerCurrency) GetDataForCurrency(ev common.EventHandler) (Handler, error) {
func (h *HandlerHolder) GetDataForCurrency(ev common.Event) (Handler, error) {
if h == nil {
return nil, fmt.Errorf("%w handler holder", gctcommon.ErrNilPointer)
}
if ev == nil {
return nil, common.ErrNilEvent
}
handler, ok := h.data[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
h.m.Lock()
defer h.m.Unlock()
exch := ev.GetExchange()
a := ev.GetAssetType()
p := ev.Pair()
handler, ok := h.data[exch][a][p.Base.Item][p.Quote.Item]
if !ok {
return nil, fmt.Errorf("%s %s %s %w", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ErrHandlerNotFound)
return nil, fmt.Errorf("%s %s %s %w", exch, a, p, ErrHandlerNotFound)
}
return handler, nil
}
// Reset returns the struct to defaults
func (h *HandlerPerCurrency) Reset() {
h.data = nil
func (h *HandlerHolder) Reset() error {
if h == nil {
return gctcommon.ErrNilPointer
}
h.m.Lock()
defer h.m.Unlock()
h.data = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler)
return nil
}
// Reset loaded data to blank state
func (b *Base) Reset() {
// GetDetails returns data about the Base Holder
func (b *Base) GetDetails() (string, asset.Item, currency.Pair, error) {
if b == nil {
return "", asset.Empty, currency.EMPTYPAIR, fmt.Errorf("%w base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
return b.latest.GetExchange(), b.latest.GetAssetType(), b.latest.Pair(), nil
}
// Reset loaded Data to blank state
func (b *Base) Reset() error {
if b == nil {
return gctcommon.ErrNilPointer
}
b.m.Lock()
defer b.m.Unlock()
b.stream = nil
b.latest = nil
b.offset = 0
b.stream = nil
b.isLiveData = false
return nil
}
// GetStream will return entire data list
func (b *Base) GetStream() []common.DataEventHandler {
return b.stream
// GetStream will return entire Data list
func (b *Base) GetStream() (Events, error) {
if b == nil {
return nil, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
stream := make([]Event, len(b.stream))
copy(stream, b.stream)
return stream, nil
}
// Offset returns the current iteration of candle data the backtester is assessing
func (b *Base) Offset() int {
return b.offset
// Offset returns the current iteration of candle Data the backtester is assessing
func (b *Base) Offset() (int64, error) {
if b == nil {
return 0, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
return b.offset, nil
}
// SetStream sets the data stream for candle analysis
func (b *Base) SetStream(s []common.DataEventHandler) {
// SetStream sets the Data stream for candle analysis
func (b *Base) SetStream(s []Event) error {
if b == nil {
return fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
sort.Slice(s, func(i, j int) bool {
return s[i].GetTime().Before(s[j].GetTime())
})
for x := range s {
if s[x] == nil {
return fmt.Errorf("%w Event", gctcommon.ErrNilPointer)
}
if s[x].GetExchange() == "" || !s[x].GetAssetType().IsValid() || s[x].Pair().IsEmpty() || s[x].GetTime().IsZero() {
return ErrInvalidEventSupplied
}
if len(b.stream) > 0 {
if s[x].GetExchange() != b.stream[0].GetExchange() ||
s[x].GetAssetType() != b.stream[0].GetAssetType() ||
!s[x].Pair().Equal(b.stream[0].Pair()) {
return fmt.Errorf("%w cannot set base stream from %v %v %v to %v %v %v", errMisMatchedEvent, s[x].GetExchange(), s[x].GetAssetType(), s[x].Pair(), b.stream[0].GetExchange(), b.stream[0].GetAssetType(), b.stream[0].Pair())
}
}
// due to the Next() function, we cannot take
// stream offsets as is, and we re-set them
s[x].SetOffset(int64(x) + 1)
}
b.stream = s
return nil
}
// AppendStream appends new datas onto the stream, however, will not
// add duplicates. Used for live analysis
func (b *Base) AppendStream(s ...common.DataEventHandler) {
for i := range s {
if s[i] == nil {
continue
}
b.stream = append(b.stream, s[i])
func (b *Base) AppendStream(s ...Event) error {
if b == nil {
return fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
if len(s) == 0 {
return errNothingToAdd
}
b.m.Lock()
defer b.m.Unlock()
candles:
for x := range s {
if s[x] == nil {
return fmt.Errorf("%w Event", gctcommon.ErrNilPointer)
}
if s[x].GetExchange() == "" || !s[x].GetAssetType().IsValid() || s[x].Pair().IsEmpty() || s[x].GetTime().IsZero() {
return ErrInvalidEventSupplied
}
if len(b.stream) > 0 {
if s[x].GetExchange() != b.stream[0].GetExchange() ||
s[x].GetAssetType() != b.stream[0].GetAssetType() ||
!s[x].Pair().Equal(b.stream[0].Pair()) {
return fmt.Errorf("%w %v %v %v received %v %v %v", errMisMatchedEvent, b.stream[0].GetExchange(), b.stream[0].GetAssetType(), b.stream[0].Pair(), s[x].GetExchange(), s[x].GetAssetType(), s[x].Pair())
}
// todo change b.stream to map
for y := len(b.stream) - 1; y >= 0; y-- {
if s[x].GetTime().Equal(b.stream[y].GetTime()) {
continue candles
}
}
}
b.stream = append(b.stream, s[x])
}
sort.Slice(b.stream, func(i, j int) bool {
return b.stream[i].GetTime().Before(b.stream[j].GetTime())
})
for i := range b.stream {
b.stream[i].SetOffset(int64(i) + 1)
}
return nil
}
// Next will return the next event in the list and also shift the offset one
func (b *Base) Next() (dh common.DataEventHandler) {
if len(b.stream) <= b.offset {
return nil
func (b *Base) Next() (Event, error) {
if b == nil {
return nil, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
if int64(len(b.stream)) <= b.offset {
return nil, fmt.Errorf("%w data length %v offset %v", ErrEndOfData, len(b.stream), b.offset)
}
ret := b.stream[b.offset]
b.offset++
b.latest = ret
return ret
return ret, nil
}
// History will return all previous data events that have happened
func (b *Base) History() []common.DataEventHandler {
return b.stream[:b.offset]
// History will return all previous Data events that have happened
func (b *Base) History() (Events, error) {
if b == nil {
return nil, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
stream := make([]Event, len(b.stream[:b.offset]))
copy(stream, b.stream[:b.offset])
return stream, nil
}
// Latest will return latest data event
func (b *Base) Latest() common.DataEventHandler {
if b.latest == nil && len(b.stream) >= b.offset+1 {
// Latest will return latest Data event
func (b *Base) Latest() (Event, error) {
if b == nil {
return nil, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
if b.latest == nil && int64(len(b.stream)) >= b.offset+1 {
b.latest = b.stream[b.offset]
}
return b.latest
return b.latest, nil
}
// List returns all future data events from the current iteration
// List returns all future Data events from the current iteration
// ill-advised to use this in strategies because you don't know the future in real life
func (b *Base) List() []common.DataEventHandler {
return b.stream[b.offset:]
func (b *Base) List() (Events, error) {
if b == nil {
return nil, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
stream := make([]Event, len(b.stream[b.offset:]))
copy(stream, b.stream[b.offset:])
return stream, nil
}
// IsLastEvent determines whether the latest event is the last event
func (b *Base) IsLastEvent() bool {
return b.latest != nil && b.latest.GetOffset() == int64(len(b.stream))
// for live Data, this will be false, as all appended Data is the latest available Data
// and this signal cannot be completely relied upon
func (b *Base) IsLastEvent() (bool, error) {
if b == nil {
return false, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
return b.latest != nil && b.latest.GetOffset() == int64(len(b.stream)) && !b.isLiveData,
nil
}
// SortStream sorts the stream by timestamp
func (b *Base) SortStream() {
sort.Slice(b.stream, func(i, j int) bool {
b1 := b.stream[i]
b2 := b.stream[j]
// IsLive returns if the Data source is a live one
// less scrutiny on checks is required on live Data sourcing
func (b *Base) IsLive() (bool, error) {
if b == nil {
return false, fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
return b1.GetTime().Before(b2.GetTime())
})
return b.isLiveData, nil
}
// SetLive sets if the Data source is a live one
// less scrutiny on checks is required on live Data sourcing
func (b *Base) SetLive(isLive bool) error {
if b == nil {
return fmt.Errorf("%w Base", gctcommon.ErrNilPointer)
}
b.m.Lock()
defer b.m.Unlock()
b.isLiveData = isLive
return nil
}
// First returns the first element of a slice
func (e Events) First() (Event, error) {
if len(e) == 0 {
return nil, ErrEmptySlice
}
return e[0], nil
}
// Last returns the last element of a slice
func (e Events) Last() (Event, error) {
if len(e) == 0 {
return nil, ErrEmptySlice
}
return e[len(e)-1], nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package data
import (
"errors"
"sync"
"time"
"github.com/shopspring/decimal"
@@ -10,58 +11,87 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// ErrHandlerNotFound returned when a handler is not found for specified exchange, asset, pair
var ErrHandlerNotFound = errors.New("handler not found")
var (
// ErrHandlerNotFound returned when a handler is not found for specified exchange, asset, pair
ErrHandlerNotFound = errors.New("handler not found")
// ErrInvalidEventSupplied returned when a bad event is supplied
ErrInvalidEventSupplied = errors.New("invalid event supplied")
// ErrEmptySlice is returned when the supplied slice is nil or empty
ErrEmptySlice = errors.New("empty slice")
// ErrEndOfData is returned when attempting to load the next offset when there is no more
ErrEndOfData = errors.New("no more data to retrieve")
// HandlerPerCurrency stores an event handler per exchange asset pair
type HandlerPerCurrency struct {
data map[string]map[asset.Item]map[currency.Pair]Handler
errNothingToAdd = errors.New("cannot append empty event to stream")
errMisMatchedEvent = errors.New("cannot add event to stream, does not match")
)
// HandlerHolder stores an event handler per exchange asset pair
type HandlerHolder struct {
m sync.Mutex
data map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]Handler
}
// Holder interface dictates what a data holder is expected to do
// Holder interface dictates what a Data holder is expected to do
type Holder interface {
Setup()
SetDataForCurrency(string, asset.Item, currency.Pair, Handler)
GetAllData() map[string]map[asset.Item]map[currency.Pair]Handler
GetDataForCurrency(ev common.EventHandler) (Handler, error)
Reset()
SetDataForCurrency(string, asset.Item, currency.Pair, Handler) error
GetAllData() ([]Handler, error)
GetDataForCurrency(ev common.Event) (Handler, error)
Reset() error
}
// Base is the base implementation of some interface functions
// where further specific functions are implmented in DataFromKline
// where further specific functions are implemented in DataFromKline
type Base struct {
latest common.DataEventHandler
stream []common.DataEventHandler
offset int
m sync.Mutex
latest Event
stream []Event
offset int64
isLiveData bool
}
// Handler interface for Loading and Streaming data
// Handler interface for Loading and Streaming Data
type Handler interface {
Loader
Streamer
Reset()
GetDetails() (string, asset.Item, currency.Pair, error)
Reset() error
}
// Loader interface for Loading data into backtest supported format
// Loader interface for Loading Data into backtest supported format
type Loader interface {
Load() error
AppendStream(s ...Event) error
}
// Streamer interface handles loading, parsing, distributing BackTest data
// Streamer interface handles loading, parsing, distributing BackTest Data
type Streamer interface {
Next() common.DataEventHandler
GetStream() []common.DataEventHandler
History() []common.DataEventHandler
Latest() common.DataEventHandler
List() []common.DataEventHandler
IsLastEvent() bool
Offset() int
Next() (Event, error)
GetStream() (Events, error)
History() (Events, error)
Latest() (Event, error)
List() (Events, error)
IsLastEvent() (bool, error)
Offset() (int64, error)
StreamOpen() []decimal.Decimal
StreamHigh() []decimal.Decimal
StreamLow() []decimal.Decimal
StreamClose() []decimal.Decimal
StreamVol() []decimal.Decimal
StreamOpen() ([]decimal.Decimal, error)
StreamHigh() ([]decimal.Decimal, error)
StreamLow() ([]decimal.Decimal, error)
StreamClose() ([]decimal.Decimal, error)
StreamVol() ([]decimal.Decimal, error)
HasDataAtTime(time.Time) bool
HasDataAtTime(time.Time) (bool, error)
}
// Event interface used for loading and interacting with Data
type Event interface {
common.Event
GetUnderlyingPair() currency.Pair
GetClosePrice() decimal.Decimal
GetHighPrice() decimal.Decimal
GetLowPrice() decimal.Decimal
GetOpenPrice() decimal.Decimal
GetVolume() decimal.Decimal
}
// Events allows for some common functions on a slice of events
type Events []Event

View File

@@ -11,10 +11,10 @@ import (
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
gctkline "github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"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/kline"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
@@ -23,8 +23,8 @@ import (
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, isUSDTrackingPair bool) (*gctkline.DataFromKline, error) {
resp := &gctkline.DataFromKline{}
func LoadData(dataType int64, filepath, exchangeName string, interval time.Duration, fPair currency.Pair, a asset.Item, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
resp := kline.NewDataFromKline()
csvFile, err := os.Open(filepath)
if err != nil {
return nil, err
@@ -41,11 +41,11 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat
switch dataType {
case common.DataCandle:
candles := kline.Item{
candles := gctkline.Item{
Exchange: exchangeName,
Pair: fPair,
Asset: a,
Interval: kline.Interval(interval),
Interval: gctkline.Interval(interval),
}
for {
@@ -57,7 +57,7 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat
return nil, fmt.Errorf("could not read csv data for %v %v %v, %v", exchangeName, a, fPair, errCSV)
}
candle := kline.Candle{}
candle := gctkline.Candle{}
v, errParse := strconv.ParseInt(row[0], 10, 32)
if errParse != nil {
return nil, errParse
@@ -146,7 +146,7 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat
trades = append(trades, t)
}
resp.Item, err = trade.ConvertTradesToCandles(kline.Interval(interval), trades...)
resp.Item, err = trade.ConvertTradesToCandles(gctkline.Interval(interval), trades...)
if err != nil {
return nil, fmt.Errorf("could not read csv trade data for %v %v %v, %v", exchangeName, a, fPair, err)
}
@@ -159,7 +159,7 @@ func LoadData(dataType int64, filepath, exchangeName string, interval time.Durat
resp.Item.Exchange = strings.ToLower(exchangeName)
resp.Item.Pair = fPair
resp.Item.Asset = a
resp.Item.Interval = kline.Interval(interval)
resp.Item.Interval = gctkline.Interval(interval)
return resp, nil
}

View File

@@ -25,8 +25,8 @@ func TestLoadDataCandles(t *testing.T) {
p,
a,
false)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -42,8 +42,8 @@ func TestLoadDataTrades(t *testing.T) {
p,
a,
false)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}

View File

@@ -19,7 +19,7 @@ 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, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
resp := &kline.DataFromKline{}
resp := kline.NewDataFromKline()
switch dataType {
case common.DataCandle:
klineItem, err := getCandleDatabaseData(

View File

@@ -85,8 +85,8 @@ func TestLoadDataCandles(t *testing.T) {
database.MigrationDir = filepath.Join("..", "..", "..", "..", "database", "migrations")
testhelpers.MigrationDir = filepath.Join("..", "..", "..", "..", "database", "migrations")
conn, err := testhelpers.ConnectToDatabase(&dbConfg)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = exchangeDB.InsertMany([]exchangeDB.Details{{Name: testExchange}})
@@ -115,13 +115,13 @@ func TestLoadDataCandles(t *testing.T) {
},
}
_, err = gctkline.StoreInDatabase(data, true)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, common.DataCandle, p, a, false)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if err = conn.SQL.Close(); err != nil {
@@ -160,8 +160,8 @@ func TestLoadDataTrades(t *testing.T) {
database.MigrationDir = filepath.Join("..", "..", "..", "..", "database", "migrations")
testhelpers.MigrationDir = filepath.Join("..", "..", "..", "..", "database", "migrations")
conn, err := testhelpers.ConnectToDatabase(&dbConfg)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = exchangeDB.InsertMany([]exchangeDB.Details{{Name: testExchange}})
@@ -183,13 +183,13 @@ func TestLoadDataTrades(t *testing.T) {
Side: gctorder.Buy.String(),
Timestamp: dInsert,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = LoadData(dStart, dEnd, gctkline.FifteenMin.Duration(), exch, common.DataTrade, p, a, false)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if err = conn.SQL.Close(); err != nil {

View File

@@ -1,33 +1,57 @@
package kline
import (
"fmt"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/log"
)
// NewDataFromKline returns a new struct
func NewDataFromKline() *DataFromKline {
return &DataFromKline{
Base: &data.Base{},
}
}
// HasDataAtTime verifies checks the underlying range data
// To determine whether there is any candle data present at the time provided
func (d *DataFromKline) HasDataAtTime(t time.Time) bool {
if d.RangeHolder == nil {
return false
func (d *DataFromKline) HasDataAtTime(t time.Time) (bool, error) {
isLive, err := d.Base.IsLive()
if err != nil {
return false, err
}
return d.RangeHolder.HasDataAtDate(t)
if isLive {
var s []data.Event
s, err = d.GetStream()
if err != nil {
return false, err
}
for i := range s {
if s[i].GetTime().Equal(t) {
return true, nil
}
}
return false, nil
}
if d.RangeHolder == nil {
return false, fmt.Errorf("%w RangeHolder", gctcommon.ErrNilPointer)
}
return d.RangeHolder.HasDataAtDate(t), nil
}
// Load sets the candle data to the stream for processing
func (d *DataFromKline) Load() error {
d.addedTimes = make(map[int64]bool)
if len(d.Item.Candles) == 0 {
return errNoCandleData
}
klineData := make([]common.DataEventHandler, len(d.Item.Candles))
klineData := make([]data.Event, len(d.Item.Candles))
for i := range d.Item.Candles {
newKline := &kline.Kline{
Base: &event.Base{
@@ -47,135 +71,138 @@ func (d *DataFromKline) Load() error {
ValidationIssues: d.Item.Candles[i].ValidationIssues,
}
klineData[i] = newKline
d.addedTimes[d.Item.Candles[i].Time.UTC().UnixNano()] = true
}
d.SetStream(klineData)
d.SortStream()
return nil
return d.SetStream(klineData)
}
// AppendResults adds a candle item to the data stream and sorts it to ensure it is all in order
func (d *DataFromKline) AppendResults(ki *gctkline.Item) {
if d.addedTimes == nil {
d.addedTimes = make(map[int64]bool)
func (d *DataFromKline) AppendResults(ki *gctkline.Item) error {
if ki == nil {
return fmt.Errorf("%w kline item", gctcommon.ErrNilPointer)
}
err := d.Item.EqualSource(ki)
if err != nil {
return err
}
var gctCandles []gctkline.Candle
for i := range ki.Candles {
if _, ok := d.addedTimes[ki.Candles[i].Time.UnixNano()]; !ok {
gctCandles = append(gctCandles, ki.Candles[i])
d.addedTimes[ki.Candles[i].Time.UnixNano()] = true
stream, err := d.Base.GetStream()
if err != nil {
return err
}
candleLoop:
for x := range ki.Candles {
for y := range stream {
if stream[y].GetTime().Equal(ki.Candles[x].Time) {
continue candleLoop
}
}
gctCandles = append(gctCandles, ki.Candles[x])
}
if len(gctCandles) == 0 {
return nil
}
klineData := make([]data.Event, len(gctCandles))
for i := range gctCandles {
d.Item.Candles = append(d.Item.Candles, gctCandles[i])
newKline := &kline.Kline{
Base: &event.Base{
Exchange: d.Item.Exchange,
Interval: d.Item.Interval,
CurrencyPair: d.Item.Pair,
AssetType: d.Item.Asset,
UnderlyingPair: d.Item.UnderlyingPair,
Time: gctCandles[i].Time.UTC(),
},
Open: decimal.NewFromFloat(gctCandles[i].Open),
High: decimal.NewFromFloat(gctCandles[i].High),
Low: decimal.NewFromFloat(gctCandles[i].Low),
Close: decimal.NewFromFloat(gctCandles[i].Close),
Volume: decimal.NewFromFloat(gctCandles[i].Volume),
}
klineData[i] = newKline
}
err = d.AppendStream(klineData...)
if err != nil {
return err
}
klineData := make([]common.DataEventHandler, len(gctCandles))
candleTimes := make([]time.Time, len(gctCandles))
for i := range gctCandles {
klineData[i] = &kline.Kline{
Base: &event.Base{
Offset: int64(i + 1),
Exchange: ki.Exchange,
Time: gctCandles[i].Time,
Interval: ki.Interval,
CurrencyPair: ki.Pair,
AssetType: ki.Asset,
},
Open: decimal.NewFromFloat(gctCandles[i].Open),
High: decimal.NewFromFloat(gctCandles[i].High),
Low: decimal.NewFromFloat(gctCandles[i].Low),
Close: decimal.NewFromFloat(gctCandles[i].Close),
Volume: decimal.NewFromFloat(gctCandles[i].Volume),
ValidationIssues: gctCandles[i].ValidationIssues,
}
candleTimes[i] = gctCandles[i].Time
d.Item.RemoveDuplicateCandlesByTime()
d.Item.SortCandlesByTimestamp(false)
if d.RangeHolder != nil {
// offline data check when there is a known range
// live data does not need this
d.RangeHolder.SetHasDataFromCandles(d.Item.Candles)
}
for i := range d.RangeHolder.Ranges {
for j := range d.RangeHolder.Ranges[i].Intervals {
d.RangeHolder.Ranges[i].Intervals[j].HasData = true
}
}
log.Debugf(common.Data, "Appending %v candle intervals: %v", len(gctCandles), candleTimes)
d.AppendStream(klineData...)
d.SortStream()
return nil
}
// StreamOpen returns all Open prices from the beginning until the current iteration
func (d *DataFromKline) StreamOpen() []decimal.Decimal {
s := d.GetStream()
o := d.Offset()
ret := make([]decimal.Decimal, o)
for x := range s[:o] {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Open
} else {
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
func (d *DataFromKline) StreamOpen() ([]decimal.Decimal, error) {
s, err := d.History()
if err != nil {
return nil, err
}
return ret
ret := make([]decimal.Decimal, len(s))
for x := range s {
ret[x] = s[x].GetOpenPrice()
}
return ret, nil
}
// StreamHigh returns all High prices from the beginning until the current iteration
func (d *DataFromKline) StreamHigh() []decimal.Decimal {
s := d.GetStream()
o := d.Offset()
ret := make([]decimal.Decimal, o)
for x := range s[:o] {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.High
} else {
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
func (d *DataFromKline) StreamHigh() ([]decimal.Decimal, error) {
s, err := d.History()
if err != nil {
return nil, err
}
return ret
ret := make([]decimal.Decimal, len(s))
for x := range s {
ret[x] = s[x].GetHighPrice()
}
return ret, nil
}
// StreamLow returns all Low prices from the beginning until the current iteration
func (d *DataFromKline) StreamLow() []decimal.Decimal {
s := d.GetStream()
o := d.Offset()
ret := make([]decimal.Decimal, o)
for x := range s[:o] {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Low
} else {
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
func (d *DataFromKline) StreamLow() ([]decimal.Decimal, error) {
s, err := d.History()
if err != nil {
return nil, err
}
return ret
ret := make([]decimal.Decimal, len(s))
for x := range s {
ret[x] = s[x].GetLowPrice()
}
return ret, nil
}
// StreamClose returns all Close prices from the beginning until the current iteration
func (d *DataFromKline) StreamClose() []decimal.Decimal {
s := d.GetStream()
o := d.Offset()
ret := make([]decimal.Decimal, o)
for x := range s[:o] {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Close
} else {
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
func (d *DataFromKline) StreamClose() ([]decimal.Decimal, error) {
s, err := d.History()
if err != nil {
return nil, err
}
return ret
ret := make([]decimal.Decimal, len(s))
for x := range s {
ret[x] = s[x].GetClosePrice()
}
return ret, nil
}
// StreamVol returns all Volume prices from the beginning until the current iteration
func (d *DataFromKline) StreamVol() []decimal.Decimal {
s := d.GetStream()
o := d.Offset()
ret := make([]decimal.Decimal, o)
for x := range s[:o] {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Volume
} else {
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
func (d *DataFromKline) StreamVol() ([]decimal.Decimal, error) {
s, err := d.History()
if err != nil {
return nil, err
}
return ret
ret := make([]decimal.Decimal, len(s))
for x := range s {
ret[x] = s[x].GetVolume()
}
return ret, nil
}

View File

@@ -6,9 +6,10 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -24,7 +25,9 @@ func TestLoad(t *testing.T) {
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
tt := time.Now()
d := DataFromKline{}
d := DataFromKline{
Base: &data.Base{},
}
err := d.Load()
if !errors.Is(err, errNoCandleData) {
t.Errorf("received: %v, expected: %v", err, errNoCandleData)
@@ -46,8 +49,8 @@ func TestLoad(t *testing.T) {
},
}
err = d.Load()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -59,8 +62,22 @@ func TestHasDataAtTime(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := DataFromKline{}
has := d.HasDataAtTime(time.Now())
d := DataFromKline{
Base: &data.Base{},
}
has, err := d.HasDataAtTime(time.Now())
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNilPointer)
}
if has {
t.Error("expected false")
}
d.RangeHolder = &gctkline.IntervalRangeHolder{}
has, err = d.HasDataAtTime(time.Now())
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if has {
t.Error("expected false")
}
@@ -81,22 +98,46 @@ func TestHasDataAtTime(t *testing.T) {
},
},
}
if err := d.Load(); err != nil {
if err = d.Load(); err != nil {
t.Error(err)
}
has = d.HasDataAtTime(dInsert)
has, err = d.HasDataAtTime(dInsert)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if has {
t.Error("expected false")
}
ranger, err := gctkline.CalculateCandleDateRanges(dStart, dEnd, gctkline.OneDay, 100000)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
d.RangeHolder = ranger
d.RangeHolder.SetHasDataFromCandles(d.Item.Candles)
has = d.HasDataAtTime(dInsert)
has, err = d.HasDataAtTime(dInsert)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !has {
t.Error("expected true")
}
err = d.SetLive(true)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
has, err = d.HasDataAtTime(time.Time{})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if has {
t.Error("expected false")
}
has, err = d.HasDataAtTime(dInsert)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !has {
t.Error("expected true")
}
@@ -104,16 +145,18 @@ func TestHasDataAtTime(t *testing.T) {
func TestAppend(t *testing.T) {
t.Parallel()
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := DataFromKline{
Base: &data.Base{},
Item: gctkline.Item{
Exchange: testExchange,
Asset: a,
Pair: p,
},
RangeHolder: &gctkline.IntervalRangeHolder{},
}
item := gctkline.Item{
Exchange: exch,
Pair: p,
Asset: a,
Interval: gctkline.OneDay,
Candles: []gctkline.Candle{
{
@@ -126,7 +169,28 @@ func TestAppend(t *testing.T) {
},
},
}
d.AppendResults(&item)
err := d.AppendResults(&item)
if !errors.Is(err, gctkline.ErrItemNotEqual) {
t.Errorf("received: %v, expected: %v", err, gctkline.ErrItemNotEqual)
}
item.Exchange = testExchange
item.Pair = p
item.Asset = a
err = d.AppendResults(&item)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = d.AppendResults(&item)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = d.AppendResults(nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNilPointer)
}
}
func TestStreamOpen(t *testing.T) {
@@ -134,11 +198,17 @@ func TestStreamOpen(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := DataFromKline{}
if bad := d.StreamOpen(); len(bad) > 0 {
d := DataFromKline{
Base: &data.Base{},
}
bad, err := d.StreamOpen()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(bad) > 0 {
t.Error("expected no stream")
}
d.SetStream([]common.DataEventHandler{
err = d.SetStream([]data.Event{
&kline.Kline{
Base: &event.Base{
Exchange: exch,
@@ -154,8 +224,18 @@ func TestStreamOpen(t *testing.T) {
Volume: elite,
},
})
d.Next()
if open := d.StreamOpen(); len(open) == 0 {
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
open, err := d.StreamOpen()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(open) == 0 {
t.Error("expected open")
}
}
@@ -165,11 +245,17 @@ func TestStreamVolume(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := DataFromKline{}
if bad := d.StreamVol(); len(bad) > 0 {
d := DataFromKline{
Base: &data.Base{},
}
bad, err := d.StreamVol()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(bad) > 0 {
t.Error("expected no stream")
}
d.SetStream([]common.DataEventHandler{
err = d.SetStream([]data.Event{
&kline.Kline{
Base: &event.Base{
Exchange: exch,
@@ -185,8 +271,18 @@ func TestStreamVolume(t *testing.T) {
Volume: elite,
},
})
d.Next()
if open := d.StreamVol(); len(open) == 0 {
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
vol, err := d.StreamVol()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(vol) == 0 {
t.Error("expected volume")
}
}
@@ -196,11 +292,18 @@ func TestStreamClose(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := DataFromKline{}
if bad := d.StreamClose(); len(bad) > 0 {
d := DataFromKline{
Base: &data.Base{},
}
bad, err := d.StreamClose()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(bad) > 0 {
t.Error("expected no stream")
}
d.SetStream([]common.DataEventHandler{
err = d.SetStream([]data.Event{
&kline.Kline{
Base: &event.Base{
Exchange: exch,
@@ -216,8 +319,18 @@ func TestStreamClose(t *testing.T) {
Volume: elite,
},
})
d.Next()
if open := d.StreamClose(); len(open) == 0 {
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
cl, err := d.StreamClose()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(cl) == 0 {
t.Error("expected close")
}
}
@@ -227,11 +340,18 @@ func TestStreamHigh(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := DataFromKline{}
if bad := d.StreamHigh(); len(bad) > 0 {
d := DataFromKline{
Base: &data.Base{},
}
bad, err := d.StreamHigh()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(bad) > 0 {
t.Error("expected no stream")
}
d.SetStream([]common.DataEventHandler{
err = d.SetStream([]data.Event{
&kline.Kline{
Base: &event.Base{
Exchange: exch,
@@ -247,8 +367,18 @@ func TestStreamHigh(t *testing.T) {
Volume: elite,
},
})
d.Next()
if open := d.StreamHigh(); len(open) == 0 {
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
high, err := d.StreamHigh()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(high) == 0 {
t.Error("expected high")
}
}
@@ -259,12 +389,18 @@ func TestStreamLow(t *testing.T) {
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := DataFromKline{
Base: &data.Base{},
RangeHolder: &gctkline.IntervalRangeHolder{},
}
if bad := d.StreamLow(); len(bad) > 0 {
bad, err := d.StreamLow()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(bad) > 0 {
t.Error("expected no stream")
}
d.SetStream([]common.DataEventHandler{
err = d.SetStream([]data.Event{
&kline.Kline{
Base: &event.Base{
Exchange: exch,
@@ -280,8 +416,19 @@ func TestStreamLow(t *testing.T) {
Volume: elite,
},
})
d.Next()
if open := d.StreamLow(); len(open) == 0 {
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
low, err := d.StreamLow()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(low) == 0 {
t.Error("expected low")
}
}

View File

@@ -12,8 +12,7 @@ var errNoCandleData = errors.New("no candle data provided")
// DataFromKline is a struct which implements the data.Streamer interface
// It holds candle data for a specified range with helper functions
type DataFromKline struct {
data.Base
addedTimes map[int64]bool
*data.Base
Item gctkline.Item
RangeHolder *gctkline.IntervalRangeHolder
}

View File

@@ -23,7 +23,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
This package will retrieve data for the backtester via continuous requests to live endpoints
## Important notice
Live trading is not fully implemented and you should never consider setting `RealOrders` to `true` in a config. *Past performance is no guarantee of future results*
Its incredibly risky to enable `real-orders`. *Past performance is no guarantee of future results*
### Please click GoDocs chevron above to view current GoDoc information for this package

View File

@@ -7,35 +7,54 @@ import (
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
)
// LoadData retrieves data from a GoCryptoTrader exchange wrapper which calls the exchange's API for the latest interval
func LoadData(ctx context.Context, exch exchange.IBotExchange, dataType int64, interval time.Duration, fPair currency.Pair, a asset.Item) (*kline.Item, error) {
// note: this is not in a state to utilise with realOrders = true
func LoadData(ctx context.Context, timeToRetrieve time.Time, exch exchange.IBotExchange, dataType int64, interval time.Duration, currencyPair, underlyingPair currency.Pair, a asset.Item, verbose bool) (*kline.Item, error) {
if exch == nil {
return nil, fmt.Errorf("%w IBotExchange", gctcommon.ErrNilPointer)
}
var candles kline.Item
var err error
if verbose {
ctx = request.WithVerbose(ctx)
}
var startTime, endTime time.Time
exchBase := exch.GetBase()
pFmt, err := exchBase.FormatExchangeCurrency(currencyPair, a)
if err != nil {
return nil, err
}
startTime = timeToRetrieve.Truncate(interval).Add(-interval)
endTime = timeToRetrieve.Truncate(interval).Add(-1)
switch dataType {
case common.DataCandle:
candles, err = exch.GetHistoricCandles(ctx,
fPair,
pFmt,
a,
time.Now().Add(-interval*2), // multiplied by 2 to ensure the latest candle is always included
time.Now(),
kline.Interval(interval))
startTime,
endTime,
kline.Interval(interval),
)
if err != nil {
return nil, fmt.Errorf("could not retrieve live candle data for %v %v %v, %v", exch.GetName(), a, fPair, err)
return nil, fmt.Errorf("could not retrieve live candle data for %v %v %v, %v", exch.GetName(), a, currencyPair, err)
}
case common.DataTrade:
var trades []trade.Data
trades, err = exch.GetHistoricTrades(ctx,
fPair,
pFmt,
a,
time.Now().Add(-interval*2), // multiplied by 2 to ensure the latest candle is always included
time.Now())
startTime,
endTime,
)
if err != nil {
return nil, err
}
@@ -47,22 +66,24 @@ func LoadData(ctx context.Context, exch exchange.IBotExchange, dataType int64, i
base := exch.GetBase()
if len(candles.Candles) <= 1 && base.GetSupportedFeatures().RESTCapabilities.TradeHistory {
trades, err = exch.GetHistoricTrades(ctx,
fPair,
pFmt,
a,
time.Now().Add(-interval),
time.Now())
startTime,
endTime,
)
if err != nil {
return nil, fmt.Errorf("could not retrieve live trade data for %v %v %v, %v", exch.GetName(), a, fPair, err)
return nil, fmt.Errorf("could not retrieve live trade data for %v %v %v, %v", exch.GetName(), a, currencyPair, err)
}
candles, err = trade.ConvertTradesToCandles(kline.Interval(interval), trades...)
if err != nil {
return nil, fmt.Errorf("could not convert live trade data to candles for %v %v %v, %v", exch.GetName(), a, fPair, err)
return nil, fmt.Errorf("could not convert live trade data to candles for %v %v %v, %v", exch.GetName(), a, currencyPair, err)
}
}
default:
return nil, fmt.Errorf("could not retrieve live data for %v %v %v, %w", exch.GetName(), a, fPair, common.ErrInvalidDataType)
return nil, fmt.Errorf("could not retrieve live data for %v %v %v, %w: '%v'", exch.GetName(), a, currencyPair, common.ErrInvalidDataType, dataType)
}
candles.Exchange = strings.ToLower(exch.GetName())
candles.UnderlyingPair = underlyingPair
return &candles, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
@@ -18,7 +19,7 @@ const testExchange = "binance"
func TestLoadCandles(t *testing.T) {
t.Parallel()
interval := gctkline.OneHour
cp1 := currency.NewPair(currency.BTC, currency.USDT)
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
em := engine.SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
@@ -30,22 +31,21 @@ func TestLoadCandles(t *testing.T) {
exch.SetDefaults()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp1},
Enabled: currency.Pairs{cp1},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
RequestFormat: pFormat,
ConfigFormat: pFormat,
}
var data *gctkline.Item
data, err = LoadData(context.Background(),
exch, common.DataCandle, interval.Duration(), cp1, a)
data, err = LoadData(context.Background(), time.Now(), exch, common.DataCandle, interval.Duration(), cp, currency.EMPTYPAIR, a, true)
if err != nil {
t.Fatal(err)
}
if len(data.Candles) == 0 {
t.Error("expected candles")
}
_, err = LoadData(context.Background(), exch, -1, interval.Duration(), cp1, a)
_, err = LoadData(context.Background(), time.Now(), exch, -1, interval.Duration(), cp, currency.EMPTYPAIR, a, true)
if !errors.Is(err, common.ErrInvalidDataType) {
t.Errorf("received: %v, expected: %v", err, common.ErrInvalidDataType)
}
@@ -54,7 +54,7 @@ func TestLoadCandles(t *testing.T) {
func TestLoadTrades(t *testing.T) {
t.Parallel()
interval := gctkline.OneMin
cp1 := currency.NewPair(currency.BTC, currency.USDT)
cp := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
em := engine.SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
@@ -66,14 +66,14 @@ func TestLoadTrades(t *testing.T) {
exch.SetDefaults()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp1},
Enabled: currency.Pairs{cp1},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
RequestFormat: pFormat,
ConfigFormat: pFormat,
}
var data *gctkline.Item
data, err = LoadData(context.Background(), exch, common.DataTrade, interval.Duration(), cp1, a)
data, err = LoadData(context.Background(), time.Now(), exch, common.DataTrade, interval.Duration(), cp, currency.EMPTYPAIR, a, true)
if err != nil {
t.Fatal(err)
}

View File

@@ -3,20 +3,17 @@ package engine
import (
"errors"
"fmt"
"sync"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
@@ -28,31 +25,100 @@ import (
"github.com/thrasher-corp/gocryptotrader/log"
)
// New returns a new BackTest instance
func New() (*BackTest, error) {
bt := &BackTest{
shutdown: make(chan struct{}),
Datas: &data.HandlerPerCurrency{},
EventQueue: &eventholder.Holder{},
}
err := bt.SetupMetaData()
if err != nil {
return nil, err
}
return bt, nil
}
// Reset BackTest values to default
func (bt *BackTest) Reset() {
bt.EventQueue.Reset()
bt.Datas.Reset()
bt.Portfolio.Reset()
bt.Statistic.Reset()
bt.Exchange.Reset()
bt.Funding.Reset()
func (bt *BackTest) Reset() error {
if bt == nil {
return gctcommon.ErrNilPointer
}
var err error
if bt.orderManager != nil {
err = bt.orderManager.Stop()
if err != nil {
return err
}
}
if bt.databaseManager != nil {
err = bt.databaseManager.Stop()
if err != nil {
return err
}
}
err = bt.EventQueue.Reset()
if err != nil {
return err
}
err = bt.DataHolder.Reset()
if err != nil {
return err
}
err = bt.Portfolio.Reset()
if err != nil {
return err
}
err = bt.Statistic.Reset()
if err != nil {
return err
}
err = bt.Exchange.Reset()
if err != nil {
return err
}
err = bt.Funding.Reset()
if err != nil {
return err
}
bt.exchangeManager = nil
bt.orderManager = nil
bt.databaseManager = nil
return nil
}
// RunLive is a proof of concept function that does not yet support multi currency usage
// It tasks by constantly checking for new live datas and running through the list of events
// once new data is processed. It will run until application close event has been received
func (bt *BackTest) RunLive() error {
if bt.LiveDataHandler == nil {
return errLiveOnly
}
var err error
if bt.LiveDataHandler.IsRealOrders() {
err = bt.LiveDataHandler.UpdateFunding(false)
if err != nil {
return err
}
}
err = bt.LiveDataHandler.Start()
if err != nil {
return err
}
bt.wg.Add(1)
go func() {
err = bt.liveCheck()
if err != nil {
log.Error(common.LiveStrategy, err)
}
bt.wg.Done()
}()
return nil
}
func (bt *BackTest) liveCheck() error {
for {
select {
case <-bt.shutdown:
return bt.LiveDataHandler.Stop()
case <-bt.LiveDataHandler.HasShutdownFromError():
return bt.Stop()
case <-bt.LiveDataHandler.HasShutdown():
return nil
case <-bt.LiveDataHandler.Updated():
err := bt.Run()
if err != nil {
return err
}
}
}
}
// ExecuteStrategy executes the strategy using the provided configs
@@ -67,96 +133,115 @@ func (bt *BackTest) ExecuteStrategy(waitForOfflineCompletion bool) error {
}
if !bt.MetaData.Closed && !bt.MetaData.DateStarted.IsZero() {
bt.m.Unlock()
return fmt.Errorf("%w %v %v", errRunIsRunning, bt.MetaData.ID, bt.MetaData.Strategy)
return fmt.Errorf("%w %v %v", errTaskIsRunning, bt.MetaData.ID, bt.MetaData.Strategy)
}
if bt.MetaData.Closed {
bt.m.Unlock()
return fmt.Errorf("%w %v %v", errAlreadyRan, bt.MetaData.ID, bt.MetaData.Strategy)
}
if waitForOfflineCompletion && bt.MetaData.LiveTesting {
bt.m.Unlock()
return fmt.Errorf("%w cannot wait for a live task to finish", errCannotHandleRequest)
}
bt.MetaData.DateStarted = time.Now()
liveTesting := bt.MetaData.LiveTesting
bt.m.Unlock()
var wg sync.WaitGroup
if waitForOfflineCompletion {
wg.Add(1)
var err error
switch {
case waitForOfflineCompletion && !liveTesting:
err = bt.Run()
if err != nil {
log.Error(common.Backtester, err)
}
return bt.Stop()
case !waitForOfflineCompletion && liveTesting:
return bt.RunLive()
case !waitForOfflineCompletion && !liveTesting:
go func() {
err = bt.Run()
if err != nil {
log.Error(common.Backtester, err)
}
err = bt.Stop()
if err != nil {
log.Error(common.Backtester, err)
}
}()
}
go func() {
if waitForOfflineCompletion {
defer wg.Done()
}
if liveTesting {
if waitForOfflineCompletion {
log.Errorf(common.Backtester, "%v cannot wait for completion of a live test", errCannotHandleRequest)
return
}
err := bt.RunLive()
if err != nil {
log.Error(log.Global, err)
}
} else {
bt.Run()
close(bt.shutdown)
bt.m.Lock()
bt.MetaData.Closed = true
bt.MetaData.DateEnded = time.Now()
bt.m.Unlock()
err := bt.Statistic.CalculateAllResults()
if err != nil {
log.Error(log.Global, err)
return
}
err = bt.Reports.GenerateReport()
if err != nil {
log.Error(log.Global, err)
}
}
}()
wg.Wait()
return nil
}
// Run will iterate over loaded data events
// save them and then handle the event based on its type
func (bt *BackTest) Run() {
func (bt *BackTest) Run() error {
// doubleNil allows the run function to exit if no new data is detected on a live run
var doubleNil bool
if bt.MetaData.DateLoaded.IsZero() {
return
return errNotSetup
}
log.Info(common.Backtester, "Running backtester against pre-defined data")
dataLoadingIssue:
for ev := bt.EventQueue.NextEvent(); ; ev = bt.EventQueue.NextEvent() {
if ev == nil {
dataHandlerMap := bt.Datas.GetAllData()
var hasProcessedData bool
for exchangeName, exchangeMap := range dataHandlerMap {
for assetItem, assetMap := range exchangeMap {
for currencyPair, dataHandler := range assetMap {
d := dataHandler.Next()
if d == nil {
if !bt.hasHandledEvent {
log.Errorf(common.Backtester, "Unable to perform `Next` for %v %v %v", exchangeName, assetItem, currencyPair)
}
break dataLoadingIssue
}
if bt.Strategy.UsingSimultaneousProcessing() && hasProcessedData {
// only append one event, as simultaneous processing
// will retrieve all relevant events to process under
// processSimultaneousDataEvents()
continue
}
bt.EventQueue.AppendEvent(d)
hasProcessedData = true
if bt.hasShutdown {
return nil
}
if doubleNil {
if bt.verbose {
log.Info(common.Backtester, "No new data on second check")
}
return nil
}
doubleNil = true
dataHandlers, err := bt.DataHolder.GetAllData()
if err != nil {
return err
}
for i := range dataHandlers {
var e data.Event
e, err = dataHandlers[i].Next()
if err != nil {
if errors.Is(err, data.ErrEndOfData) {
return nil
}
return err
}
if e == nil {
if !bt.hasProcessedAnEvent && bt.LiveDataHandler == nil {
var (
exch string
assetItem asset.Item
cp currency.Pair
)
exch, assetItem, cp, err = dataHandlers[i].GetDetails()
if err != nil {
return err
}
log.Errorf(common.Backtester, "Unable to perform `Next` for %v %v %v", exch, assetItem, cp)
}
return nil
}
o := e.GetOffset()
if bt.Strategy.UsingSimultaneousProcessing() && bt.hasProcessedDataAtOffset[o] {
// only append one event, as simultaneous processing
// will retrieve all relevant events to process under
// processSimultaneousDataEvents()
continue
}
bt.EventQueue.AppendEvent(e)
if !bt.hasProcessedDataAtOffset[o] {
bt.hasProcessedDataAtOffset[o] = true
}
}
} else {
doubleNil = false
err := bt.handleEvent(ev)
if err != nil {
log.Error(common.Backtester, err)
}
}
if !bt.hasHandledEvent {
bt.hasHandledEvent = true
if !bt.hasProcessedAnEvent {
bt.hasProcessedAnEvent = true
}
}
}
}
@@ -165,24 +250,19 @@ dataLoadingIssue:
// after data has been loaded and Run has appended a data event to the queue,
// handle event will process events and add further events to the queue if they
// are required
func (bt *BackTest) handleEvent(ev common.EventHandler) error {
func (bt *BackTest) handleEvent(ev common.Event) error {
if ev == nil {
return fmt.Errorf("cannot handle event %w", errNilData)
}
funds, err := bt.Funding.GetFundingForEvent(ev)
if err != nil {
return err
}
if bt.Funding.HasFutures() {
err = bt.Funding.UpdateCollateral(ev)
if err != nil {
return err
}
}
switch eType := ev.(type) {
case common.DataEventHandler:
case kline.Event:
// using kline.Event as signal.Event also matches data.Event
if bt.Strategy.UsingSimultaneousProcessing() {
err = bt.processSimultaneousDataEvents()
} else {
@@ -194,8 +274,19 @@ func (bt *BackTest) handleEvent(ev common.EventHandler) error {
err = bt.processOrderEvent(eType, funds.FundReleaser())
case fill.Event:
err = bt.processFillEvent(eType, funds.FundReleaser())
if bt.LiveDataHandler != nil {
// output log data per interval instead of at the end
result, logErr := bt.Statistic.CreateLog(eType)
if logErr != nil {
return logErr
}
if err != nil {
return err
}
log.Info(common.LiveStrategy, result)
}
default:
return fmt.Errorf("handleEvent %w %T received, could not process",
err = fmt.Errorf("handleEvent %w %T received, could not process",
errUnhandledDatatype,
ev)
}
@@ -203,17 +294,16 @@ func (bt *BackTest) handleEvent(ev common.EventHandler) error {
return err
}
bt.Funding.CreateSnapshot(ev.GetTime())
return nil
return bt.Funding.CreateSnapshot(ev.GetTime())
}
// processSingleDataEvent will pass the event to the strategy and determine how it should be handled
func (bt *BackTest) processSingleDataEvent(ev common.DataEventHandler, funds funding.IFundReleaser) error {
func (bt *BackTest) processSingleDataEvent(ev data.Event, funds funding.IFundReleaser) error {
err := bt.updateStatsForDataEvent(ev, funds)
if err != nil {
return err
}
d, err := bt.Datas.GetDataForCurrency(ev)
d, err := bt.DataHolder.GetDataForCurrency(ev)
if err != nil {
return err
}
@@ -239,39 +329,54 @@ func (bt *BackTest) processSingleDataEvent(ev common.DataEventHandler, funds fun
// to the event queue. It will pass all currency events to the strategy to determine what
// currencies to act upon
func (bt *BackTest) processSimultaneousDataEvents() error {
var dataEvents []data.Handler
dataHandlerMap := bt.Datas.GetAllData()
for _, exchangeMap := range dataHandlerMap {
for _, assetMap := range exchangeMap {
for _, dataHandler := range assetMap {
latestData := dataHandler.Latest()
funds, err := bt.Funding.GetFundingForEvent(latestData)
if err != nil {
return err
dataHolders, err := bt.DataHolder.GetAllData()
if err != nil {
return err
}
dataEvents := make([]data.Handler, 0, len(dataHolders))
for i := range dataHolders {
var latestData data.Event
latestData, err = dataHolders[i].Latest()
if err != nil {
return err
}
var funds funding.IFundingPair
funds, err = bt.Funding.GetFundingForEvent(latestData)
if err != nil {
return err
}
err = bt.updateStatsForDataEvent(latestData, funds.FundReleaser())
if err != nil {
switch {
case errors.Is(err, statistics.ErrAlreadyProcessed):
if !bt.MetaData.Closed || !bt.MetaData.ClosePositionsOnStop {
// Closing positions on close reuses existing events and doesn't need to be logged
// any other scenario, this should be logged
log.Warnf(common.LiveStrategy, "%v %v", latestData.GetOffset(), err)
}
err = bt.updateStatsForDataEvent(latestData, funds.FundReleaser())
if err != nil {
switch {
case errors.Is(err, statistics.ErrAlreadyProcessed):
continue
case errors.Is(err, gctorder.ErrPositionLiquidated):
return nil
default:
log.Error(common.Backtester, err)
}
}
dataEvents = append(dataEvents, dataHandler)
continue
case errors.Is(err, gctorder.ErrPositionLiquidated):
return nil
default:
log.Error(common.Backtester, err)
}
}
dataEvents = append(dataEvents, dataHolders[i])
}
signals, err := bt.Strategy.OnSimultaneousSignals(dataEvents, bt.Funding, bt.Portfolio)
if err != nil {
if errors.Is(err, base.ErrTooMuchBadData) {
switch {
case errors.Is(err, base.ErrTooMuchBadData):
// too much bad data is a severe error and backtesting must cease
return err
case errors.Is(err, base.ErrNoDataToProcess) && bt.MetaData.Closed && bt.MetaData.ClosePositionsOnStop:
// event queue is being cleared with no data events to process
return nil
default:
log.Errorf(common.Backtester, "OnSimultaneousSignals %v", err)
return nil
}
log.Errorf(common.Backtester, "OnSimultaneousSignals %v", err)
return nil
}
for i := range signals {
err = bt.Statistic.SetEventForOffset(signals[i])
@@ -285,20 +390,20 @@ func (bt *BackTest) processSimultaneousDataEvents() error {
// updateStatsForDataEvent makes various systems aware of price movements from
// data events
func (bt *BackTest) updateStatsForDataEvent(ev common.DataEventHandler, funds funding.IFundReleaser) error {
func (bt *BackTest) updateStatsForDataEvent(ev data.Event, funds funding.IFundReleaser) error {
if ev == nil {
return common.ErrNilEvent
}
if funds == nil {
return fmt.Errorf("%v %v %v %w missing fund releaser", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), common.ErrNilArguments)
return fmt.Errorf("%v %v %v %w missing fund releaser", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), gctcommon.ErrNilPointer)
}
// update statistics with the latest price
err := bt.Statistic.SetupEventForTime(ev)
err := bt.Statistic.SetEventForOffset(ev)
if err != nil {
if errors.Is(err, statistics.ErrAlreadyProcessed) {
return err
}
log.Errorf(common.Backtester, "SetupEventForTime %v", err)
log.Errorf(common.Backtester, "SetEventForOffset %v", err)
}
// update portfolio manager with the latest price
err = bt.Portfolio.UpdateHoldings(ev, funds)
@@ -332,15 +437,17 @@ func (bt *BackTest) updateStatsForDataEvent(ev common.DataEventHandler, funds fu
if pnl.Result.IsLiquidated {
return nil
}
err = bt.Portfolio.CheckLiquidationStatus(ev, cr, pnl)
if err != nil {
if errors.Is(err, gctorder.ErrPositionLiquidated) {
liquidErr := bt.triggerLiquidationsForExchange(ev, pnl)
if liquidErr != nil {
return liquidErr
if bt.LiveDataHandler == nil || (bt.LiveDataHandler != nil && !bt.LiveDataHandler.IsRealOrders()) {
err = bt.Portfolio.CheckLiquidationStatus(ev, cr, pnl)
if err != nil {
if errors.Is(err, gctorder.ErrPositionLiquidated) {
liquidErr := bt.triggerLiquidationsForExchange(ev, pnl)
if liquidErr != nil {
return liquidErr
}
}
return err
}
return err
}
return bt.Statistic.AddPNLForTime(pnl)
@@ -349,66 +456,28 @@ func (bt *BackTest) updateStatsForDataEvent(ev common.DataEventHandler, funds fu
return nil
}
func (bt *BackTest) triggerLiquidationsForExchange(ev common.DataEventHandler, pnl *portfolio.PNLSummary) error {
if ev == nil {
return common.ErrNilEvent
}
if pnl == nil {
return fmt.Errorf("%w pnl summary", common.ErrNilArguments)
}
orders, err := bt.Portfolio.CreateLiquidationOrdersForExchange(ev, bt.Funding)
if err != nil {
return err
}
for i := range orders {
// these orders are raising events for event offsets
// which may not have been processed yet
// this will create and store stats for each order
// then liquidate it at the funding level
var datas data.Handler
datas, err = bt.Datas.GetDataForCurrency(orders[i])
if err != nil {
return err
}
latest := datas.Latest()
err = bt.Statistic.SetupEventForTime(latest)
if err != nil && !errors.Is(err, statistics.ErrAlreadyProcessed) {
return err
}
bt.EventQueue.AppendEvent(orders[i])
err = bt.Statistic.SetEventForOffset(orders[i])
if err != nil {
log.Errorf(common.Backtester, "SetupEventForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
bt.Funding.Liquidate(orders[i])
}
pnl.Result.IsLiquidated = true
pnl.Result.Status = gctorder.Liquidated
return bt.Statistic.AddPNLForTime(pnl)
}
// processSignalEvent receives an event from the strategy for processing under the portfolio
func (bt *BackTest) processSignalEvent(ev signal.Event, funds funding.IFundReserver) error {
if ev == nil {
return common.ErrNilEvent
}
if funds == nil {
return fmt.Errorf("%w funds", common.ErrNilArguments)
return fmt.Errorf("%w funds", gctcommon.ErrNilPointer)
}
cs, err := bt.Exchange.GetCurrencySettings(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
if err != nil {
log.Errorf(common.Backtester, "GetCurrencySettings %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("GetCurrencySettings %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
log.Errorf(common.Backtester, "GetCurrencySettings %v", err)
return fmt.Errorf("GetCurrencySettings %v", err)
}
var o *order.Order
o, err = bt.Portfolio.OnSignal(ev, &cs, funds)
if err != nil {
log.Errorf(common.Backtester, "OnSignal %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
log.Errorf(common.Backtester, "OnSignal %v", err)
return fmt.Errorf("OnSignal %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Statistic.SetEventForOffset(o)
if err != nil {
return fmt.Errorf("SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("SetEventForOffset %v", err)
}
bt.EventQueue.AppendEvent(o)
@@ -420,9 +489,9 @@ func (bt *BackTest) processOrderEvent(ev order.Event, funds funding.IFundRelease
return common.ErrNilEvent
}
if funds == nil {
return fmt.Errorf("%w funds", common.ErrNilArguments)
return fmt.Errorf("%w funds", gctcommon.ErrNilPointer)
}
d, err := bt.Datas.GetDataForCurrency(ev)
d, err := bt.DataHolder.GetDataForCurrency(ev)
if err != nil {
return err
}
@@ -445,36 +514,27 @@ func (bt *BackTest) processOrderEvent(ev order.Event, funds funding.IFundRelease
}
func (bt *BackTest) processFillEvent(ev fill.Event, funds funding.IFundReleaser) error {
t, err := bt.Portfolio.OnFill(ev, funds)
_, err := bt.Portfolio.OnFill(ev, funds)
if err != nil {
return fmt.Errorf("OnFill %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Statistic.SetEventForOffset(t)
err = bt.Funding.UpdateCollateralForEvent(ev, false)
if err != nil {
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("UpdateCollateralForEvent %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
var holding *holdings.Holding
holding, err = bt.Portfolio.ViewHoldingAtTimePeriod(ev)
holding, err := bt.Portfolio.ViewHoldingAtTimePeriod(ev)
if err != nil {
log.Error(common.Backtester, err)
}
if holding == nil {
log.Error(common.Backtester, "ViewHoldingAtTimePeriod why is holdings nil?")
} else {
err = bt.Statistic.AddHoldingsForTime(holding)
if err != nil {
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
}
var cp *compliance.Manager
cp, err = bt.Portfolio.GetComplianceManager(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
err = bt.Statistic.AddHoldingsForTime(holding)
if err != nil {
log.Errorf(common.Backtester, "GetComplianceManager %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
snap := cp.GetLatestSnapshot()
snap, err := bt.Portfolio.GetLatestComplianceSnapshot(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
if err != nil {
log.Errorf(common.Backtester, "GetLatestComplianceSnapshot %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Statistic.AddComplianceSnapshotForTime(snap, ev)
if err != nil {
log.Errorf(common.Backtester, "AddComplianceSnapshotForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
@@ -498,81 +558,215 @@ func (bt *BackTest) processFillEvent(ev fill.Event, funds funding.IFundReleaser)
if ev.GetAssetType().IsFutures() {
return bt.processFuturesFillEvent(ev, funds)
}
return nil
}
func (bt *BackTest) processFuturesFillEvent(ev fill.Event, funds funding.IFundReleaser) error {
if ev.GetOrder() != nil {
pnl, err := bt.Portfolio.TrackFuturesOrder(ev, funds)
if err != nil && !errors.Is(err, gctorder.ErrSubmissionIsNil) {
return fmt.Errorf("TrackFuturesOrder %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
if ev.GetOrder() == nil {
return nil
}
pnl, err := bt.Portfolio.TrackFuturesOrder(ev, funds)
if err != nil && !errors.Is(err, gctorder.ErrSubmissionIsNil) {
return fmt.Errorf("TrackFuturesOrder %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.GetExchangeByName(ev.GetExchange())
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.GetExchangeByName(ev.GetExchange())
if err != nil {
return fmt.Errorf("GetExchangeByName %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
rPNL := pnl.GetRealisedPNL()
if !rPNL.PNL.IsZero() {
var receivingCurrency currency.Code
var receivingAsset asset.Item
receivingCurrency, receivingAsset, err = exch.GetCurrencyForRealisedPNL(ev.GetAssetType(), ev.Pair())
if err != nil {
return fmt.Errorf("GetExchangeByName %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("GetCurrencyForRealisedPNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
rPNL := pnl.GetRealisedPNL()
if !rPNL.PNL.IsZero() {
var receivingCurrency currency.Code
var receivingAsset asset.Item
receivingCurrency, receivingAsset, err = exch.GetCurrencyForRealisedPNL(ev.GetAssetType(), ev.Pair())
if err != nil {
return fmt.Errorf("GetCurrencyForRealisedPNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Funding.RealisePNL(ev.GetExchange(), receivingAsset, receivingCurrency, rPNL.PNL)
if err != nil {
return fmt.Errorf("RealisePNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
}
err = bt.Statistic.AddPNLForTime(pnl)
err = bt.Funding.RealisePNL(ev.GetExchange(), receivingAsset, receivingCurrency, rPNL.PNL)
if err != nil {
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("RealisePNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
}
err := bt.Funding.UpdateCollateral(ev)
err = bt.Statistic.AddPNLForTime(pnl)
if err != nil {
return fmt.Errorf("UpdateCollateral %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
return fmt.Errorf("AddPNLForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Funding.UpdateCollateralForEvent(ev, false)
if err != nil {
return fmt.Errorf("UpdateCollateralForEvent %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
return nil
}
// Stop shuts down the live data loop
func (bt *BackTest) Stop() {
func (bt *BackTest) Stop() error {
if bt == nil {
return
return gctcommon.ErrNilPointer
}
bt.m.Lock()
defer bt.m.Unlock()
if bt.MetaData.Closed {
return
return errAlreadyRan
}
close(bt.shutdown)
bt.MetaData.Closed = true
bt.MetaData.DateEnded = time.Now()
if bt.MetaData.ClosePositionsOnStop {
err := bt.CloseAllPositions()
if err != nil {
log.Errorf(common.Backtester, "Could not close all positions on stop: %s", err)
}
}
err := bt.Statistic.CalculateAllResults()
if err != nil {
log.Error(log.Global, err)
return
return err
}
err = bt.Reports.GenerateReport()
if err != nil {
log.Error(log.Global, err)
return err
}
return nil
}
// GenerateSummary creates a summary of a backtesting/livestrategy run
// this summary contains many details of a run
func (bt *BackTest) GenerateSummary() (*RunSummary, error) {
func (bt *BackTest) triggerLiquidationsForExchange(ev data.Event, pnl *portfolio.PNLSummary) error {
if ev == nil {
return common.ErrNilEvent
}
if pnl == nil {
return fmt.Errorf("%w pnl summary", gctcommon.ErrNilPointer)
}
orders, err := bt.Portfolio.CreateLiquidationOrdersForExchange(ev, bt.Funding)
if err != nil {
return err
}
for i := range orders {
// these orders are raising events for event offsets
// which may not have been processed yet
// this will create and store stats for each order
// then liquidate it at the funding level
var datas data.Handler
datas, err = bt.DataHolder.GetDataForCurrency(orders[i])
if err != nil {
return err
}
var latest data.Event
latest, err = datas.Latest()
if err != nil {
return err
}
err = bt.Statistic.SetEventForOffset(latest)
if err != nil && !errors.Is(err, statistics.ErrAlreadyProcessed) {
return err
}
bt.EventQueue.AppendEvent(orders[i])
err = bt.Statistic.SetEventForOffset(orders[i])
if err != nil {
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
}
err = bt.Funding.Liquidate(orders[i])
if err != nil {
return err
}
}
pnl.Result.IsLiquidated = true
pnl.Result.Status = gctorder.Liquidated
return bt.Statistic.AddPNLForTime(pnl)
}
// CloseAllPositions will close sell any positions held on closure
// can only be with live testing and where a strategy supports it
func (bt *BackTest) CloseAllPositions() error {
if bt.LiveDataHandler == nil {
return errLiveOnly
}
err := bt.LiveDataHandler.UpdateFunding(true)
if err != nil {
return err
}
dataHolders, err := bt.DataHolder.GetAllData()
if err != nil {
return err
}
latestPrices := make([]data.Event, len(dataHolders))
for i := range dataHolders {
var latest data.Event
latest, err = dataHolders[i].Latest()
if err != nil {
return err
}
latestPrices[i] = latest
}
events, err := bt.Strategy.CloseAllPositions(bt.Portfolio.GetLatestHoldingsForAllCurrencies(), latestPrices)
if err != nil {
if errors.Is(err, gctcommon.ErrFunctionNotSupported) {
log.Warnf(common.LiveStrategy, "Closing all positions is not supported by strategy %v", bt.Strategy.Name())
return nil
}
return err
}
if len(events) == 0 {
return nil
}
err = bt.LiveDataHandler.SetDataForClosingAllPositions(events...)
if err != nil {
return err
}
for i := range events {
k := events[i].ToKline()
err = bt.Statistic.SetEventForOffset(k)
if err != nil {
return err
}
bt.EventQueue.AppendEvent(events[i])
}
err = bt.Run()
if err != nil {
return err
}
err = bt.LiveDataHandler.UpdateFunding(true)
if err != nil {
return err
}
err = bt.Funding.CreateSnapshot(events[0].GetTime())
if err != nil {
return err
}
for i := range events {
var funds funding.IFundingPair
funds, err = bt.Funding.GetFundingForEvent(events[i])
if err != nil {
return err
}
err = bt.Portfolio.SetHoldingsForEvent(funds.FundReader(), events[i])
if err != nil {
return err
}
}
her := bt.Portfolio.GetLatestHoldingsForAllCurrencies()
for i := range her {
err = bt.Statistic.AddHoldingsForTime(&her[i])
if err != nil {
return err
}
}
return nil
}
// GenerateSummary creates a summary of a strategy task
// this summary contains many details of a task
func (bt *BackTest) GenerateSummary() (*TaskSummary, error) {
if bt == nil {
return nil, gctcommon.ErrNilPointer
}
bt.m.Lock()
defer bt.m.Unlock()
return &RunSummary{
return &TaskSummary{
MetaData: bt.MetaData,
}, nil
}
@@ -597,7 +791,7 @@ func (bt *BackTest) SetupMetaData() error {
return nil
}
// IsRunning checks if the run is running
// IsRunning checks if the task is running
func (bt *BackTest) IsRunning() bool {
if bt == nil {
return false
@@ -607,7 +801,7 @@ func (bt *BackTest) IsRunning() bool {
return !bt.MetaData.DateStarted.IsZero() && !bt.MetaData.Closed
}
// HasRan checks if the run has been ran
// HasRan checks if the task has been executed
func (bt *BackTest) HasRan() bool {
if bt == nil {
return false
@@ -617,7 +811,7 @@ func (bt *BackTest) HasRan() bool {
return bt.MetaData.Closed
}
// Equal checks if the incoming run matches
// Equal checks if the incoming task matches
func (bt *BackTest) Equal(bt2 *BackTest) bool {
if bt == nil || bt2 == nil {
return false

File diff suppressed because it is too large Load Diff

View File

@@ -18,57 +18,61 @@ import (
)
var (
errNilConfig = errors.New("unable to setup backtester with nil 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")
errNotSetup = errors.New("backtesting run not setup")
errNilConfig = errors.New("unable to setup backtester with nil 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")
errNilData = errors.New("nil data received")
errLiveOnly = errors.New("close all positions is only supported by live data type")
errNotSetup = errors.New("backtesting task not setup")
)
// BackTest is the main holder of all backtesting functionality
type BackTest struct {
m sync.Mutex
hasHandledEvent bool
MetaData RunMetaData
shutdown chan struct{}
Datas data.Holder
Strategy strategies.Handler
Portfolio portfolio.Handler
Exchange exchange.ExecutionHandler
Statistic statistics.Handler
EventQueue eventholder.EventHolder
Reports report.Handler
Funding funding.IFundingManager
exchangeManager *engine.ExchangeManager
orderManager *engine.OrderManager
databaseManager *engine.DatabaseConnectionManager
m sync.Mutex
wg sync.WaitGroup
verbose bool
hasProcessedAnEvent bool
hasShutdown bool
shutdown chan struct{}
MetaData TaskMetaData
DataHolder data.Holder
LiveDataHandler Handler
Strategy strategies.Handler
Portfolio portfolio.Handler
Exchange exchange.ExecutionHandler
Statistic statistics.Handler
EventQueue eventholder.EventHolder
Reports report.Handler
Funding funding.IFundingManager
exchangeManager *engine.ExchangeManager
orderManager *engine.OrderManager
databaseManager *engine.DatabaseConnectionManager
hasProcessedDataAtOffset map[int64]bool
}
// RunSummary holds details of a BackTest
// TaskSummary holds details of a BackTest
// rather than passing entire contents around
type RunSummary struct {
MetaData RunMetaData
type TaskSummary struct {
MetaData TaskMetaData
}
// RunMetaData contains details about a run such as when it was loaded
type RunMetaData struct {
ID uuid.UUID
Strategy string
DateLoaded time.Time
DateStarted time.Time
DateEnded time.Time
Closed bool
LiveTesting bool
RealOrders bool
// TaskMetaData contains details about a run such as when it was loaded
type TaskMetaData struct {
ID uuid.UUID
Strategy string
DateLoaded time.Time
DateStarted time.Time
DateEnded time.Time
Closed bool
ClosePositionsOnStop bool
LiveTesting bool
RealOrders bool
}
// RunManager contains all backtesting/livestrategy runs
type RunManager struct {
m sync.Mutex
runs []*BackTest
// TaskManager contains all strategy tasks
type TaskManager struct {
m sync.Mutex
tasks []*BackTest
}

View File

@@ -0,0 +1,338 @@
package engine
import (
"time"
"github.com/gofrs/uuid"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// Overriding functions
// these are designed to override interface implementations
// so there is less requirement gathering per test as the functions are
// tested in their own package
type fakeFolio struct{}
func (f fakeFolio) GetLatestComplianceSnapshot(string, asset.Item, currency.Pair) (*compliance.Snapshot, error) {
return &compliance.Snapshot{}, nil
}
func (f fakeFolio) GetPositions(common.Event) ([]gctorder.Position, error) {
return nil, nil
}
func (f fakeFolio) SetHoldingsForEvent(funding.IFundReader, common.Event) error {
return nil
}
func (f fakeFolio) SetHoldingsForTimestamp(*holdings.Holding) error {
return nil
}
func (f fakeFolio) OnSignal(signal.Event, *exchange.Settings, funding.IFundReserver) (*order.Order, error) {
return nil, nil
}
func (f fakeFolio) OnFill(fill.Event, funding.IFundReleaser) (fill.Event, error) {
return nil, nil
}
func (f fakeFolio) GetLatestOrderSnapshotForEvent(common.Event) (compliance.Snapshot, error) {
return compliance.Snapshot{}, nil
}
func (f fakeFolio) GetLatestOrderSnapshots() ([]compliance.Snapshot, error) {
return nil, nil
}
func (f fakeFolio) ViewHoldingAtTimePeriod(common.Event) (*holdings.Holding, error) {
return nil, nil
}
func (f fakeFolio) UpdateHoldings(data.Event, funding.IFundReleaser) error {
return nil
}
func (f fakeFolio) GetComplianceManager(string, asset.Item, currency.Pair) (*compliance.Manager, error) {
return nil, nil
}
func (f fakeFolio) TrackFuturesOrder(fill.Event, funding.IFundReleaser) (*portfolio.PNLSummary, error) {
return &portfolio.PNLSummary{}, nil
}
func (f fakeFolio) UpdatePNL(common.Event, decimal.Decimal) error {
return nil
}
func (f fakeFolio) GetLatestPNLForEvent(common.Event) (*portfolio.PNLSummary, error) {
return &portfolio.PNLSummary{}, nil
}
func (f fakeFolio) GetLatestPNLs() []portfolio.PNLSummary {
return nil
}
func (f fakeFolio) CheckLiquidationStatus(data.Event, funding.ICollateralReader, *portfolio.PNLSummary) error {
return nil
}
func (f fakeFolio) CreateLiquidationOrdersForExchange(data.Event, funding.IFundingManager) ([]order.Event, error) {
return nil, nil
}
func (f fakeFolio) GetLatestHoldingsForAllCurrencies() []holdings.Holding {
return nil
}
func (f fakeFolio) Reset() error {
return nil
}
type fakeReport struct{}
func (f fakeReport) GenerateReport() error {
return nil
}
func (f fakeReport) SetKlineData(*gctkline.Item) error {
return nil
}
func (f fakeReport) UseDarkMode(bool) {}
type fakeStats struct{}
func (f *fakeStats) SetStrategyName(string) {
}
func (f *fakeStats) SetEventForOffset(common.Event) error {
return nil
}
func (f *fakeStats) AddHoldingsForTime(*holdings.Holding) error {
return nil
}
func (f *fakeStats) AddComplianceSnapshotForTime(*compliance.Snapshot, common.Event) error {
return nil
}
func (f *fakeStats) CalculateAllResults() error {
return nil
}
func (f *fakeStats) Reset() error {
return nil
}
func (f *fakeStats) Serialise() (string, error) {
return "", nil
}
func (f *fakeStats) AddPNLForTime(*portfolio.PNLSummary) error {
return nil
}
func (f *fakeStats) CreateLog(common.Event) (string, error) {
return "", nil
}
type fakeDataHolder struct{}
func (f fakeDataHolder) Setup() {
}
func (f fakeDataHolder) SetDataForCurrency(string, asset.Item, currency.Pair, data.Handler) error {
return nil
}
func (f fakeDataHolder) GetAllData() ([]data.Handler, error) {
cp := currency.NewPair(currency.BTC, currency.USD)
return []data.Handler{&kline.DataFromKline{
Base: &data.Base{},
Item: gctkline.Item{
Exchange: testExchange,
Pair: cp,
UnderlyingPair: cp,
Asset: asset.Spot,
Interval: gctkline.OneMin,
Candles: []gctkline.Candle{
{
Time: time.Now(),
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
},
},
SourceJobID: uuid.UUID{},
ValidationJobID: uuid.UUID{},
},
RangeHolder: &gctkline.IntervalRangeHolder{},
},
}, nil
}
func (f fakeDataHolder) GetDataForCurrency(common.Event) (data.Handler, error) {
return nil, nil
}
func (f fakeDataHolder) Reset() error {
return nil
}
type fakeFunding struct {
hasFutures bool
}
func (f fakeFunding) UpdateCollateralForEvent(common.Event, bool) error {
return nil
}
func (f fakeFunding) UpdateAllCollateral(bool, bool) error {
return nil
}
func (f fakeFunding) UpdateFundingFromLiveData(bool) error {
return nil
}
func (f fakeFunding) SetFunding(string, asset.Item, *account.Balance, bool) error {
return nil
}
func (f fakeFunding) Reset() error {
return nil
}
func (f fakeFunding) IsUsingExchangeLevelFunding() bool {
return true
}
func (f fakeFunding) GetFundingForEvent(common.Event) (funding.IFundingPair, error) {
return &funding.SpotPair{}, nil
}
func (f fakeFunding) Transfer(decimal.Decimal, *funding.Item, *funding.Item, bool) error {
return nil
}
func (f fakeFunding) GenerateReport() (*funding.Report, error) {
return nil, nil
}
func (f fakeFunding) AddUSDTrackingData(*kline.DataFromKline) error {
return nil
}
func (f fakeFunding) CreateSnapshot(time.Time) error {
return nil
}
func (f fakeFunding) USDTrackingDisabled() bool {
return false
}
func (f fakeFunding) Liquidate(common.Event) error {
return nil
}
func (f fakeFunding) GetAllFunding() ([]funding.BasicItem, error) {
return nil, nil
}
func (f fakeFunding) UpdateCollateral() error {
return nil
}
func (f fakeFunding) HasFutures() bool {
return f.hasFutures
}
func (f fakeFunding) HasExchangeBeenLiquidated(common.Event) bool {
return false
}
func (f fakeFunding) RealisePNL(string, asset.Item, currency.Code, decimal.Decimal) error {
return nil
}
type fakeStrat struct{}
func (f fakeStrat) Name() string {
return "fake"
}
func (f fakeStrat) Description() string {
return "fake"
}
func (f fakeStrat) OnSignal(data.Handler, funding.IFundingTransferer, portfolio.Handler) (signal.Event, error) {
return nil, nil
}
func (f fakeStrat) OnSimultaneousSignals([]data.Handler, funding.IFundingTransferer, portfolio.Handler) ([]signal.Event, error) {
return nil, nil
}
func (f fakeStrat) UsingSimultaneousProcessing() bool {
return true
}
func (f fakeStrat) SupportsSimultaneousProcessing() bool {
return true
}
func (f fakeStrat) SetSimultaneousProcessing(bool) {}
func (f fakeStrat) SetCustomSettings(map[string]interface{}) error {
return nil
}
func (f fakeStrat) SetDefaults() {}
func (f fakeStrat) CloseAllPositions([]holdings.Holding, []data.Event) ([]signal.Event, error) {
return []signal.Event{
&signal.Signal{
Base: &event.Base{
Offset: 1,
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.FifteenSecond,
CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
UnderlyingPair: currency.NewPair(currency.BTC, currency.USD),
AssetType: asset.Spot,
},
OpenPrice: leet,
HighPrice: leet,
LowPrice: leet,
ClosePrice: leet,
Volume: leet,
BuyLimit: leet,
SellLimit: leet,
Amount: leet,
Direction: gctorder.Buy,
},
}, nil
}

View File

@@ -9,13 +9,13 @@ import (
"net/http"
"path/filepath"
"strings"
"time"
"github.com/gofrs/uuid"
grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/btrpc"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
@@ -23,6 +23,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/database/drivers"
gctengine "github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/gctrpc/auth"
@@ -42,16 +43,16 @@ var (
type GRPCServer struct {
btrpc.BacktesterServiceServer
config *config.BacktesterConfig
manager *RunManager
manager *TaskManager
}
// SetupRPCServer sets up the gRPC server
func SetupRPCServer(cfg *config.BacktesterConfig, manager *RunManager) (*GRPCServer, error) {
func SetupRPCServer(cfg *config.BacktesterConfig, manager *TaskManager) (*GRPCServer, error) {
if cfg == nil {
return nil, fmt.Errorf("%w backtester config", common.ErrNilArguments)
return nil, fmt.Errorf("%w backtester config", gctcommon.ErrNilPointer)
}
if manager == nil {
return nil, fmt.Errorf("%w run manager", common.ErrNilArguments)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
return &GRPCServer{
config: cfg,
@@ -100,7 +101,7 @@ func StartRPCServer(server *GRPCServer) error {
// StartRPCRESTProxy starts a gRPC proxy
func (s *GRPCServer) StartRPCRESTProxy() error {
log.Debugf(log.GRPCSys, "GRPC proxy server support enabled. Starting gRPC proxy server on http://%v.\n", s.config.GRPC.GRPCProxyListenAddress)
log.Debugf(log.GRPCSys, "GRPC proxy server support enabled. Starting gRPC proxy server on %v\n", s.config.GRPC.GRPCProxyListenAddress)
targetDir := utils.GetTLSDir(s.config.GRPC.TLSDir)
creds, err := credentials.NewClientTLSFromFile(filepath.Join(targetDir, "cert.pem"), "")
if err != nil {
@@ -121,7 +122,13 @@ func (s *GRPCServer) StartRPCRESTProxy() error {
}
go func() {
if err = http.ListenAndServe(s.config.GRPC.GRPCProxyListenAddress, mux); err != nil {
server := &http.Server{
Addr: s.config.GRPC.GRPCProxyListenAddress,
ReadHeaderTimeout: time.Minute,
ReadTimeout: time.Minute,
}
if err = server.ListenAndServe(); err != nil {
log.Errorf(log.GRPCSys, "GRPC proxy failed to server: %s\n", err)
}
}()
@@ -161,25 +168,25 @@ func (s *GRPCServer) authenticateClient(ctx context.Context) (context.Context, e
return ctx, nil
}
// convertSummary converts a run summary into a RPC format
func convertSummary(run *RunSummary) *btrpc.RunSummary {
runSummary := &btrpc.RunSummary{
Id: run.MetaData.ID.String(),
StrategyName: run.MetaData.Strategy,
Closed: run.MetaData.Closed,
LiveTesting: run.MetaData.LiveTesting,
RealOrders: run.MetaData.RealOrders,
// convertSummary converts a task summary into a RPC format
func convertSummary(task *TaskSummary) *btrpc.TaskSummary {
taskSummary := &btrpc.TaskSummary{
Id: task.MetaData.ID.String(),
StrategyName: task.MetaData.Strategy,
Closed: task.MetaData.Closed,
LiveTesting: task.MetaData.LiveTesting,
RealOrders: task.MetaData.RealOrders,
}
if !run.MetaData.DateStarted.IsZero() {
runSummary.DateStarted = run.MetaData.DateStarted.Format(gctcommon.SimpleTimeFormatWithTimezone)
if !task.MetaData.DateStarted.IsZero() {
taskSummary.DateStarted = task.MetaData.DateStarted.Format(gctcommon.SimpleTimeFormatWithTimezone)
}
if !run.MetaData.DateLoaded.IsZero() {
runSummary.DateLoaded = run.MetaData.DateLoaded.Format(gctcommon.SimpleTimeFormatWithTimezone)
if !task.MetaData.DateLoaded.IsZero() {
taskSummary.DateLoaded = task.MetaData.DateLoaded.Format(gctcommon.SimpleTimeFormatWithTimezone)
}
if !run.MetaData.DateEnded.IsZero() {
runSummary.DateEnded = run.MetaData.DateEnded.Format(gctcommon.SimpleTimeFormatWithTimezone)
if !task.MetaData.DateEnded.IsZero() {
taskSummary.DateEnded = task.MetaData.DateEnded.Format(gctcommon.SimpleTimeFormatWithTimezone)
}
return runSummary
return taskSummary
}
// ExecuteStrategyFromFile will backtest a strategy from the filepath provided
@@ -188,13 +195,13 @@ func (s *GRPCServer) ExecuteStrategyFromFile(_ context.Context, request *btrpc.E
return nil, fmt.Errorf("%w server config", gctcommon.ErrNilPointer)
}
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
if request == nil {
return nil, fmt.Errorf("%w nil request", common.ErrNilArguments)
return nil, fmt.Errorf("%w request", gctcommon.ErrNilPointer)
}
if request.DoNotRunImmediately && request.DoNotStore {
return nil, fmt.Errorf("%w cannot manage a run with both dnr and dns", errCannotHandleRequest)
return nil, fmt.Errorf("%w cannot manage a task with both dnr and dns", errCannotHandleRequest)
}
dir := request.StrategyFilePath
@@ -208,7 +215,7 @@ func (s *GRPCServer) ExecuteStrategyFromFile(_ context.Context, request *btrpc.E
return nil, err
}
if cfg == nil {
err = fmt.Errorf("%w backtester config", common.ErrNilArguments)
err = fmt.Errorf("%w backtester config", gctcommon.ErrNilPointer)
return nil, err
}
@@ -217,13 +224,13 @@ func (s *GRPCServer) ExecuteStrategyFromFile(_ context.Context, request *btrpc.E
s.config.Report.TemplatePath = ""
}
bt, err := NewFromConfig(cfg, s.config.Report.TemplatePath, s.config.Report.OutputPath, s.config.Verbose)
bt, err := NewBacktesterFromConfigs(cfg, s.config)
if err != nil {
return nil, err
}
if !request.DoNotStore {
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if err != nil {
return nil, err
}
@@ -240,7 +247,7 @@ func (s *GRPCServer) ExecuteStrategyFromFile(_ context.Context, request *btrpc.E
return nil, err
}
return &btrpc.ExecuteStrategyResponse{
Run: convertSummary(btSum),
Task: convertSummary(btSum),
}, nil
}
@@ -252,13 +259,13 @@ func (s *GRPCServer) ExecuteStrategyFromConfig(_ context.Context, request *btrpc
return nil, fmt.Errorf("%w server config", gctcommon.ErrNilPointer)
}
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
if request == nil || request.Config == nil {
return nil, fmt.Errorf("%w nil request", common.ErrNilArguments)
return nil, fmt.Errorf("%w request", gctcommon.ErrNilPointer)
}
if request.DoNotRunImmediately && request.DoNotStore {
return nil, fmt.Errorf("%w cannot manage a run with both dnr and dns", errCannotHandleRequest)
return nil, fmt.Errorf("%w cannot manage a task with both dnr and dns", errCannotHandleRequest)
}
rfr, err := decimal.NewFromString(request.Config.StatisticSettings.RiskFreeRate)
@@ -518,13 +525,28 @@ func (s *GRPCServer) ExecuteStrategyFromConfig(_ context.Context, request *btrpc
}
var liveData *config.LiveData
if request.Config.DataSettings.LiveData != nil {
creds := make([]config.Credentials, len(request.Config.DataSettings.LiveData.Credentials))
for i := range request.Config.DataSettings.LiveData.Credentials {
creds[i] = config.Credentials{
Exchange: request.Config.DataSettings.LiveData.Credentials[i].Exchange,
Keys: account.Credentials{
Key: request.Config.DataSettings.LiveData.Credentials[i].Keys.Key,
Secret: request.Config.DataSettings.LiveData.Credentials[i].Keys.Secret,
ClientID: request.Config.DataSettings.LiveData.Credentials[i].Keys.ClientId,
PEMKey: request.Config.DataSettings.LiveData.Credentials[i].Keys.PemKey,
SubAccount: request.Config.DataSettings.LiveData.Credentials[i].Keys.SubAccount,
OneTimePassword: request.Config.DataSettings.LiveData.Credentials[i].Keys.OneTimePassword,
},
}
}
liveData = &config.LiveData{
APIKeyOverride: request.Config.DataSettings.LiveData.ApiKeyOverride,
APISecretOverride: request.Config.DataSettings.LiveData.ApiSecretOverride,
APIClientIDOverride: request.Config.DataSettings.LiveData.ApiClientIdOverride,
API2FAOverride: request.Config.DataSettings.LiveData.Api_2FaOverride,
APISubAccountOverride: request.Config.DataSettings.LiveData.ApiSubAccountOverride,
RealOrders: request.Config.DataSettings.LiveData.UseRealOrders,
NewEventTimeout: time.Duration(request.Config.DataSettings.LiveData.NewEventTimeout),
DataCheckTimer: time.Duration(request.Config.DataSettings.LiveData.DataCheckTimer),
RealOrders: request.Config.DataSettings.LiveData.RealOrders,
ClosePositionsOnStop: request.Config.DataSettings.LiveData.ClosePositionsOnStop,
DataRequestRetryTolerance: request.Config.DataSettings.LiveData.DataRequestRetryTolerance,
DataRequestRetryWaitTime: time.Duration(request.Config.DataSettings.LiveData.DataRequestRetryWaitTime),
ExchangeCredentials: creds,
}
}
var csvData *config.CSVData
@@ -584,13 +606,13 @@ func (s *GRPCServer) ExecuteStrategyFromConfig(_ context.Context, request *btrpc
s.config.Report.TemplatePath = ""
}
bt, err := NewFromConfig(cfg, s.config.Report.TemplatePath, s.config.Report.OutputPath, s.config.Verbose)
bt, err := NewBacktesterFromConfigs(cfg, s.config)
if err != nil {
return nil, err
}
if !request.DoNotStore {
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if err != nil {
return nil, err
}
@@ -607,157 +629,157 @@ func (s *GRPCServer) ExecuteStrategyFromConfig(_ context.Context, request *btrpc
return nil, err
}
return &btrpc.ExecuteStrategyResponse{
Run: convertSummary(btSum),
Task: convertSummary(btSum),
}, nil
}
// ListAllRuns returns all backtesting/livestrategy runs managed by the server
func (s *GRPCServer) ListAllRuns(_ context.Context, _ *btrpc.ListAllRunsRequest) (*btrpc.ListAllRunsResponse, error) {
// ListAllTasks returns all strategy tasks managed by the server
func (s *GRPCServer) ListAllTasks(_ context.Context, _ *btrpc.ListAllTasksRequest) (*btrpc.ListAllTasksResponse, error) {
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
list, err := s.manager.List()
if err != nil {
return nil, err
}
response := make([]*btrpc.RunSummary, len(list))
response := make([]*btrpc.TaskSummary, len(list))
for i := range list {
response[i] = convertSummary(list[i])
}
return &btrpc.ListAllRunsResponse{
Runs: response,
return &btrpc.ListAllTasksResponse{
Tasks: response,
}, nil
}
// StopRun stops a backtest/livestrategy run in its tracks
func (s *GRPCServer) StopRun(_ context.Context, req *btrpc.StopRunRequest) (*btrpc.StopRunResponse, error) {
// StopTask stops a strategy task in its tracks
func (s *GRPCServer) StopTask(_ context.Context, req *btrpc.StopTaskRequest) (*btrpc.StopTaskResponse, error) {
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
if req == nil {
return nil, fmt.Errorf("%w StopRunRequest", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w StopTaskRequest", gctcommon.ErrNilPointer)
}
id, err := uuid.FromString(req.Id)
if err != nil {
return nil, err
}
run, err := s.manager.GetSummary(id)
task, err := s.manager.GetSummary(id)
if err != nil {
return nil, err
}
err = s.manager.StopRun(id)
err = s.manager.StopTask(id)
if err != nil {
return nil, err
}
return &btrpc.StopRunResponse{
StoppedRun: convertSummary(run),
return &btrpc.StopTaskResponse{
StoppedTask: convertSummary(task),
}, nil
}
// StopAllRuns stops all backtest/livestrategy runs in its tracks
func (s *GRPCServer) StopAllRuns(_ context.Context, _ *btrpc.StopAllRunsRequest) (*btrpc.StopAllRunsResponse, error) {
// StopAllTasks stops all strategy tasks in its tracks
func (s *GRPCServer) StopAllTasks(_ context.Context, _ *btrpc.StopAllTasksRequest) (*btrpc.StopAllTasksResponse, error) {
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
stopped, err := s.manager.StopAllRuns()
stopped, err := s.manager.StopAllTasks()
if err != nil {
return nil, err
}
stoppedRuns := make([]*btrpc.RunSummary, len(stopped))
stoppedTasks := make([]*btrpc.TaskSummary, len(stopped))
for i := range stopped {
stoppedRuns[i] = convertSummary(stopped[i])
stoppedTasks[i] = convertSummary(stopped[i])
}
return &btrpc.StopAllRunsResponse{
RunsStopped: stoppedRuns,
return &btrpc.StopAllTasksResponse{
TasksStopped: stoppedTasks,
}, nil
}
// StartRun starts a backtest/livestrategy that was set to not start automatically
func (s *GRPCServer) StartRun(_ context.Context, req *btrpc.StartRunRequest) (*btrpc.StartRunResponse, error) {
// StartTask starts a strategy that was set to not start automatically
func (s *GRPCServer) StartTask(_ context.Context, req *btrpc.StartTaskRequest) (*btrpc.StartTaskResponse, error) {
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
if req == nil {
return nil, fmt.Errorf("%w StartRunRequest", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w StartTaskRequest", gctcommon.ErrNilPointer)
}
id, err := uuid.FromString(req.Id)
if err != nil {
return nil, err
}
err = s.manager.StartRun(id)
err = s.manager.StartTask(id)
if err != nil {
return nil, err
}
return &btrpc.StartRunResponse{
return &btrpc.StartTaskResponse{
Started: true,
}, nil
}
// StartAllRuns starts all backtest/livestrategy runs
func (s *GRPCServer) StartAllRuns(_ context.Context, _ *btrpc.StartAllRunsRequest) (*btrpc.StartAllRunsResponse, error) {
// StartAllTasks starts all strategy tasks
func (s *GRPCServer) StartAllTasks(_ context.Context, _ *btrpc.StartAllTasksRequest) (*btrpc.StartAllTasksResponse, error) {
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
started, err := s.manager.StartAllRuns()
started, err := s.manager.StartAllTasks()
if err != nil {
return nil, err
}
startedRuns := make([]string, len(started))
startedTasks := make([]string, len(started))
for i := range started {
startedRuns[i] = started[i].String()
startedTasks[i] = started[i].String()
}
return &btrpc.StartAllRunsResponse{
RunsStarted: startedRuns,
return &btrpc.StartAllTasksResponse{
TasksStarted: startedTasks,
}, nil
}
// ClearRun removes a run from memory, but only if it is not running
func (s *GRPCServer) ClearRun(_ context.Context, req *btrpc.ClearRunRequest) (*btrpc.ClearRunResponse, error) {
// ClearTask removes a task from memory, but only if it is not running
func (s *GRPCServer) ClearTask(_ context.Context, req *btrpc.ClearTaskRequest) (*btrpc.ClearTaskResponse, error) {
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
if req == nil {
return nil, fmt.Errorf("%w ClearRunRequest", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w ClearTaskRequest", gctcommon.ErrNilPointer)
}
id, err := uuid.FromString(req.Id)
if err != nil {
return nil, err
}
run, err := s.manager.GetSummary(id)
task, err := s.manager.GetSummary(id)
if err != nil {
return nil, err
}
err = s.manager.ClearRun(id)
err = s.manager.ClearTask(id)
if err != nil {
return nil, err
}
return &btrpc.ClearRunResponse{
ClearedRun: convertSummary(run),
return &btrpc.ClearTaskResponse{
ClearedTask: convertSummary(task),
}, nil
}
// ClearAllRuns removes all runs from memory, but only if they are not running
func (s *GRPCServer) ClearAllRuns(_ context.Context, _ *btrpc.ClearAllRunsRequest) (*btrpc.ClearAllRunsResponse, error) {
// ClearAllTasks removes all tasks from memory, but only if they are not running
func (s *GRPCServer) ClearAllTasks(_ context.Context, _ *btrpc.ClearAllTasksRequest) (*btrpc.ClearAllTasksResponse, error) {
if s.manager == nil {
return nil, fmt.Errorf("%w run manager", gctcommon.ErrNilPointer)
return nil, fmt.Errorf("%w task manager", gctcommon.ErrNilPointer)
}
clearedRuns, remainingRuns, err := s.manager.ClearAllRuns()
clearedTasks, remainingTasks, err := s.manager.ClearAllTasks()
if err != nil {
return nil, err
}
clearedResponse := make([]*btrpc.RunSummary, len(clearedRuns))
for i := range clearedRuns {
clearedResponse[i] = convertSummary(clearedRuns[i])
clearedResponse := make([]*btrpc.TaskSummary, len(clearedTasks))
for i := range clearedTasks {
clearedResponse[i] = convertSummary(clearedTasks[i])
}
remainingResponse := make([]*btrpc.RunSummary, len(remainingRuns))
for i := range remainingRuns {
remainingResponse[i] = convertSummary(remainingRuns[i])
remainingResponse := make([]*btrpc.TaskSummary, len(remainingTasks))
for i := range remainingTasks {
remainingResponse[i] = convertSummary(remainingTasks[i])
}
return &btrpc.ClearAllRunsResponse{
ClearedRuns: clearedResponse,
RemainingRuns: remainingResponse,
return &btrpc.ClearAllTasksResponse{
ClearedTasks: clearedResponse,
RemainingTasks: remainingResponse,
}, nil
}

View File

@@ -14,7 +14,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/ftxcashandcarry"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/binancecashandcarry"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -39,10 +39,10 @@ func TestExecuteStrategyFromFile(t *testing.T) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
s.manager = NewTaskManager()
_, err = s.ExecuteStrategyFromFile(context.Background(), nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received '%v' expecting '%v'", err, common.ErrNilArguments)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
_, err = s.ExecuteStrategyFromFile(context.Background(), &btrpc.ExecuteStrategyFromFileRequest{})
@@ -85,10 +85,10 @@ func TestExecuteStrategyFromConfig(t *testing.T) {
}
s.config.Report.GenerateReport = false
s.manager = SetupRunManager()
s.manager = NewTaskManager()
_, err = s.ExecuteStrategyFromConfig(context.Background(), nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received '%v' expecting '%v'", err, common.ErrNilArguments)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
defaultConfig, err := config.ReadStrategyConfigFromFile(dcaConfigPath)
@@ -188,13 +188,23 @@ func TestExecuteStrategyFromConfig(t *testing.T) {
}
}
if defaultConfig.DataSettings.LiveData != nil {
creds := make([]*btrpc.ExchangeCredentials, len(defaultConfig.DataSettings.LiveData.ExchangeCredentials))
for i := range defaultConfig.DataSettings.LiveData.ExchangeCredentials {
creds[i] = &btrpc.ExchangeCredentials{
Exchange: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Exchange,
Keys: &btrpc.ExchangeKeys{
Key: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.Key,
Secret: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.Secret,
ClientId: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.ClientID,
PemKey: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.PEMKey,
SubAccount: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.SubAccount,
OneTimePassword: defaultConfig.DataSettings.LiveData.ExchangeCredentials[i].Keys.OneTimePassword,
},
}
}
dataSettings.LiveData = &btrpc.LiveData{
ApiKeyOverride: defaultConfig.DataSettings.LiveData.APIKeyOverride,
ApiSecretOverride: defaultConfig.DataSettings.LiveData.APISecretOverride,
ApiClientIdOverride: defaultConfig.DataSettings.LiveData.APIClientIDOverride,
Api_2FaOverride: defaultConfig.DataSettings.LiveData.API2FAOverride,
ApiSubAccountOverride: defaultConfig.DataSettings.LiveData.APISubAccountOverride,
UseRealOrders: defaultConfig.DataSettings.LiveData.RealOrders,
RealOrders: defaultConfig.DataSettings.LiveData.RealOrders,
Credentials: creds,
}
}
if defaultConfig.DataSettings.CSVData != nil {
@@ -325,278 +335,281 @@ func TestExecuteStrategyFromConfig(t *testing.T) {
}
}
func TestListAllRuns(t *testing.T) {
func TestListAllTasks(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.ListAllRuns(context.Background(), nil)
_, err := s.ListAllTasks(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
_, err = s.ListAllRuns(context.Background(), nil)
s.manager = NewTaskManager()
_, err = s.ListAllTasks(context.Background(), nil)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
resp, err := s.ListAllRuns(context.Background(), &btrpc.ListAllRunsRequest{})
resp, err := s.ListAllTasks(context.Background(), &btrpc.ListAllTasksRequest{})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if len(resp.Runs) != 1 {
t.Errorf("received '%v' expecting '%v'", len(resp.Runs), 1)
if len(resp.Tasks) != 1 {
t.Errorf("received '%v' expecting '%v'", len(resp.Tasks), 1)
}
}
func TestGRPCStopRun(t *testing.T) {
func TestGRPCStopTask(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.StopRun(context.Background(), nil)
_, err := s.StopTask(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
_, err = s.StopRun(context.Background(), nil)
s.manager = NewTaskManager()
_, err = s.StopTask(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &fakeStrat{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
Statistic: &statistics.Statistic{},
DataHolder: &data.HandlerHolder{},
Statistic: &fakeStats{},
Reports: &fakeReport{},
shutdown: make(chan struct{}),
}
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = s.StopRun(context.Background(), &btrpc.StopRunRequest{
_, err = s.StopTask(context.Background(), &btrpc.StopTaskRequest{
Id: bt.MetaData.ID.String(),
})
if !errors.Is(err, errRunHasNotRan) {
t.Errorf("received '%v' expecting '%v'", err, errRunHasNotRan)
if !errors.Is(err, errTaskHasNotRan) {
t.Errorf("received '%v' expecting '%v'", err, errTaskHasNotRan)
}
if len(s.manager.runs) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.runs), 1)
if len(s.manager.tasks) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.tasks), 1)
}
s.manager.runs[0].MetaData.DateStarted = time.Now()
_, err = s.StopRun(context.Background(), &btrpc.StopRunRequest{
s.manager.tasks[0].MetaData.DateStarted = time.Now()
_, err = s.StopTask(context.Background(), &btrpc.StopTaskRequest{
Id: bt.MetaData.ID.String(),
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if s.manager.runs[0].MetaData.DateEnded.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.runs[0].MetaData.DateEnded, "a date")
if s.manager.tasks[0].MetaData.DateEnded.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.tasks[0].MetaData.DateEnded, "a date")
}
}
func TestGRPCStopAllRuns(t *testing.T) {
func TestGRPCStopAllTasks(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.StopAllRuns(context.Background(), nil)
_, err := s.StopAllTasks(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
_, err = s.StopAllRuns(context.Background(), nil)
s.manager = NewTaskManager()
_, err = s.StopAllTasks(context.Background(), nil)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &fakeStrat{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
Statistic: &statistics.Statistic{},
DataHolder: &data.HandlerHolder{},
Statistic: &fakeStats{},
Reports: &fakeReport{},
shutdown: make(chan struct{}),
}
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
resp, err := s.StopAllRuns(context.Background(), &btrpc.StopAllRunsRequest{})
resp, err := s.StopAllTasks(context.Background(), &btrpc.StopAllTasksRequest{})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if len(s.manager.runs) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.runs), 1)
if len(s.manager.tasks) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.tasks), 1)
}
if len(resp.RunsStopped) != 0 {
t.Errorf("received '%v' expecting '%v'", len(resp.RunsStopped), 0)
if len(resp.TasksStopped) != 0 {
t.Errorf("received '%v' expecting '%v'", len(resp.TasksStopped), 0)
}
s.manager.runs[0].MetaData.DateStarted = time.Now()
resp, err = s.StopAllRuns(context.Background(), &btrpc.StopAllRunsRequest{})
s.manager.tasks[0].MetaData.DateStarted = time.Now()
resp, err = s.StopAllTasks(context.Background(), &btrpc.StopAllTasksRequest{})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if s.manager.runs[0].MetaData.DateEnded.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.runs[0].MetaData.DateEnded, "a date")
if s.manager.tasks[0].MetaData.DateEnded.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.tasks[0].MetaData.DateEnded, "a date")
}
if len(resp.RunsStopped) != 1 {
t.Errorf("received '%v' expecting '%v'", len(resp.RunsStopped), 1)
if len(resp.TasksStopped) != 1 {
t.Errorf("received '%v' expecting '%v'", len(resp.TasksStopped), 1)
}
}
func TestGRPCStartRun(t *testing.T) {
func TestGRPCStartTask(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.StartRun(context.Background(), nil)
_, err := s.StartTask(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
_, err = s.StartRun(context.Background(), nil)
s.manager = NewTaskManager()
_, err = s.StartTask(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &fakeStrat{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
Statistic: &statistics.Statistic{},
DataHolder: &data.HandlerHolder{},
Statistic: &fakeStats{},
Reports: &fakeReport{},
shutdown: make(chan struct{}),
}
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = s.StartRun(context.Background(), &btrpc.StartRunRequest{
_, err = s.StartTask(context.Background(), &btrpc.StartTaskRequest{
Id: bt.MetaData.ID.String(),
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if len(s.manager.runs) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.runs), 1)
if len(s.manager.tasks) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.tasks), 1)
}
if s.manager.runs[0].MetaData.DateStarted.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.runs[0].MetaData.DateStarted, "a date")
if s.manager.tasks[0].MetaData.DateStarted.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.tasks[0].MetaData.DateStarted, "a date")
}
}
func TestGRPCStartAllRuns(t *testing.T) {
func TestGRPCStartAllTasks(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.StartAllRuns(context.Background(), nil)
_, err := s.StartAllTasks(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
_, err = s.StartAllRuns(context.Background(), nil)
s.manager = NewTaskManager()
_, err = s.StartAllTasks(context.Background(), nil)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = s.StartAllRuns(context.Background(), &btrpc.StartAllRunsRequest{})
_, err = s.StartAllTasks(context.Background(), &btrpc.StartAllTasksRequest{})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if len(s.manager.runs) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.runs), 1)
if len(s.manager.tasks) != 1 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.tasks), 1)
}
if s.manager.runs[0].MetaData.DateStarted.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.runs[0].MetaData.DateStarted, "a date")
if s.manager.tasks[0].MetaData.DateStarted.IsZero() {
t.Errorf("received '%v' expecting '%v'", s.manager.tasks[0].MetaData.DateStarted, "a date")
}
}
func TestGRPCClearRun(t *testing.T) {
func TestGRPCClearTask(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.ClearRun(context.Background(), nil)
_, err := s.ClearTask(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
_, err = s.ClearRun(context.Background(), nil)
s.manager = NewTaskManager()
_, err = s.ClearTask(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = s.ClearRun(context.Background(), &btrpc.ClearRunRequest{
_, err = s.ClearTask(context.Background(), &btrpc.ClearTaskRequest{
Id: bt.MetaData.ID.String(),
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if len(s.manager.runs) != 0 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.runs), 0)
if len(s.manager.tasks) != 0 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.tasks), 0)
}
}
func TestGRPCClearAllRuns(t *testing.T) {
func TestGRPCClearAllTasks(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.ClearAllRuns(context.Background(), nil)
_, err := s.ClearAllTasks(context.Background(), nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expecting '%v'", err, gctcommon.ErrNilPointer)
}
s.manager = SetupRunManager()
_, err = s.ClearAllRuns(context.Background(), nil)
s.manager = NewTaskManager()
_, err = s.ClearAllTasks(context.Background(), nil)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = s.manager.AddRun(bt)
err = s.manager.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = s.ClearAllRuns(context.Background(), &btrpc.ClearAllRunsRequest{})
_, err = s.ClearAllTasks(context.Background(), &btrpc.ClearAllTasksRequest{})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
if len(s.manager.runs) != 0 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.runs), 0)
if len(s.manager.tasks) != 0 {
t.Fatalf("received '%v' expecting '%v'", len(s.manager.tasks), 0)
}
}

View File

@@ -2,138 +2,525 @@ package engine
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/live"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/log"
)
// RunLive is a proof of concept function that does not yet support multi currency usage
// It runs by constantly checking for new live datas and running through the list of events
// once new data is processed. It will run until application close event has been received
func (bt *BackTest) RunLive() error {
log.Info(common.Backtester, "Running backtester against live data")
timeoutTimer := time.NewTimer(time.Minute * 5)
// a frequent timer so that when a new candle is released by an exchange
// that it can be processed quickly
processEventTicker := time.NewTicker(time.Second)
doneARun := false
// SetupLiveDataHandler creates a live data handler to retrieve and append
// live data as it comes in
func (bt *BackTest) SetupLiveDataHandler(eventTimeout, dataCheckInterval time.Duration, realOrders, verbose bool) error {
if bt == nil {
return fmt.Errorf("%w backtester", gctcommon.ErrNilPointer)
}
if bt.exchangeManager == nil {
return fmt.Errorf("%w engine manager", gctcommon.ErrNilPointer)
}
if bt.DataHolder == nil {
return fmt.Errorf("%w data holder", gctcommon.ErrNilPointer)
}
if bt.Reports == nil {
return fmt.Errorf("%w reports", gctcommon.ErrNilPointer)
}
if bt.Funding == nil {
return fmt.Errorf("%w funding manager", gctcommon.ErrNilPointer)
}
if eventTimeout <= 0 {
log.Warnf(common.LiveStrategy, "Invalid event timeout '%v', defaulting to '%v'", eventTimeout, defaultEventTimeout)
eventTimeout = defaultEventTimeout
}
if dataCheckInterval <= 0 {
log.Warnf(common.LiveStrategy, "Invalid data check interval '%v', defaulting to '%v'", dataCheckInterval, defaultDataCheckInterval)
dataCheckInterval = defaultDataCheckInterval
}
bt.LiveDataHandler = &dataChecker{
verboseDataCheck: verbose,
realOrders: realOrders,
hasUpdatedFunding: false,
exchangeManager: bt.exchangeManager,
sourcesToCheck: nil,
eventTimeout: eventTimeout,
dataCheckInterval: dataCheckInterval,
dataHolder: bt.DataHolder,
shutdownErr: make(chan bool),
dataUpdated: make(chan bool),
report: bt.Reports,
funding: bt.Funding,
}
return nil
}
// Start begins fetching and appending live data
func (d *dataChecker) Start() error {
if d == nil {
return gctcommon.ErrNilPointer
}
if !atomic.CompareAndSwapUint32(&d.started, 0, 1) {
return engine.ErrSubSystemAlreadyStarted
}
d.wg.Add(1)
d.shutdown = make(chan bool)
d.dataUpdated = make(chan bool)
d.shutdownErr = make(chan bool)
go func() {
err := d.DataFetcher()
if err != nil {
stopErr := d.SignalStopFromError(err)
if stopErr != nil {
log.Error(common.LiveStrategy, stopErr)
}
}
}()
return nil
}
// IsRunning verifies whether the live data checker is running
func (d *dataChecker) IsRunning() bool {
return d != nil && atomic.LoadUint32(&d.started) == 1
}
// Stop ceases fetching and processing live data
func (d *dataChecker) Stop() error {
if d == nil {
return gctcommon.ErrNilPointer
}
if !atomic.CompareAndSwapUint32(&d.started, 1, 0) {
return engine.ErrSubSystemNotStarted
}
close(d.shutdown)
return nil
}
// SignalStopFromError ceases fetching and processing live data
func (d *dataChecker) SignalStopFromError(err error) error {
if err == nil {
return errNilError
}
if d == nil {
return gctcommon.ErrNilPointer
}
if !atomic.CompareAndSwapUint32(&d.started, 1, 0) {
return engine.ErrSubSystemNotStarted
}
log.Error(common.LiveStrategy, err)
d.shutdownErr <- true
return nil
}
// DataFetcher will fetch and append live data
func (d *dataChecker) DataFetcher() error {
if d == nil {
return fmt.Errorf("%w dataChecker", gctcommon.ErrNilPointer)
}
d.wg.Done()
if atomic.LoadUint32(&d.started) == 0 {
return engine.ErrSubSystemNotStarted
}
checkTimer := time.NewTimer(0)
timeoutTimer := time.NewTimer(d.eventTimeout)
for {
select {
case <-bt.shutdown:
case <-d.shutdown:
return nil
case <-timeoutTimer.C:
return errLiveDataTimeout
case <-processEventTicker.C:
for e := bt.EventQueue.NextEvent(); ; e = bt.EventQueue.NextEvent() {
if e == nil {
// as live only supports singular currency, just get the proper reference manually
var d data.Handler
dd := bt.Datas.GetAllData()
for k1, v1 := range dd {
for k2, v2 := range v1 {
for k3 := range v2 {
d = dd[k1][k2][k3]
}
}
}
de := d.Next()
if de == nil {
break
}
bt.EventQueue.AppendEvent(de)
doneARun = true
continue
}
err := bt.handleEvent(e)
if err != nil {
return err
}
}
if doneARun {
timeoutTimer = time.NewTimer(time.Minute * 5)
}
}
}
}
// loadLiveDataLoop is an incomplete function to continuously retrieve exchange data on a loop
// from live. Its purpose is to be able to perform strategy analysis against current data
func (bt *BackTest) loadLiveDataLoop(resp *kline.DataFromKline, cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, dataType int64) {
startDate := time.Now().Add(-cfg.DataSettings.Interval.Duration() * 2)
dates, err := gctkline.CalculateCandleDateRanges(
startDate,
startDate.AddDate(1, 0, 0),
cfg.DataSettings.Interval,
0)
if err != nil {
log.Errorf(common.Backtester, "%v. Please check your GoCryptoTrader configuration", err)
return
}
candles, err := live.LoadData(context.TODO(),
exch,
dataType,
cfg.DataSettings.Interval.Duration(),
fPair,
a)
if err != nil {
log.Errorf(common.Backtester, "%v. Please check your GoCryptoTrader configuration", err)
return
}
dates.SetHasDataFromCandles(candles.Candles)
resp.RangeHolder = dates
resp.Item = *candles
loadNewDataTimer := time.NewTimer(time.Second * 5)
for {
select {
case <-bt.shutdown:
return
case <-loadNewDataTimer.C:
log.Infof(common.Backtester, "Fetching data for %v %v %v %v", exch.GetName(), a, fPair, cfg.DataSettings.Interval)
loadNewDataTimer.Reset(time.Second * 15)
err = bt.loadLiveData(resp, cfg, exch, fPair, a, dataType)
return fmt.Errorf("%w of %v", ErrLiveDataTimeout, d.eventTimeout)
case <-checkTimer.C:
err := d.checkData()
if err != nil {
log.Error(common.Backtester, err)
return
return err
}
checkTimer.Reset(d.dataCheckInterval)
if !timeoutTimer.Stop() {
<-timeoutTimer.C
}
timeoutTimer.Reset(d.eventTimeout)
}
}
}
func (bt *BackTest) loadLiveData(resp *kline.DataFromKline, cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, dataType int64) error {
if resp == nil {
return errNilData
}
if cfg == nil {
return errNilConfig
}
if exch == nil {
return errNilExchange
}
candles, err := live.LoadData(context.TODO(),
exch,
dataType,
cfg.DataSettings.Interval.Duration(),
fPair,
a)
func (d *dataChecker) checkData() error {
hasDataUpdated, err := d.FetchLatestData()
if err != nil {
return err
}
if len(candles.Candles) == 0 {
if !hasDataUpdated {
return nil
}
resp.AppendResults(candles)
bt.Reports.UpdateItem(&resp.Item)
log.Info(common.Backtester, "Sleeping for 30 seconds before checking for new candle data")
d.dataUpdated <- hasDataUpdated
if d.realOrders {
go func() {
err = d.UpdateFunding(false)
if err != nil {
log.Errorf(common.LiveStrategy, "Could not update funding: %v", err)
}
}()
}
return nil
}
// UpdateFunding requests and updates funding levels
func (d *dataChecker) UpdateFunding(force bool) error {
switch {
case d == nil:
return fmt.Errorf("%w datachecker", gctcommon.ErrNilPointer)
case d.funding == nil:
return fmt.Errorf("%w datachecker funding manager", gctcommon.ErrNilPointer)
case force:
atomic.StoreUint32(&d.updatingFunding, 1)
case !atomic.CompareAndSwapUint32(&d.updatingFunding, 0, 1):
// already processing funding and can't go any faster
return nil
}
defer atomic.StoreUint32(&d.updatingFunding, 0)
var err error
if d.funding.HasFutures() {
err = d.funding.UpdateAllCollateral(d.realOrders, d.hasUpdatedFunding)
if err != nil {
return err
}
}
if d.realOrders {
// TODO: design a more sophisticated way of keeping funds up to date
// with current data type retrieval, this still functions appropriately
err = d.funding.UpdateFundingFromLiveData(d.hasUpdatedFunding)
if err != nil {
return err
}
}
if !d.hasUpdatedFunding {
d.hasUpdatedFunding = true
}
return nil
}
func closedChan() chan bool {
immediateClosure := make(chan bool)
close(immediateClosure)
return immediateClosure
}
// Updated gives other endpoints the ability to listen to
// when data is dataUpdated from live sources
func (d *dataChecker) Updated() chan bool {
if d == nil {
return closedChan()
}
return d.dataUpdated
}
// HasShutdown indicates when the live data checker
// has been shutdown
func (d *dataChecker) HasShutdown() chan bool {
if d == nil {
return closedChan()
}
return d.shutdown
}
// HasShutdownFromError indicates when the live data checker
// has been shutdown from encountering an error
func (d *dataChecker) HasShutdownFromError() chan bool {
if d == nil {
return closedChan()
}
return d.shutdownErr
}
// Reset clears all stored data
func (d *dataChecker) Reset() error {
if d == nil {
return gctcommon.ErrNilPointer
}
d.m.Lock()
defer d.m.Unlock()
d.wg = sync.WaitGroup{}
d.started = 0
d.updatingFunding = 0
d.verboseDataCheck = false
d.realOrders = false
d.hasUpdatedFunding = false
d.exchangeManager = nil
d.sourcesToCheck = nil
d.eventTimeout = 0
d.dataCheckInterval = 0
d.dataHolder = nil
d.report = nil
d.funding = nil
return nil
}
// AppendDataSource stores params to allow the datachecker to fetch and append live data
func (d *dataChecker) AppendDataSource(dataSource *liveDataSourceSetup) error {
if d == nil {
return fmt.Errorf("%w dataChecker", gctcommon.ErrNilPointer)
}
if dataSource == nil {
return fmt.Errorf("%w live data source", gctcommon.ErrNilPointer)
}
if dataSource.exchange == nil {
return fmt.Errorf("%w IBotExchange", gctcommon.ErrNilPointer)
}
if dataSource.dataType != common.DataCandle && dataSource.dataType != common.DataTrade {
return fmt.Errorf("%w '%v'", common.ErrInvalidDataType, dataSource.dataType)
}
if !dataSource.asset.IsValid() {
return fmt.Errorf("%w '%v'", asset.ErrNotSupported, dataSource.asset)
}
if dataSource.pair.IsEmpty() {
return fmt.Errorf("main %w", currency.ErrCurrencyPairEmpty)
}
if dataSource.interval.Duration() == 0 {
return gctkline.ErrUnsetInterval
}
d.m.Lock()
defer d.m.Unlock()
exchName := strings.ToLower(dataSource.exchange.GetName())
for i := range d.sourcesToCheck {
if d.sourcesToCheck[i].exchangeName == exchName &&
d.sourcesToCheck[i].asset == dataSource.asset &&
d.sourcesToCheck[i].pair.Equal(dataSource.pair) {
return fmt.Errorf("%w %v %v %v", errDataSourceExists, exchName, dataSource.asset, dataSource.pair)
}
}
k := kline.NewDataFromKline()
k.Item = gctkline.Item{
Exchange: exchName,
Pair: dataSource.pair,
UnderlyingPair: dataSource.underlyingPair,
Asset: dataSource.asset,
Interval: dataSource.interval,
}
err := k.SetLive(true)
if err != nil {
return err
}
if dataSource.dataRequestRetryTolerance <= 0 {
log.Warnf(common.LiveStrategy, "Invalid data retry tolerance, setting %v to %v", dataSource.dataRequestRetryTolerance, defaultDataRetryAttempts)
dataSource.dataRequestRetryTolerance = defaultDataRetryAttempts
}
if dataSource.dataRequestRetryWaitTime <= 0 {
log.Warnf(common.LiveStrategy, "Invalid data request wait time, setting %v to %v", dataSource.dataRequestRetryWaitTime, defaultDataRequestWaitTime)
dataSource.dataRequestRetryWaitTime = defaultDataRequestWaitTime
}
d.sourcesToCheck = append(d.sourcesToCheck, &liveDataSourceDataHandler{
exchange: dataSource.exchange,
exchangeName: exchName,
asset: dataSource.asset,
pair: dataSource.pair,
underlyingPair: dataSource.underlyingPair,
pairCandles: k,
dataType: dataSource.dataType,
processedData: make(map[int64]struct{}),
dataRequestRetryTolerance: dataSource.dataRequestRetryTolerance,
dataRequestRetryWaitTime: dataSource.dataRequestRetryWaitTime,
verboseExchangeRequest: dataSource.verboseExchangeRequest,
})
return nil
}
// FetchLatestData loads the latest data for all stored data sources
func (d *dataChecker) FetchLatestData() (bool, error) {
if d == nil {
return false, fmt.Errorf("%w dataChecker", gctcommon.ErrNilPointer)
}
if atomic.LoadUint32(&d.started) == 0 {
return false, engine.ErrSubSystemNotStarted
}
d.m.Lock()
defer d.m.Unlock()
var err error
results := make([]bool, len(d.sourcesToCheck))
// timeToRetrieve ensures consistent data retrieval
// in the event of a candle rollover mid-loop
timeToRetrieve := time.Now()
for i := range d.sourcesToCheck {
if d.verboseDataCheck {
log.Infof(common.LiveStrategy, "%v %v %v checking for new data", d.sourcesToCheck[i].exchangeName, d.sourcesToCheck[i].asset, d.sourcesToCheck[i].pair)
}
var updated bool
updated, err = d.sourcesToCheck[i].loadCandleData(timeToRetrieve)
if err != nil {
return false, err
}
results[i] = updated
}
for i := range results {
if !results[i] {
return false, nil
}
}
for i := range d.sourcesToCheck {
if d.verboseDataCheck {
log.Infof(common.LiveStrategy, "%v %v %v found new data", d.sourcesToCheck[i].exchangeName, d.sourcesToCheck[i].asset, d.sourcesToCheck[i].pair)
}
err = d.sourcesToCheck[i].pairCandles.AppendResults(d.sourcesToCheck[i].candlesToAppend)
if err != nil {
return false, err
}
d.sourcesToCheck[i].candlesToAppend.Candles = nil
err = d.dataHolder.SetDataForCurrency(d.sourcesToCheck[i].exchangeName, d.sourcesToCheck[i].asset, d.sourcesToCheck[i].pair, d.sourcesToCheck[i].pairCandles)
if err != nil {
return false, err
}
err = d.report.SetKlineData(&d.sourcesToCheck[i].pairCandles.Item)
if err != nil {
return false, err
}
err = d.funding.AddUSDTrackingData(d.sourcesToCheck[i].pairCandles)
if err != nil && !errors.Is(err, funding.ErrUSDTrackingDisabled) {
return false, err
}
}
if !d.hasUpdatedFunding {
err = d.UpdateFunding(false)
if err != nil {
if err != nil {
log.Error(common.LiveStrategy, err)
}
}
}
return true, nil
}
// SetDataForClosingAllPositions is triggered on a live data run
// when closing all positions on close is true.
// it will ensure all data is set such as USD tracking data
func (d *dataChecker) SetDataForClosingAllPositions(s ...signal.Event) error {
if d == nil {
return fmt.Errorf("%w dataChecker", gctcommon.ErrNilPointer)
}
if len(s) == 0 {
return fmt.Errorf("%w signal events", gctcommon.ErrNilPointer)
}
d.m.Lock()
defer d.m.Unlock()
var err error
setData := false
for x := range s {
if s[x] == nil {
return fmt.Errorf("%w signal events", errNilData)
}
for y := range d.sourcesToCheck {
if s[x].GetExchange() != d.sourcesToCheck[y].exchangeName ||
s[x].GetAssetType() != d.sourcesToCheck[y].asset ||
!s[x].Pair().Equal(d.sourcesToCheck[y].pair) {
continue
}
d.sourcesToCheck[y].pairCandles.Item.Candles = append(d.sourcesToCheck[y].pairCandles.Item.Candles, gctkline.Candle{
Time: s[x].GetTime(),
Open: s[x].GetOpenPrice().InexactFloat64(),
High: s[x].GetHighPrice().InexactFloat64(),
Low: s[x].GetLowPrice().InexactFloat64(),
Close: s[x].GetClosePrice().InexactFloat64(),
Volume: s[x].GetVolume().InexactFloat64(),
})
err = d.sourcesToCheck[y].pairCandles.AppendResults(&d.sourcesToCheck[y].pairCandles.Item)
if err != nil {
log.Errorf(common.LiveStrategy, "%v %v %v issue appending kline data: %v", d.sourcesToCheck[y].exchangeName, d.sourcesToCheck[y].asset, d.sourcesToCheck[y].pair, err)
continue
}
err = d.report.SetKlineData(&d.sourcesToCheck[y].pairCandles.Item)
if err != nil {
log.Errorf(common.LiveStrategy, "%v %v %v issue processing kline data: %v", d.sourcesToCheck[y].exchangeName, d.sourcesToCheck[y].asset, d.sourcesToCheck[y].pair, err)
continue
}
err = d.funding.AddUSDTrackingData(d.sourcesToCheck[y].pairCandles)
if err != nil && !errors.Is(err, funding.ErrUSDTrackingDisabled) {
log.Errorf(common.LiveStrategy, "%v %v %v issue processing USD tracking data: %v", d.sourcesToCheck[y].exchangeName, d.sourcesToCheck[y].asset, d.sourcesToCheck[y].pair, err)
continue
}
setData = true
}
}
if !setData {
return errNoDataSetForClosingPositions
}
return nil
}
// IsRealOrders is a quick check for if the strategy is using real orders
func (d *dataChecker) IsRealOrders() bool {
return d.realOrders
}
// loadCandleData fetches data from the exchange API and appends it
// to the candles to be added to the backtester event queue
func (c *liveDataSourceDataHandler) loadCandleData(timeToRetrieve time.Time) (bool, error) {
if c == nil {
return false, fmt.Errorf("%w live data source data handler", gctcommon.ErrNilPointer)
}
if c.pairCandles == nil {
return false, fmt.Errorf("%w pair candles", gctcommon.ErrNilPointer)
}
var candles *gctkline.Item
var err error
for i := int64(1); i <= c.dataRequestRetryTolerance; i++ {
candles, err = live.LoadData(context.TODO(),
timeToRetrieve,
c.exchange,
c.dataType,
c.pairCandles.Item.Interval.Duration(),
c.pair,
c.underlyingPair,
c.asset,
c.verboseExchangeRequest)
if err != nil {
if i < c.dataRequestRetryTolerance {
log.Errorf(common.Data, "%v %v %v failed to retrieve data %v of %v attempts: %v", c.exchangeName, c.asset, c.pair, i, c.dataRequestRetryTolerance, err)
continue
} else {
return false, err
}
}
break
}
if candles == nil {
return false, fmt.Errorf("%w kline Asset", gctcommon.ErrNilPointer)
}
if len(candles.Candles) == 0 {
return false, nil
}
unprocessedCandles := make([]gctkline.Candle, 0, len(candles.Candles))
for i := range candles.Candles {
if _, ok := c.processedData[candles.Candles[i].Time.UnixNano()]; !ok {
unprocessedCandles = append(unprocessedCandles, candles.Candles[i])
c.processedData[candles.Candles[i].Time.UnixNano()] = struct{}{}
}
}
if len(unprocessedCandles) > 0 {
if c.candlesToAppend == nil {
c.candlesToAppend = candles
}
c.candlesToAppend.Candles = append(c.candlesToAppend.Candles, unprocessedCandles...)
return true, nil
}
return false, nil
}

View File

@@ -2,59 +2,629 @@ package engine
import (
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
datakline "github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/backtester/report"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
func TestLoadLiveData(t *testing.T) {
func TestSetupLiveDataHandler(t *testing.T) {
t.Parallel()
err := loadLiveData(nil, nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
cfg := &config.Config{}
err = loadLiveData(cfg, nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
b := &gctexchange.Base{
Name: testExchange,
API: gctexchange.API{
CredentialsValidator: gctexchange.CredentialsValidator{
RequiresPEM: true,
RequiresKey: true,
RequiresSecret: true,
RequiresClientID: true,
RequiresBase64DecodeSecret: true,
},
},
bt := &BackTest{}
var err error
err = bt.SetupLiveDataHandler(-1, -1, false, false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
err = loadLiveData(cfg, b)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
cfg.DataSettings.LiveData = &config.LiveData{
RealOrders: true,
}
cfg.DataSettings.Interval = gctkline.OneDay
cfg.DataSettings.DataType = common.CandleStr
err = loadLiveData(cfg, b)
if err != nil {
t.Error(err)
bt.exchangeManager = engine.SetupExchangeManager()
err = bt.SetupLiveDataHandler(-1, -1, false, false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
cfg.DataSettings.LiveData.APIKeyOverride = "1234"
cfg.DataSettings.LiveData.APISecretOverride = "1234"
cfg.DataSettings.LiveData.APIClientIDOverride = "1234"
cfg.DataSettings.LiveData.API2FAOverride = "1234"
cfg.DataSettings.LiveData.APISubAccountOverride = "1234"
err = loadLiveData(cfg, b)
if err != nil {
t.Error(err)
bt.DataHolder = &data.HandlerHolder{}
err = bt.SetupLiveDataHandler(-1, -1, false, false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
bt.Reports = &report.Data{}
err = bt.SetupLiveDataHandler(-1, -1, false, false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
bt.Funding = &funding.FundManager{}
err = bt.SetupLiveDataHandler(-1, -1, false, false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
dc, ok := bt.LiveDataHandler.(*dataChecker)
if !ok {
t.Fatalf("received '%T' expected '%v'", dc, "dataChecker")
}
if dc.eventTimeout != defaultEventTimeout {
t.Errorf("received '%v' expected '%v'", dc.eventTimeout, defaultEventTimeout)
}
if dc.dataCheckInterval != defaultDataCheckInterval {
t.Errorf("received '%v' expected '%v'", dc.dataCheckInterval, defaultDataCheckInterval)
}
bt = nil
err = bt.SetupLiveDataHandler(-1, -1, false, false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestStart(t *testing.T) {
t.Parallel()
dc := &dataChecker{
shutdown: make(chan bool),
}
err := dc.Start()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
close(dc.shutdown)
dc.wg.Wait()
atomic.CompareAndSwapUint32(&dc.started, 0, 1)
err = dc.Start()
if !errors.Is(err, engine.ErrSubSystemAlreadyStarted) {
t.Errorf("received '%v' expected '%v'", err, engine.ErrSubSystemAlreadyStarted)
}
var dh *dataChecker
err = dh.Start()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestDataCheckerIsRunning(t *testing.T) {
t.Parallel()
dataHandler := &dataChecker{}
if dataHandler.IsRunning() {
t.Errorf("received '%v' expected '%v'", true, false)
}
dataHandler.started = 1
if !dataHandler.IsRunning() {
t.Errorf("received '%v' expected '%v'", false, true)
}
var dh *dataChecker
if dh.IsRunning() {
t.Errorf("received '%v' expected '%v'", true, false)
}
}
func TestLiveHandlerStop(t *testing.T) {
t.Parallel()
dc := &dataChecker{
shutdown: make(chan bool),
}
err := dc.Stop()
if !errors.Is(err, engine.ErrSubSystemNotStarted) {
t.Errorf("received '%v' expected '%v'", err, engine.ErrSubSystemNotStarted)
}
dc.started = 1
err = dc.Stop()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
dc.shutdown = make(chan bool)
err = dc.Stop()
if !errors.Is(err, engine.ErrSubSystemNotStarted) {
t.Errorf("received '%v' expected '%v'", err, engine.ErrSubSystemNotStarted)
}
var dh *dataChecker
err = dh.Stop()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestLiveHandlerStopFromError(t *testing.T) {
t.Parallel()
dc := &dataChecker{
shutdownErr: make(chan bool, 10),
}
err := dc.SignalStopFromError(errNoCredsNoLive)
if !errors.Is(err, engine.ErrSubSystemNotStarted) {
t.Errorf("received '%v' expected '%v'", err, engine.ErrSubSystemNotStarted)
}
err = dc.SignalStopFromError(nil)
if !errors.Is(err, errNilError) {
t.Errorf("received '%v' expected '%v'", err, errNilError)
}
dc.started = 1
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
err = dc.SignalStopFromError(errNoCredsNoLive)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}()
wg.Wait()
var dh *dataChecker
err = dh.SignalStopFromError(errNoCredsNoLive)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestDataFetcher(t *testing.T) {
t.Parallel()
dc := &dataChecker{
dataCheckInterval: time.Second,
eventTimeout: time.Millisecond,
shutdown: make(chan bool, 10),
shutdownErr: make(chan bool, 10),
dataUpdated: make(chan bool, 10),
}
dc.wg.Add(1)
err := dc.DataFetcher()
if !errors.Is(err, engine.ErrSubSystemNotStarted) {
t.Errorf("received '%v' expected '%v'", err, engine.ErrSubSystemNotStarted)
}
dc.started = 1
dc.wg.Add(1)
err = dc.DataFetcher()
if !errors.Is(err, ErrLiveDataTimeout) {
t.Errorf("received '%v' expected '%v'", err, ErrLiveDataTimeout)
}
var dh *dataChecker
err = dh.DataFetcher()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestUpdated(t *testing.T) {
t.Parallel()
dc := &dataChecker{
dataUpdated: make(chan bool, 10),
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
_ = dc.Updated()
wg.Done()
}()
wg.Wait()
dc = nil
wg.Add(1)
go func() {
_ = dc.Updated()
wg.Done()
}()
wg.Wait()
}
func TestLiveHandlerReset(t *testing.T) {
t.Parallel()
dataHandler := &dataChecker{
eventTimeout: 1,
}
err := dataHandler.Reset()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if dataHandler.eventTimeout != 0 {
t.Errorf("received '%v' expected '%v'", dataHandler.eventTimeout, 0)
}
var dh *dataChecker
err = dh.Reset()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestAppendDataSource(t *testing.T) {
t.Parallel()
dataHandler := &dataChecker{}
err := dataHandler.AppendDataSource(nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
setup := &liveDataSourceSetup{}
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
setup.exchange = &binance.Binance{}
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, common.ErrInvalidDataType) {
t.Errorf("received '%v' expected '%v'", err, common.ErrInvalidDataType)
}
setup.dataType = common.DataCandle
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received '%v' expected '%v'", err, asset.ErrNotSupported)
}
setup.asset = asset.Spot
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, currency.ErrCurrencyPairEmpty) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrCurrencyPairEmpty)
}
setup.pair = currency.NewPair(currency.BTC, currency.USDT)
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, kline.ErrUnsetInterval) {
t.Errorf("received '%v' expected '%v'", err, kline.ErrUnsetInterval)
}
setup.interval = kline.OneDay
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(dataHandler.sourcesToCheck) != 1 {
t.Errorf("received '%v' expected '%v'", len(dataHandler.sourcesToCheck), 1)
}
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, errDataSourceExists) {
t.Errorf("received '%v' expected '%v'", err, errDataSourceExists)
}
dataHandler = nil
err = dataHandler.AppendDataSource(setup)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestFetchLatestData(t *testing.T) {
t.Parallel()
dataHandler := &dataChecker{
report: &report.Data{},
funding: &fakeFunding{},
}
_, err := dataHandler.FetchLatestData()
if !errors.Is(err, engine.ErrSubSystemNotStarted) {
t.Errorf("received '%v' expected '%v'", err, engine.ErrSubSystemNotStarted)
}
dataHandler.started = 1
_, err = dataHandler.FetchLatestData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
cp := currency.NewPair(currency.BTC, currency.USDT).Format(
currency.PairFormat{
Uppercase: true,
})
f := &binance.Binance{}
f.SetDefaults()
fb := f.GetBase()
fbA := fb.CurrencyPairs.Pairs[asset.Spot]
fbA.Enabled = fbA.Enabled.Add(cp)
fbA.Available = fbA.Available.Add(cp)
dataHandler.sourcesToCheck = []*liveDataSourceDataHandler{
{
exchange: f,
exchangeName: testExchange,
asset: asset.Spot,
pair: cp,
dataRequestRetryWaitTime: defaultDataRequestWaitTime,
dataRequestRetryTolerance: 1,
underlyingPair: cp,
pairCandles: &datakline.DataFromKline{
Base: &data.Base{},
Item: kline.Item{
Exchange: testExchange,
Pair: cp,
UnderlyingPair: cp,
Asset: asset.Spot,
Interval: kline.OneHour,
Candles: []kline.Candle{
{
Time: time.Now(),
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
},
},
},
},
dataType: common.DataCandle,
processedData: make(map[int64]struct{}),
},
}
dataHandler.dataHolder = &fakeDataHolder{}
_, err = dataHandler.FetchLatestData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
var dh *dataChecker
_, err = dh.FetchLatestData()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestLoadCandleData(t *testing.T) {
t.Parallel()
l := &liveDataSourceDataHandler{
dataRequestRetryTolerance: 1,
dataRequestRetryWaitTime: defaultDataRequestWaitTime,
processedData: make(map[int64]struct{}),
}
_, err := l.loadCandleData(time.Now())
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
exch := &binance.Binance{}
exch.SetDefaults()
cp := currency.NewPair(currency.BTC, currency.USDT).Format(
currency.PairFormat{
Uppercase: true,
})
eba := exch.CurrencyPairs.Pairs[asset.Spot]
eba.Available = eba.Available.Add(cp)
eba.Enabled = eba.Enabled.Add(cp)
eba.AssetEnabled = convert.BoolPtr(true)
l.exchange = exch
l.dataType = common.DataCandle
l.asset = asset.Spot
l.pair = cp
l.pairCandles = &datakline.DataFromKline{
Base: &data.Base{},
Item: kline.Item{
Exchange: testExchange,
Asset: asset.Spot,
Pair: cp,
UnderlyingPair: cp,
Interval: kline.OneHour,
},
}
updated, err := l.loadCandleData(time.Now())
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if !updated {
t.Errorf("received '%v' expected '%v'", updated, true)
}
var ldh *liveDataSourceDataHandler
_, err = ldh.loadCandleData(time.Now())
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestSetDataForClosingAllPositions(t *testing.T) {
t.Parallel()
dataHandler := &dataChecker{
report: &fakeReport{},
funding: &fakeFunding{},
}
dataHandler.started = 1
cp := currency.NewPair(currency.BTC, currency.USDT).Format(
currency.PairFormat{
Uppercase: true,
})
f := &binance.Binance{}
f.SetDefaults()
fb := f.GetBase()
fbA := fb.CurrencyPairs.Pairs[asset.Spot]
fbA.Enabled = fbA.Enabled.Add(cp)
fbA.Available = fbA.Available.Add(cp)
dataHandler.sourcesToCheck = []*liveDataSourceDataHandler{
{
exchange: f,
exchangeName: testExchange,
asset: asset.Spot,
pair: cp,
dataRequestRetryWaitTime: defaultDataRequestWaitTime,
dataRequestRetryTolerance: 1,
underlyingPair: cp,
pairCandles: &datakline.DataFromKline{
Base: &data.Base{},
Item: kline.Item{
Exchange: testExchange,
Pair: cp,
UnderlyingPair: cp,
Asset: asset.Spot,
Interval: kline.OneHour,
Candles: []kline.Candle{
{
Time: time.Now(),
Open: 1337,
High: 1337,
Low: 1337,
Close: 1337,
Volume: 1337,
},
},
},
},
dataType: common.DataCandle,
processedData: make(map[int64]struct{}),
},
}
dataHandler.dataHolder = &fakeDataHolder{}
_, err := dataHandler.FetchLatestData()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = dataHandler.SetDataForClosingAllPositions()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
err = dataHandler.SetDataForClosingAllPositions(nil)
if !errors.Is(err, errNilData) {
t.Errorf("received '%v' expected '%v'", err, errNilData)
}
err = dataHandler.SetDataForClosingAllPositions(&signal.Signal{
Base: &event.Base{
Offset: 3,
Exchange: testExchange,
Time: time.Now(),
Interval: kline.OneHour,
CurrencyPair: cp,
UnderlyingPair: cp,
AssetType: asset.Spot,
},
OpenPrice: leet,
HighPrice: leet,
LowPrice: leet,
ClosePrice: leet,
Volume: leet,
BuyLimit: leet,
SellLimit: leet,
Amount: leet,
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = dataHandler.SetDataForClosingAllPositions(&signal.Signal{
Base: &event.Base{
Offset: 4,
Exchange: testExchange,
Time: time.Now(),
Interval: kline.OneHour,
CurrencyPair: cp,
UnderlyingPair: cp,
AssetType: asset.Spot,
},
OpenPrice: leet,
HighPrice: leet,
LowPrice: leet,
ClosePrice: leet,
Volume: leet,
BuyLimit: leet,
SellLimit: leet,
Amount: leet,
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
dataHandler = nil
err = dataHandler.SetDataForClosingAllPositions()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestIsRealOrders(t *testing.T) {
t.Parallel()
d := &dataChecker{}
if d.IsRealOrders() {
t.Error("expected false")
}
d.realOrders = true
if !d.IsRealOrders() {
t.Error("expected true")
}
}
func TestUpdateFunding(t *testing.T) {
t.Parallel()
d := &dataChecker{}
err := d.UpdateFunding(false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
ff := &fakeFunding{}
d.funding = ff
err = d.UpdateFunding(false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = d.UpdateFunding(true)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
d.realOrders = true
err = d.UpdateFunding(true)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
ff.hasFutures = true
err = d.UpdateFunding(true)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
d.updatingFunding = 1
err = d.UpdateFunding(true)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
d.updatingFunding = 1
err = d.UpdateFunding(false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
d = nil
err = d.UpdateFunding(false)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestClosedChan(t *testing.T) {
t.Parallel()
chantel := closedChan()
if chantel == nil {
t.Errorf("expected channel, received %v", nil)
}
<-chantel
// demonstrate nil channel still functions on a select case
chantel = nil
select {
case <-chantel:
t.Error("woah")
default:
}
}

View File

@@ -0,0 +1,107 @@
package engine
import (
"errors"
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/backtester/report"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
var (
// ErrLiveDataTimeout returns when an event has not been processed within the timeframe
ErrLiveDataTimeout = errors.New("no data processed within timeframe")
errDataSourceExists = errors.New("data source already exists")
errInvalidCredentials = errors.New("credentials are invalid, please check your config")
errNoCredsNoLive = errors.New("cannot use real orders without credentials to fulfil those real orders")
errNoDataSetForClosingPositions = errors.New("no data was set for closing positions")
errNilError = errors.New("nil error received when expecting an error")
)
var (
defaultEventTimeout = time.Minute
defaultDataCheckInterval = time.Second
defaultDataRetryAttempts int64 = 1
defaultDataRequestWaitTime = time.Millisecond * 500
)
// Handler is all the functionality required in order to
// run a backtester with live data
type Handler interface {
AppendDataSource(*liveDataSourceSetup) error
FetchLatestData() (bool, error)
Start() error
IsRunning() bool
DataFetcher() error
Stop() error
Reset() error
Updated() chan bool
HasShutdown() chan bool
HasShutdownFromError() chan bool
SetDataForClosingAllPositions(events ...signal.Event) error
UpdateFunding(force bool) error
IsRealOrders() bool
}
// dataChecker is responsible for managing all data retrieval
// for a live data option
type dataChecker struct {
m sync.Mutex
wg sync.WaitGroup
started uint32
updatingFunding uint32
verboseDataCheck bool
realOrders bool
hasUpdatedFunding bool
exchangeManager *engine.ExchangeManager
sourcesToCheck []*liveDataSourceDataHandler
eventTimeout time.Duration
dataCheckInterval time.Duration
dataHolder data.Holder
shutdownErr chan bool
shutdown chan bool
dataUpdated chan bool
report report.Handler
funding funding.IFundingManager
}
// liveDataSourceSetup is used to add new data sources
// to retrieve live data
type liveDataSourceSetup struct {
exchange gctexchange.IBotExchange
interval gctkline.Interval
asset asset.Item
pair currency.Pair
underlyingPair currency.Pair
dataType int64
dataRequestRetryTolerance int64
dataRequestRetryWaitTime time.Duration
verboseExchangeRequest bool
}
// liveDataSourceDataHandler is used to collect
// and store live data
type liveDataSourceDataHandler struct {
exchange gctexchange.IBotExchange
exchangeName string
asset asset.Item
pair currency.Pair
underlyingPair currency.Pair
dataType int64
pairCandles *kline.DataFromKline
processedData map[int64]struct{}
candlesToAppend *gctkline.Item
dataRequestRetryTolerance int64
dataRequestRetryWaitTime time.Duration
verboseExchangeRequest bool
}

View File

@@ -1,214 +0,0 @@
package engine
import (
"errors"
"fmt"
"github.com/gofrs/uuid"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
)
var (
errRunNotFound = errors.New("run not found")
errRunAlreadyMonitored = errors.New("run already monitored")
errAlreadyRan = errors.New("run already ran")
errRunHasNotRan = errors.New("run hasn't ran yet")
errRunIsRunning = errors.New("run is already running")
errCannotClear = errors.New("cannot clear run")
)
// SetupRunManager creates a run manager to allow the backtester to manage multiple strategies
func SetupRunManager() *RunManager {
return &RunManager{}
}
// AddRun adds a run to the manager
func (r *RunManager) AddRun(b *BackTest) error {
if r == nil {
return fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
if b == nil {
return fmt.Errorf("%w BackTest", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.runs {
if r.runs[i].Equal(b) {
return fmt.Errorf("%w %s %s", errRunAlreadyMonitored, b.MetaData.ID, b.MetaData.Strategy)
}
}
err := b.SetupMetaData()
if err != nil {
return err
}
r.runs = append(r.runs, b)
return nil
}
// List details all backtesting/livestrategy runs
func (r *RunManager) List() ([]*RunSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
resp := make([]*RunSummary, len(r.runs))
for i := range r.runs {
sum, err := r.runs[i].GenerateSummary()
if err != nil {
return nil, err
}
resp[i] = sum
}
return resp, nil
}
// GetSummary returns details about a completed backtesting/livestrategy run
func (r *RunManager) GetSummary(id uuid.UUID) (*RunSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.runs {
if !r.runs[i].MatchesID(id) {
continue
}
return r.runs[i].GenerateSummary()
}
return nil, fmt.Errorf("%s %w", id, errRunNotFound)
}
// StopRun stops a backtesting/livestrategy run if enabled, this will run CloseAllPositions
func (r *RunManager) StopRun(id uuid.UUID) error {
if r == nil {
return fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.runs {
switch {
case !r.runs[i].MatchesID(id):
continue
case r.runs[i].IsRunning():
r.runs[i].Stop()
return nil
case r.runs[i].HasRan():
return fmt.Errorf("%w %v", errAlreadyRan, id)
default:
return fmt.Errorf("%w %v", errRunHasNotRan, id)
}
}
return fmt.Errorf("%s %w", id, errRunNotFound)
}
// StopAllRuns stops all running strategies
func (r *RunManager) StopAllRuns() ([]*RunSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
resp := make([]*RunSummary, 0, len(r.runs))
for i := range r.runs {
if !r.runs[i].IsRunning() {
continue
}
r.runs[i].Stop()
sum, err := r.runs[i].GenerateSummary()
if err != nil {
return nil, err
}
resp = append(resp, sum)
}
return resp, nil
}
// StartRun executes a strategy if found
func (r *RunManager) StartRun(id uuid.UUID) error {
if r == nil {
return fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.runs {
switch {
case !r.runs[i].MatchesID(id):
continue
case r.runs[i].IsRunning():
return fmt.Errorf("%w %v", errRunIsRunning, id)
case r.runs[i].HasRan():
return fmt.Errorf("%w %v", errAlreadyRan, id)
default:
return r.runs[i].ExecuteStrategy(false)
}
}
return fmt.Errorf("%s %w", id, errRunNotFound)
}
// StartAllRuns executes all strategies
func (r *RunManager) StartAllRuns() ([]uuid.UUID, error) {
if r == nil {
return nil, fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
executedRuns := make([]uuid.UUID, 0, len(r.runs))
for i := range r.runs {
if r.runs[i].HasRan() {
continue
}
executedRuns = append(executedRuns, r.runs[i].MetaData.ID)
err := r.runs[i].ExecuteStrategy(false)
if err != nil {
return nil, err
}
}
return executedRuns, nil
}
// ClearRun removes a run from memory
func (r *RunManager) ClearRun(id uuid.UUID) error {
if r == nil {
return fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.runs {
if !r.runs[i].MatchesID(id) {
continue
}
if r.runs[i].IsRunning() {
return fmt.Errorf("%w %v, currently running. Stop it first", errCannotClear, r.runs[i].MetaData.ID)
}
r.runs = append(r.runs[:i], r.runs[i+1:]...)
return nil
}
return fmt.Errorf("%s %w", id, errRunNotFound)
}
// ClearAllRuns removes all runs from memory
func (r *RunManager) ClearAllRuns() (clearedRuns, remainingRuns []*RunSummary, err error) {
if r == nil {
return nil, nil, fmt.Errorf("%w RunManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := 0; i < len(r.runs); i++ {
var run *RunSummary
run, err = r.runs[i].GenerateSummary()
if err != nil {
return nil, nil, err
}
if r.runs[i].IsRunning() {
remainingRuns = append(remainingRuns, run)
} else {
clearedRuns = append(clearedRuns, run)
r.runs = append(r.runs[:i], r.runs[i+1:]...)
i--
}
}
return clearedRuns, remainingRuns, nil
}

View File

@@ -8,15 +8,16 @@ import (
"runtime"
"strings"
"sync"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/api"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/csv"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/database"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange/slippage"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
@@ -37,45 +38,57 @@ import (
"github.com/thrasher-corp/gocryptotrader/engine"
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
// NewFromConfig takes a strategy config and configures a backtester variable to run
func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool) (*BackTest, error) {
log.Infoln(common.Setup, "Loading config...")
if cfg == nil {
return nil, errNilConfig
// NewBacktester returns a new BackTest instance
func NewBacktester() (*BackTest, error) {
bt := &BackTest{
shutdown: make(chan struct{}),
DataHolder: &data.HandlerHolder{},
EventQueue: &eventholder.Holder{},
hasProcessedDataAtOffset: make(map[int64]bool),
}
bt, err := New()
err := bt.SetupMetaData()
if err != nil {
return nil, err
}
bt.exchangeManager = engine.SetupExchangeManager()
bt.orderManager, err = engine.SetupOrderManager(bt.exchangeManager, &engine.CommunicationManager{}, &sync.WaitGroup{}, false, false, 0)
if err != nil {
return nil, err
return bt, nil
}
// SetupFromConfig takes a strategy config and configures a backtester variable to run
func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output string, verbose bool) error {
var err error
defer func() {
if err != nil {
log.Errorf(common.Backtester, "Could not setup backtester %v: %v", cfg.Nickname, err)
}
}()
log.Infoln(common.Setup, "Loading config...")
if cfg == nil {
return errNilConfig
}
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
return err
}
}
bt.verbose = verbose
bt.DataHolder = data.NewHandlerHolder()
reports := &report.Data{
Config: cfg,
TemplatePath: templatePath,
OutputPath: output,
}
bt.Reports = reports
buyRule := exchange.MinMax{
MinimumSize: cfg.PortfolioSettings.BuySide.MinimumSize,
MaximumSize: cfg.PortfolioSettings.BuySide.MaximumSize,
@@ -95,12 +108,13 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
bt.exchangeManager,
cfg.FundingSettings.UseExchangeLevelFunding,
cfg.StrategySettings.DisableUSDTracking,
bt.verbose,
)
if err != nil {
return nil, err
return err
}
if cfg.FundingSettings.UseExchangeLevelFunding {
if cfg.FundingSettings.UseExchangeLevelFunding && !(cfg.DataSettings.LiveData != nil && cfg.DataSettings.LiveData.RealOrders) {
for i := range cfg.FundingSettings.ExchangeLevelFunding {
a := cfg.FundingSettings.ExchangeLevelFunding[i].Asset
cq := cfg.FundingSettings.ExchangeLevelFunding[i].Currency
@@ -111,71 +125,101 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
cfg.FundingSettings.ExchangeLevelFunding[i].InitialFunds,
cfg.FundingSettings.ExchangeLevelFunding[i].TransferFee)
if err != nil {
return nil, err
return err
}
err = funds.AddItem(item)
if err != nil {
return nil, err
return err
}
}
}
var emm = make(map[string]gctexchange.IBotExchange)
for i := range cfg.CurrencySettings {
_, ok := emm[cfg.CurrencySettings[i].ExchangeName]
if ok {
continue
}
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.NewExchangeByName(cfg.CurrencySettings[i].ExchangeName)
exch, err = bt.exchangeManager.GetExchangeByName(cfg.CurrencySettings[i].ExchangeName)
if err != nil {
return nil, err
}
var conf *gctconfig.Exchange
conf, err = exch.GetDefaultConfig()
if err != nil {
return nil, err
}
conf.Enabled = true
conf.WebsocketTrafficTimeout = time.Second
conf.Websocket = convert.BoolPtr(false)
conf.WebsocketResponseCheckTimeout = time.Second
conf.WebsocketResponseMaxLimit = time.Second
conf.Verbose = verbose
err = exch.Setup(conf)
if err != nil {
return nil, err
if errors.Is(err, engine.ErrExchangeNotFound) {
exch, err = bt.exchangeManager.NewExchangeByName(cfg.CurrencySettings[i].ExchangeName)
if err != nil {
return err
}
exch.SetDefaults()
exchBase := exch.GetBase()
exchBase.Verbose = cfg.DataSettings.VerboseExchangeRequests
exchBase.Config = &gctconfig.Exchange{
Name: exchBase.Name,
HTTPTimeout: gctexchange.DefaultHTTPTimeout,
BaseCurrencies: exchBase.BaseCurrencies,
CurrencyPairs: &currency.PairsManager{},
}
err = exch.UpdateTradablePairs(context.TODO(), true)
if err != nil {
return err
}
if cfg.DataSettings.LiveData != nil && cfg.DataSettings.LiveData.RealOrders {
exchBase.States = currencystate.NewCurrencyStates()
}
if cfg.CurrencySettings[i].CanUseExchangeLimits || (cfg.DataSettings.LiveData != nil && cfg.DataSettings.LiveData.RealOrders) {
err = exch.UpdateOrderExecutionLimits(context.TODO(), cfg.CurrencySettings[i].Asset)
if err != nil && !errors.Is(err, gctcommon.ErrNotYetImplemented) {
return err
}
}
bt.exchangeManager.Add(exch)
} else {
return err
}
}
exchBase := exch.GetBase()
err = exch.UpdateTradablePairs(context.Background(), true)
if err != nil {
return nil, err
exchangeAsset, ok := exchBase.CurrencyPairs.Pairs[cfg.CurrencySettings[i].Asset]
if !ok {
return fmt.Errorf("%v %v %w", cfg.CurrencySettings[i].ExchangeName, cfg.CurrencySettings[i].Asset, asset.ErrNotSupported)
}
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
exchangeAsset.AssetEnabled = convert.BoolPtr(true)
cp := currency.NewPair(cfg.CurrencySettings[i].Base, cfg.CurrencySettings[i].Quote).Format(*exchangeAsset.RequestFormat)
exchangeAsset.Available = exchangeAsset.Available.Add(cp)
exchangeAsset.Enabled = exchangeAsset.Enabled.Add(cp)
exchBase.Verbose = verbose
exchBase.CurrencyPairs.Pairs[cfg.CurrencySettings[i].Asset] = exchangeAsset
}
portfolioRisk := &risk.Risk{
CurrencySettings: make(map[string]map[asset.Item]map[currency.Pair]*risk.CurrencySettings),
CurrencySettings: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*risk.CurrencySettings),
}
bt.Funding = funds
var trackFuturesPositions bool
if cfg.DataSettings.LiveData != nil {
trackFuturesPositions = cfg.DataSettings.LiveData.RealOrders
err = bt.SetupLiveDataHandler(cfg.DataSettings.LiveData.NewEventTimeout, cfg.DataSettings.LiveData.DataCheckTimer, cfg.DataSettings.LiveData.RealOrders, verbose)
if err != nil {
return err
}
}
bt.orderManager, err = engine.SetupOrderManager(
bt.exchangeManager,
&engine.CommunicationManager{},
&sync.WaitGroup{},
verbose,
trackFuturesPositions,
0)
if err != nil {
return err
}
err = bt.orderManager.Start()
if err != nil {
return err
}
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)
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*risk.CurrencySettings)
}
a := cfg.CurrencySettings[i].Asset
if !a.IsValid() {
return nil, fmt.Errorf(
return fmt.Errorf(
"%w for %v %v %v-%v. Err %v",
asset.ErrNotSupported,
cfg.CurrencySettings[i].ExchangeName,
@@ -185,46 +229,28 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
err)
}
if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] == nil {
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] = make(map[currency.Pair]*risk.CurrencySettings)
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] = make(map[*currency.Item]map[*currency.Item]*risk.CurrencySettings)
}
if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][cfg.CurrencySettings[i].Base.Item] == nil {
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][cfg.CurrencySettings[i].Base.Item] = make(map[*currency.Item]*risk.CurrencySettings)
}
var curr currency.Pair
var b, q currency.Code
b = cfg.CurrencySettings[i].Base
q = cfg.CurrencySettings[i].Quote
curr = currency.NewPair(b, q)
curr = currency.NewPair(b, q).Format(currency.EMPTYFORMAT)
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.GetExchangeByName(cfg.CurrencySettings[i].ExchangeName)
if err != nil {
return nil, err
return err
}
exchBase := exch.GetBase()
var requestFormat currency.PairFormat
requestFormat, err = exchBase.GetPairFormat(a, true)
if err != nil {
return nil, fmt.Errorf("could not get pair format %v, %w", curr, err)
if cfg.DataSettings.LiveData != nil && cfg.DataSettings.LiveData.RealOrders {
exchBase := exch.GetBase()
err = setExchangeCredentials(cfg, exchBase)
if err != nil {
return err
}
}
curr = curr.Format(requestFormat)
var avail, enabled currency.Pairs
avail, err = exch.GetAvailablePairs(a)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
enabled, err = exch.GetEnabledPairs(a)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
avail = avail.Add(curr)
enabled = enabled.Add(curr)
err = exch.SetPairs(enabled, a, true)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
err = exch.SetPairs(avail, a, false)
if err != nil {
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
}
portSet := &risk.CurrencySettings{
MaximumHoldingRatio: cfg.CurrencySettings[i].MaximumHoldingsRatio,
}
@@ -232,7 +258,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
portSet.MaximumOrdersWithLeverageRatio = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio
portSet.MaxLeverageRate = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate
}
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][curr] = portSet
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][curr.Base.Item][curr.Quote.Item] = portSet
if cfg.CurrencySettings[i].MakerFee != nil &&
cfg.CurrencySettings[i].TakerFee != nil &&
cfg.CurrencySettings[i].MakerFee.GreaterThan(*cfg.CurrencySettings[i].TakerFee) {
@@ -242,7 +268,8 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
}
var baseItem, quoteItem, futureItem *funding.Item
if cfg.FundingSettings.UseExchangeLevelFunding {
switch {
case cfg.FundingSettings.UseExchangeLevelFunding:
switch {
case a == asset.Spot:
// add any remaining currency items that have no funding data in the strategy config
@@ -252,7 +279,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
decimal.Zero,
decimal.Zero)
if err != nil {
return nil, err
return err
}
quoteItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
a,
@@ -260,15 +287,15 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
decimal.Zero,
decimal.Zero)
if err != nil {
return nil, err
return err
}
err = funds.AddItem(baseItem)
if err != nil && !errors.Is(err, funding.ErrAlreadyExists) {
return nil, err
return err
}
err = funds.AddItem(quoteItem)
if err != nil && !errors.Is(err, funding.ErrAlreadyExists) {
return nil, err
return err
}
case a.IsFutures():
// setup contract items
@@ -279,27 +306,27 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
decimal.Zero,
decimal.Zero)
if err != nil {
return nil, err
return err
}
var collateralCurrency currency.Code
collateralCurrency, _, err = exch.GetCollateralCurrencyForContract(a, currency.NewPair(b, q))
if err != nil {
return nil, err
return err
}
err = funds.LinkCollateralCurrency(futureItem, collateralCurrency)
if err != nil {
return nil, err
return err
}
err = funds.AddItem(futureItem)
if err != nil {
return nil, err
return err
}
default:
return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
return fmt.Errorf("%w: %v", asset.ErrNotSupported, a)
}
} else {
default:
var bFunds, qFunds decimal.Decimal
if cfg.CurrencySettings[i].SpotDetails != nil {
if cfg.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
@@ -316,7 +343,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
bFunds,
decimal.Zero)
if err != nil {
return nil, err
return err
}
quoteItem, err = funding.CreateItem(
cfg.CurrencySettings[i].ExchangeName,
@@ -325,30 +352,29 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
qFunds,
decimal.Zero)
if err != nil {
return nil, err
return err
}
var pair *funding.SpotPair
pair, err = funding.CreatePair(baseItem, quoteItem)
if err != nil {
return nil, err
return err
}
err = funds.AddPair(pair)
if err != nil {
return nil, err
return err
}
}
}
bt.Funding = funds
var p *portfolio.Portfolio
p, err = portfolio.Setup(sizeManager, portfolioRisk, cfg.StatisticSettings.RiskFreeRate)
if err != nil {
return nil, err
return err
}
bt.Strategy, err = strategies.LoadStrategyByName(cfg.StrategySettings.Name, cfg.StrategySettings.SimultaneousSignalProcessing)
if err != nil {
return nil, err
return err
}
bt.MetaData.Strategy = bt.Strategy.Name()
bt.Strategy.SetDefaults()
@@ -356,7 +382,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
if cfg.StrategySettings.CustomSettings != nil {
err = bt.Strategy.SetCustomSettings(cfg.StrategySettings.CustomSettings)
if err != nil && !errors.Is(err, base.ErrCustomSettingsUnsupported) {
return nil, err
return err
}
}
stats := &statistics.Statistic{
@@ -364,7 +390,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
StrategyNickname: cfg.Nickname,
StrategyDescription: bt.Strategy.Description(),
StrategyGoal: cfg.Goal,
ExchangeAssetPairStatistics: make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic),
ExchangeAssetPairStatistics: make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*statistics.CurrencyPairStatistic),
RiskFreeRate: cfg.StatisticSettings.RiskFreeRate,
CandleInterval: cfg.DataSettings.Interval,
FundManager: bt.Funding,
@@ -384,7 +410,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
}
trackingPairs, err = trackingcurrencies.CreateUSDTrackingPairs(trackingPairs, bt.exchangeManager)
if err != nil {
return nil, err
return err
}
trackingPairCheck:
for i := range trackingPairs {
@@ -403,44 +429,55 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
Quote: trackingPairs[i].Quote,
USDTrackingPair: true,
})
// ensure new tracking pairs are enabled
var exch gctexchange.IBotExchange
exch, err = bt.exchangeManager.GetExchangeByName(trackingPairs[i].Exchange)
if err != nil {
return err
}
exchBase := exch.GetBase()
exchangeAsset := exchBase.CurrencyPairs.Pairs[trackingPairs[i].Asset] // no ok as handled earlier
exchangeAsset.Enabled = exchangeAsset.Enabled.Add(currency.NewPair(trackingPairs[i].Base, trackingPairs[i].Quote))
}
}
e, err := bt.setupExchangeSettings(cfg)
if err != nil {
return nil, err
return err
}
bt.Exchange = &e
bt.Exchange = e
for i := range e.CurrencySettings {
err = p.SetupCurrencySettingsMap(&e.CurrencySettings[i])
err = p.SetCurrencySettingsMap(&e.CurrencySettings[i])
if err != nil {
return nil, err
return err
}
}
bt.Portfolio = p
hasFunding := false
fundingItems := funds.GetAllFunding()
fundingItems, err := funds.GetAllFunding()
if err != nil {
return err
}
for i := range fundingItems {
if fundingItems[i].InitialFunds.IsPositive() {
hasFunding = true
break
}
}
if !hasFunding {
return nil, holdings.ErrInitialFundsZero
if !hasFunding && !(cfg.DataSettings.LiveData != nil && cfg.DataSettings.LiveData.RealOrders) {
return holdings.ErrInitialFundsZero
}
cfg.PrintSetting()
return bt, nil
return nil
}
func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange, error) {
func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (*exchange.Exchange, error) {
log.Infoln(common.Setup, "Setting exchange settings...")
resp := exchange.Exchange{}
resp := &exchange.Exchange{}
for i := range cfg.CurrencySettings {
exch, pair, a, err := bt.loadExchangePairAssetBase(
cfg.CurrencySettings[i].ExchangeName,
@@ -448,29 +485,31 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
cfg.CurrencySettings[i].Quote,
cfg.CurrencySettings[i].Asset)
if err != nil {
return resp, err
return nil, err
}
exchangeName := strings.ToLower(exch.GetName())
bt.Datas.Setup()
klineData, err := bt.loadData(cfg, exch, pair, a, cfg.CurrencySettings[i].USDTrackingPair)
if err != nil {
return resp, err
return nil, err
}
if bt.LiveDataHandler == nil {
err = bt.Funding.AddUSDTrackingData(klineData)
if err != nil &&
!errors.Is(err, trackingcurrencies.ErrCurrencyDoesNotContainsUSD) &&
!errors.Is(err, funding.ErrUSDTrackingDisabled) {
return nil, err
}
err = bt.Funding.AddUSDTrackingData(klineData)
if err != nil &&
!errors.Is(err, trackingcurrencies.ErrCurrencyDoesNotContainsUSD) &&
!errors.Is(err, funding.ErrUSDTrackingDisabled) {
return resp, err
if cfg.CurrencySettings[i].USDTrackingPair {
continue
}
err = bt.DataHolder.SetDataForCurrency(exchangeName, a, pair, klineData)
if err != nil {
return nil, err
}
}
if cfg.CurrencySettings[i].USDTrackingPair {
continue
}
bt.Datas.SetDataForCurrency(exchangeName, a, pair, klineData)
var makerFee, takerFee decimal.Decimal
if cfg.CurrencySettings[i].MakerFee != nil && cfg.CurrencySettings[i].MakerFee.GreaterThan(decimal.Zero) {
makerFee = *cfg.CurrencySettings[i].MakerFee
@@ -480,7 +519,10 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
}
if cfg.CurrencySettings[i].TakerFee == nil || cfg.CurrencySettings[i].MakerFee == nil {
var apiMakerFee, apiTakerFee decimal.Decimal
apiMakerFee, apiTakerFee = getFees(context.TODO(), exch, pair)
apiMakerFee, apiTakerFee, err = getFees(context.TODO(), exch, pair)
if err != nil {
log.Errorf(common.Setup, "Could not retrieve fees for %v. %v", exch.GetName(), err)
}
if cfg.CurrencySettings[i].MakerFee == nil {
makerFee = apiMakerFee
cfg.CurrencySettings[i].MakerFee = &makerFee
@@ -520,6 +562,7 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
realOrders = cfg.DataSettings.LiveData.RealOrders
bt.MetaData.LiveTesting = true
bt.MetaData.RealOrders = realOrders
bt.MetaData.ClosePositionsOnStop = cfg.DataSettings.LiveData.ClosePositionsOnStop
}
buyRule := exchange.MinMax{
@@ -540,11 +583,19 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
if limits != (gctorder.MinMaxLevel{}) {
if !cfg.CurrencySettings[i].CanUseExchangeLimits {
log.Warnf(common.Setup, "Exchange %s order execution limits supported but disabled for %s %s, live results may differ",
cfg.CurrencySettings[i].ExchangeName,
pair,
a)
cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true
if realOrders {
log.Warnf(common.Setup, "Exchange %s order execution limits enabled for %s %s due to using real orders",
cfg.CurrencySettings[i].ExchangeName,
pair,
a)
cfg.CurrencySettings[i].CanUseExchangeLimits = true
} else {
log.Warnf(common.Setup, "Exchange %s order execution limits supported but disabled for %s %s, live results may differ",
cfg.CurrencySettings[i].ExchangeName,
pair,
a)
cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true
}
}
}
var lev exchange.Leverage
@@ -587,10 +638,6 @@ func (bt *BackTest) loadExchangePairAssetBase(exch string, base, quote currency.
cp = currency.NewPair(base, quote)
exchangeBase := e.GetBase()
if exchangeBase.ValidateAPICredentials(exchangeBase.GetDefaultCredentials()) != nil {
log.Warnf(common.Setup, "No credentials set for %v, this is theoretical only", exchangeBase.Name)
}
fPair, err = exchangeBase.FormatExchangeCurrency(cp, ai)
if err != nil {
return nil, currency.EMPTYPAIR, asset.Empty, err
@@ -599,7 +646,13 @@ func (bt *BackTest) loadExchangePairAssetBase(exch string, base, quote currency.
}
// 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) {
func getFees(ctx context.Context, exch gctexchange.IBotExchange, fPair currency.Pair) (makerFee, takerFee decimal.Decimal, err error) {
if exch == nil {
return decimal.Zero, decimal.Zero, fmt.Errorf("exchange %w", gctcommon.ErrNilPointer)
}
if fPair.IsEmpty() {
return decimal.Zero, decimal.Zero, currency.ErrCurrencyPairEmpty
}
fTakerFee, err := exch.GetFeeByType(ctx,
&gctexchange.FeeBuilder{FeeType: gctexchange.OfflineTradeFee,
Pair: fPair,
@@ -608,7 +661,7 @@ func getFees(ctx context.Context, exch gctexchange.IBotExchange, fPair currency.
Amount: 1,
})
if err != nil {
log.Errorf(common.Setup, "Could not retrieve taker fee for %v. %v", exch.GetName(), err)
return decimal.Zero, decimal.Zero, err
}
fMakerFee, err := exch.GetFeeByType(ctx,
@@ -620,10 +673,10 @@ func getFees(ctx context.Context, exch gctexchange.IBotExchange, fPair currency.
Amount: 1,
})
if err != nil {
log.Errorf(common.Setup, "Could not retrieve maker fee for %v. %v", exch.GetName(), err)
return decimal.Zero, decimal.Zero, err
}
return decimal.NewFromFloat(fMakerFee), decimal.NewFromFloat(fTakerFee)
return decimal.NewFromFloat(fMakerFee), decimal.NewFromFloat(fTakerFee), nil
}
// loadData will create kline data from the sources defined in start config files. It can exist from databases, csv or API endpoints
@@ -654,7 +707,23 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
}
log.Infof(common.Setup, "Loading data for %v %v %v...\n", exch.GetName(), a, fPair)
resp := &kline.DataFromKline{}
resp := kline.NewDataFromKline()
underlyingPair := currency.EMPTYPAIR
if a.IsFutures() {
// returning the collateral currency along with using the
// fPair base creates a pair that links the futures contract to
// its underlyingPair pair
// eg BTCUSDT-PERP on Binance has a collateral currency of USDT
// taking the BTC base and USDT as quote, allows linking
// BTC-USDT and BTCUSDT-PERP
var curr currency.Code
curr, _, err = exch.GetCollateralCurrencyForContract(a, fPair)
if err != nil {
return resp, err
}
underlyingPair = currency.NewPair(fPair.Base, curr)
}
switch {
case cfg.DataSettings.CSVData != nil:
if cfg.DataSettings.Interval <= 0 {
@@ -671,7 +740,7 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
if err != nil {
return nil, fmt.Errorf("%v. Please check your GoCryptoTrader configuration", err)
}
resp.Item.RemoveDuplicates()
resp.Item.RemoveDuplicateCandlesByTime()
resp.Item.SortCandlesByTimestamp(false)
resp.RangeHolder, err = gctkline.CalculateCandleDateRanges(
resp.Item.Candles[0].Time,
@@ -714,7 +783,7 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
return nil, fmt.Errorf("unable to retrieve data from GoCryptoTrader database. Error: %v. Please ensure the database is setup correctly and has data before use", err)
}
resp.Item.RemoveDuplicates()
resp.Item.RemoveDuplicateCandlesByTime()
resp.Item.SortCandlesByTimestamp(false)
resp.RangeHolder, err = gctkline.CalculateCandleDateRanges(
cfg.DataSettings.DatabaseData.StartDate,
@@ -745,44 +814,25 @@ 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")
}
err = loadLiveData(cfg, b)
if err != nil {
return nil, err
}
go bt.loadLiveDataLoop(
resp,
cfg,
exch,
fPair,
a,
dataType)
return resp, nil
bt.exchangeManager.Add(exch)
err = bt.LiveDataHandler.AppendDataSource(&liveDataSourceSetup{
exchange: exch,
interval: cfg.DataSettings.Interval,
asset: a,
pair: fPair,
underlyingPair: underlyingPair,
dataType: dataType,
dataRequestRetryTolerance: cfg.DataSettings.LiveData.DataRequestRetryTolerance,
dataRequestRetryWaitTime: cfg.DataSettings.LiveData.DataRequestRetryWaitTime,
verboseExchangeRequest: cfg.DataSettings.VerboseExchangeRequests,
})
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("processing error, response returned nil")
}
if a.IsFutures() {
// returning the collateral currency along with using the
// fPair base creates a pair that links the futures contract to
// is underlying pair
// eg BTC-PERP on FTX has a collateral currency of USD
// taking the BTC base and USD as quote, allows linking
// BTC-USD and BTC-PERP
var curr currency.Code
curr, _, err = exch.GetCollateralCurrencyForContract(a, fPair)
if err != nil {
return resp, err
}
resp.Item.UnderlyingPair = currency.NewPair(fPair.Base, curr)
}
resp.Item.UnderlyingPair = underlyingPair
err = b.ValidateKline(fPair, a, resp.Item.Interval)
if err != nil {
if dataType != common.DataTrade || !strings.EqualFold(err.Error(), "interval not supported") {
@@ -794,7 +844,10 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
if err != nil {
return nil, err
}
bt.Reports.AddKlineItem(&resp.Item)
err = bt.Reports.SetKlineData(&resp.Item)
if err != nil {
return nil, err
}
return resp, nil
}
@@ -848,41 +901,47 @@ func loadAPIData(cfg *config.Config, exch gctexchange.IBotExchange, fPair curren
candles.FillMissingDataWithEmptyEntries(dates)
candles.RemoveOutsideRange(cfg.DataSettings.APIData.StartDate, cfg.DataSettings.APIData.EndDate)
return &kline.DataFromKline{
Base: &data.Base{},
Item: *candles,
RangeHolder: dates,
}, nil
}
func loadLiveData(cfg *config.Config, base *gctexchange.Base) error {
func setExchangeCredentials(cfg *config.Config, base *gctexchange.Base) error {
if cfg == nil || base == nil || cfg.DataSettings.LiveData == nil {
return common.ErrNilArguments
return gctcommon.ErrNilPointer
}
if !cfg.DataSettings.LiveData.RealOrders {
return nil
}
if cfg.DataSettings.Interval <= 0 {
return errIntervalUnset
}
if len(cfg.DataSettings.LiveData.ExchangeCredentials) == 0 {
return errNoCredsNoLive
}
name := strings.ToLower(base.Name)
for i := range cfg.DataSettings.LiveData.ExchangeCredentials {
if !strings.EqualFold(cfg.DataSettings.LiveData.ExchangeCredentials[i].Exchange, name) ||
cfg.DataSettings.LiveData.ExchangeCredentials[i].Keys.IsEmpty() {
return fmt.Errorf("%v %w, please review your live, real order config", base.GetName(), gctexchange.ErrCredentialsAreEmpty)
}
if cfg.DataSettings.LiveData.APIKeyOverride != "" {
base.API.SetKey(cfg.DataSettings.LiveData.APIKeyOverride)
}
if cfg.DataSettings.LiveData.APISecretOverride != "" {
base.API.SetSecret(cfg.DataSettings.LiveData.APISecretOverride)
}
if cfg.DataSettings.LiveData.APIClientIDOverride != "" {
base.API.SetClientID(cfg.DataSettings.LiveData.APIClientIDOverride)
}
if cfg.DataSettings.LiveData.API2FAOverride != "" {
base.API.SetPEMKey(cfg.DataSettings.LiveData.API2FAOverride)
}
if cfg.DataSettings.LiveData.APISubAccountOverride != "" {
base.API.SetSubAccount(cfg.DataSettings.LiveData.APISubAccountOverride)
base.SetCredentials(
cfg.DataSettings.LiveData.ExchangeCredentials[i].Keys.Key,
cfg.DataSettings.LiveData.ExchangeCredentials[i].Keys.Secret,
cfg.DataSettings.LiveData.ExchangeCredentials[i].Keys.ClientID,
cfg.DataSettings.LiveData.ExchangeCredentials[i].Keys.SubAccount,
cfg.DataSettings.LiveData.ExchangeCredentials[i].Keys.PEMKey,
cfg.DataSettings.LiveData.ExchangeCredentials[i].Keys.OneTimePassword,
)
validated := base.AreCredentialsValid(context.TODO())
base.API.AuthenticatedSupport = validated
if !validated {
return fmt.Errorf("%v %w", base.GetName(), errInvalidCredentials)
}
}
validated := base.AreCredentialsValid(context.TODO())
base.API.AuthenticatedSupport = validated
if !validated && cfg.DataSettings.LiveData.RealOrders {
log.Warn(common.Setup, "Invalid API credentials set, real orders set to false")
cfg.DataSettings.LiveData.RealOrders = false
}
return nil
}
@@ -897,7 +956,11 @@ func NewBacktesterFromConfigs(strategyCfg *config.Config, backtesterCfg *config.
if err := strategyCfg.Validate(); err != nil {
return nil, err
}
bt, err := NewFromConfig(strategyCfg, backtesterCfg.Report.TemplatePath, backtesterCfg.Report.OutputPath, backtesterCfg.Verbose)
bt, err := NewBacktester()
if err != nil {
return nil, err
}
err = bt.SetupFromConfig(strategyCfg, backtesterCfg.Report.TemplatePath, backtesterCfg.Report.OutputPath, backtesterCfg.Verbose)
if err != nil {
return nil, err
}

View File

@@ -1,411 +0,0 @@
package engine
import (
"errors"
"path/filepath"
"strings"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage"
"github.com/thrasher-corp/gocryptotrader/backtester/report"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/database/drivers"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
func TestNewFromConfig(t *testing.T) {
t.Parallel()
_, err := NewFromConfig(nil, "", "", false)
if !errors.Is(err, errNilConfig) {
t.Errorf("received %v, expected %v", err, errNilConfig)
}
cfg := &config.Config{}
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, base.ErrStrategyNotFound) {
t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
}
cfg.CurrencySettings = []config.CurrencySettings{
{
ExchangeName: "test",
Base: currency.NewCode("test"),
Quote: currency.NewCode("test"),
},
{
ExchangeName: testExchange,
Base: currency.BTC,
Quote: currency.NewCode("0624"),
Asset: asset.Futures,
},
}
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, engine.ErrExchangeNotFound) {
t.Errorf("received: %v, expected: %v", err, engine.ErrExchangeNotFound)
}
cfg.CurrencySettings[0].ExchangeName = testExchange
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received: %v, expected: %v", err, asset.ErrNotSupported)
}
cfg.CurrencySettings[0].Asset = asset.Spot
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, base.ErrStrategyNotFound) {
t.Errorf("received: %v, expected: %v", err, base.ErrStrategyNotFound)
}
cfg.StrategySettings = config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
}
cfg.CurrencySettings[0].Base = currency.BTC
cfg.CurrencySettings[0].Quote = currency.USD
cfg.DataSettings.APIData = &config.APIData{
StartDate: time.Time{},
EndDate: time.Time{},
}
_, err = NewFromConfig(cfg, "", "", false)
if err != nil && !strings.Contains(err.Error(), "unrecognised dataType") {
t.Error(err)
}
cfg.DataSettings.DataType = common.CandleStr
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, errIntervalUnset) {
t.Errorf("received: %v, expected: %v", err, errIntervalUnset)
}
cfg.DataSettings.Interval = gctkline.OneMin
cfg.CurrencySettings[0].MakerFee = &decimal.Zero
cfg.CurrencySettings[0].TakerFee = &decimal.Zero
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, gctcommon.ErrDateUnset) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrDateUnset)
}
cfg.DataSettings.APIData.StartDate = time.Now().Add(-time.Minute)
cfg.DataSettings.APIData.EndDate = time.Now()
cfg.DataSettings.APIData.InclusiveEndDate = true
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, holdings.ErrInitialFundsZero) {
t.Errorf("received: %v, expected: %v", err, holdings.ErrInitialFundsZero)
}
cfg.FundingSettings.UseExchangeLevelFunding = true
cfg.FundingSettings.ExchangeLevelFunding = []config.ExchangeLevelFunding{
{
ExchangeName: testExchange,
Asset: asset.Spot,
Currency: currency.BTC,
InitialFunds: leet,
TransferFee: leet,
},
{
ExchangeName: testExchange,
Asset: asset.Futures,
Currency: currency.BTC,
InitialFunds: leet,
TransferFee: leet,
},
}
_, err = NewFromConfig(cfg, "", "", false)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
func TestLoadDataAPI(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
Statistic: &statistics.Statistic{},
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: &decimal.Zero,
TakerFee: &decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
APIData: &config.APIData{
StartDate: time.Now().Add(-time.Minute * 5),
EndDate: time.Now(),
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil {
t.Error(err)
}
}
func TestLoadDataDatabase(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
Statistic: &statistics.Statistic{},
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: &decimal.Zero,
TakerFee: &decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
DatabaseData: &config.DatabaseData{
Config: database.Config{
Enabled: true,
Driver: "sqlite3",
ConnectionDetails: drivers.ConnectionDetails{
Database: t.TempDir() + "gocryptotrader.db",
},
},
StartDate: time.Now().Add(-time.Minute),
EndDate: time.Now(),
InclusiveEndDate: true,
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
bt.databaseManager, err = engine.SetupDatabaseConnectionManager(&cfg.DataSettings.DatabaseData.Config)
if err != nil {
t.Fatal(err)
}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil && !strings.Contains(err.Error(), "unable to retrieve data from GoCryptoTrader database") {
t.Error(err)
}
}
func TestLoadDataCSV(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
Statistic: &statistics.Statistic{},
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: &decimal.Zero,
TakerFee: &decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
CSVData: &config.CSVData{
FullPath: "test",
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil &&
!strings.Contains(err.Error(), "The system cannot find the file specified.") &&
!strings.Contains(err.Error(), "no such file or directory") {
t.Error(err)
}
}
func TestLoadDataLive(t *testing.T) {
t.Parallel()
bt := BackTest{
Reports: &report.Data{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
cp := currency.NewPair(currency.BTC, currency.USDT)
cfg := &config.Config{
CurrencySettings: []config.CurrencySettings{
{
ExchangeName: "Binance",
Asset: asset.Spot,
Base: cp.Base,
Quote: cp.Quote,
SpotDetails: &config.SpotDetails{
InitialQuoteFunds: &leet,
},
BuySide: config.MinMax{},
SellSide: config.MinMax{},
MakerFee: &decimal.Zero,
TakerFee: &decimal.Zero,
},
},
DataSettings: config.DataSettings{
DataType: common.CandleStr,
Interval: gctkline.OneMin,
LiveData: &config.LiveData{
APIKeyOverride: "test",
APISecretOverride: "test",
APIClientIDOverride: "test",
API2FAOverride: "test",
RealOrders: true,
}},
StrategySettings: config.StrategySettings{
Name: dollarcostaverage.Name,
CustomSettings: map[string]interface{}{
"hello": "moto",
},
},
}
em := engine.ExchangeManager{}
exch, err := em.NewExchangeByName("Binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true}}
_, err = bt.loadData(cfg, exch, cp, asset.Spot, false)
if err != nil {
t.Error(err)
}
bt.Stop()
}
func TestNewBacktesterFromConfigs(t *testing.T) {
t.Parallel()
_, err := NewBacktesterFromConfigs(nil, nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
strat1 := filepath.Join("..", "config", "strategyexamples", "dca-api-candles.strat")
cfg, err := config.ReadStrategyConfigFromFile(strat1)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
dc, err := config.GenerateDefaultConfig()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = NewBacktesterFromConfigs(cfg, nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
_, err = NewBacktesterFromConfigs(nil, dc)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
bt, err := NewBacktesterFromConfigs(cfg, dc)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.MetaData.DateLoaded.IsZero() {
t.Errorf("received '%v' expected '%v'", bt.MetaData.DateLoaded, "a date")
}
}

View File

@@ -0,0 +1,216 @@
package engine
import (
"errors"
"fmt"
"github.com/gofrs/uuid"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
)
var (
errTaskNotFound = errors.New("task not found")
errTaskAlreadyMonitored = errors.New("task already monitored")
errAlreadyRan = errors.New("task already ran")
errTaskHasNotRan = errors.New("task hasn't ran yet")
errTaskIsRunning = errors.New("task is already running")
errCannotClear = errors.New("cannot clear task")
)
// NewTaskManager creates a run manager to allow the backtester to manage multiple strategies
func NewTaskManager() *TaskManager {
return &TaskManager{}
}
// AddTask adds a run to the manager
func (r *TaskManager) AddTask(b *BackTest) error {
if r == nil {
return fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
if b == nil {
return fmt.Errorf("%w BackTest", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.tasks {
if r.tasks[i].Equal(b) {
return fmt.Errorf("%w %s %s", errTaskAlreadyMonitored, b.MetaData.ID, b.MetaData.Strategy)
}
}
err := b.SetupMetaData()
if err != nil {
return err
}
r.tasks = append(r.tasks, b)
return nil
}
// List details all strategy tasks
func (r *TaskManager) List() ([]*TaskSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
resp := make([]*TaskSummary, len(r.tasks))
for i := range r.tasks {
sum, err := r.tasks[i].GenerateSummary()
if err != nil {
return nil, err
}
resp[i] = sum
}
return resp, nil
}
// GetSummary returns details about a completed strategy task
func (r *TaskManager) GetSummary(id uuid.UUID) (*TaskSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.tasks {
if !r.tasks[i].MatchesID(id) {
continue
}
return r.tasks[i].GenerateSummary()
}
return nil, fmt.Errorf("%s %w", id, errTaskNotFound)
}
// StopTask stops a strategy task if enabled, this will run CloseAllPositions
func (r *TaskManager) StopTask(id uuid.UUID) error {
if r == nil {
return fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.tasks {
switch {
case !r.tasks[i].MatchesID(id):
continue
case r.tasks[i].IsRunning():
return r.tasks[i].Stop()
case r.tasks[i].HasRan():
return fmt.Errorf("%w %v", errAlreadyRan, id)
default:
return fmt.Errorf("%w %v", errTaskHasNotRan, id)
}
}
return fmt.Errorf("%s %w", id, errTaskNotFound)
}
// StopAllTasks stops all running strategies
func (r *TaskManager) StopAllTasks() ([]*TaskSummary, error) {
if r == nil {
return nil, fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
resp := make([]*TaskSummary, 0, len(r.tasks))
for i := range r.tasks {
if !r.tasks[i].IsRunning() {
continue
}
err := r.tasks[i].Stop()
if err != nil {
return nil, err
}
sum, err := r.tasks[i].GenerateSummary()
if err != nil {
return nil, err
}
resp = append(resp, sum)
}
return resp, nil
}
// StartTask executes a strategy if found
func (r *TaskManager) StartTask(id uuid.UUID) error {
if r == nil {
return fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.tasks {
switch {
case !r.tasks[i].MatchesID(id):
continue
case r.tasks[i].IsRunning():
return fmt.Errorf("%w %v", errTaskIsRunning, id)
case r.tasks[i].HasRan():
return fmt.Errorf("%w %v", errAlreadyRan, id)
default:
return r.tasks[i].ExecuteStrategy(false)
}
}
return fmt.Errorf("%s %w", id, errTaskNotFound)
}
// StartAllTasks executes all strategies
func (r *TaskManager) StartAllTasks() ([]uuid.UUID, error) {
if r == nil {
return nil, fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
executedRuns := make([]uuid.UUID, 0, len(r.tasks))
for i := range r.tasks {
if r.tasks[i].HasRan() {
continue
}
executedRuns = append(executedRuns, r.tasks[i].MetaData.ID)
err := r.tasks[i].ExecuteStrategy(false)
if err != nil {
return nil, err
}
}
return executedRuns, nil
}
// ClearTask removes a run from memory, but only if it is not running
func (r *TaskManager) ClearTask(id uuid.UUID) error {
if r == nil {
return fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := range r.tasks {
if !r.tasks[i].MatchesID(id) {
continue
}
if r.tasks[i].IsRunning() {
return fmt.Errorf("%w %v, currently running. Stop it first", errCannotClear, r.tasks[i].MetaData.ID)
}
r.tasks = append(r.tasks[:i], r.tasks[i+1:]...)
return nil
}
return fmt.Errorf("%s %w", id, errTaskNotFound)
}
// ClearAllTasks removes all tasks from memory, but only if they are not running
func (r *TaskManager) ClearAllTasks() (clearedRuns, remainingRuns []*TaskSummary, err error) {
if r == nil {
return nil, nil, fmt.Errorf("%w TaskManager", gctcommon.ErrNilPointer)
}
r.m.Lock()
defer r.m.Unlock()
for i := 0; i < len(r.tasks); i++ {
var run *TaskSummary
run, err = r.tasks[i].GenerateSummary()
if err != nil {
return nil, nil, err
}
if r.tasks[i].IsRunning() {
remainingRuns = append(remainingRuns, run)
} else {
clearedRuns = append(clearedRuns, run)
r.tasks = append(r.tasks[:i], r.tasks[i+1:]...)
i--
}
}
return clearedRuns, remainingRuns, nil
}

View File

@@ -9,48 +9,48 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/ftxcashandcarry"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/binancecashandcarry"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
)
func TestSetupRunManager(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
rm := NewTaskManager()
if rm == nil {
t.Errorf("received '%v' expected '%v'", rm, "&RunManager{}")
t.Errorf("received '%v' expected '%v'", rm, "&TaskManager{}")
}
}
func TestAddRun(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
err := rm.AddRun(nil)
rm := NewTaskManager()
err := rm.AddTask(nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
bt := &BackTest{}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if bt.MetaData.ID.IsNil() {
t.Errorf("received '%v' expected '%v'", bt.MetaData.ID, "a random ID")
}
if len(rm.runs) != 1 {
t.Errorf("received '%v' expected '%v'", len(rm.runs), 1)
if len(rm.tasks) != 1 {
t.Errorf("received '%v' expected '%v'", len(rm.tasks), 1)
}
err = rm.AddRun(bt)
if !errors.Is(err, errRunAlreadyMonitored) {
t.Errorf("received '%v' expected '%v'", err, errRunAlreadyMonitored)
err = rm.AddTask(bt)
if !errors.Is(err, errTaskAlreadyMonitored) {
t.Errorf("received '%v' expected '%v'", err, errTaskAlreadyMonitored)
}
if len(rm.runs) != 1 {
t.Errorf("received '%v' expected '%v'", len(rm.runs), 1)
if len(rm.tasks) != 1 {
t.Errorf("received '%v' expected '%v'", len(rm.tasks), 1)
}
rm = nil
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
@@ -58,21 +58,21 @@ func TestAddRun(t *testing.T) {
func TestGetSummary(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
rm := NewTaskManager()
id, err := uuid.NewV4()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
_, err = rm.GetSummary(id)
if !errors.Is(err, errRunNotFound) {
t.Errorf("received '%v' expected '%v'", err, errRunNotFound)
if !errors.Is(err, errTaskNotFound) {
t.Errorf("received '%v' expected '%v'", err, errTaskNotFound)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
Statistic: &statistics.Statistic{},
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -94,7 +94,7 @@ func TestGetSummary(t *testing.T) {
func TestList(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
rm := NewTaskManager()
list, err := rm.List()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
@@ -104,10 +104,10 @@ func TestList(t *testing.T) {
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
Statistic: &statistics.Statistic{},
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -129,7 +129,7 @@ func TestList(t *testing.T) {
func TestStopRun(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
rm := NewTaskManager()
list, err := rm.List()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
@@ -142,38 +142,42 @@ func TestStopRun(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = rm.StopRun(id)
if !errors.Is(err, errRunNotFound) {
t.Errorf("received '%v' expected '%v'", err, errRunNotFound)
err = rm.StopTask(id)
if !errors.Is(err, errTaskNotFound) {
t.Errorf("received '%v' expected '%v'", err, errTaskNotFound)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Statistic: &statistics.Statistic{},
Strategy: &fakeStrat{},
Statistic: &fakeStats{},
Reports: &fakeReport{},
shutdown: make(chan struct{}),
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = rm.StopRun(bt.MetaData.ID)
if !errors.Is(err, errRunHasNotRan) {
t.Errorf("received '%v' expected '%v'", err, errRunHasNotRan)
err = rm.StopTask(bt.MetaData.ID)
if !errors.Is(err, errTaskHasNotRan) {
t.Errorf("received '%v' expected '%v'", err, errTaskHasNotRan)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Now()
err = rm.StopRun(bt.MetaData.ID)
bt.m.Unlock()
err = rm.StopTask(bt.MetaData.ID)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = rm.StopRun(bt.MetaData.ID)
err = rm.StopTask(bt.MetaData.ID)
if !errors.Is(err, errAlreadyRan) {
t.Errorf("received '%v' expected '%v'", err, errAlreadyRan)
}
rm = nil
err = rm.StopRun(id)
err = rm.StopTask(id)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
@@ -181,8 +185,8 @@ func TestStopRun(t *testing.T) {
func TestStopAllRuns(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
stoppedRuns, err := rm.StopAllRuns()
rm := NewTaskManager()
stoppedRuns, err := rm.StopAllTasks()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -191,16 +195,19 @@ func TestStopAllRuns(t *testing.T) {
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Statistic: &statistics.Statistic{},
Strategy: &binancecashandcarry.Strategy{},
Statistic: &fakeStats{},
Reports: &fakeReport{},
shutdown: make(chan struct{}),
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Now()
stoppedRuns, err = rm.StopAllRuns()
bt.m.Unlock()
stoppedRuns, err = rm.StopAllTasks()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -209,7 +216,7 @@ func TestStopAllRuns(t *testing.T) {
}
rm = nil
_, err = rm.StopAllRuns()
_, err = rm.StopAllTasks()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
@@ -217,7 +224,7 @@ func TestStopAllRuns(t *testing.T) {
func TestStartRun(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
rm := NewTaskManager()
list, err := rm.List()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
@@ -230,42 +237,44 @@ func TestStartRun(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = rm.StartRun(id)
if !errors.Is(err, errRunNotFound) {
t.Errorf("received '%v' expected '%v'", err, errRunNotFound)
err = rm.StartTask(id)
if !errors.Is(err, errTaskNotFound) {
t.Errorf("received '%v' expected '%v'", err, errTaskNotFound)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = rm.StartRun(bt.MetaData.ID)
err = rm.StartTask(bt.MetaData.ID)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = rm.StartRun(bt.MetaData.ID)
if !errors.Is(err, errRunIsRunning) {
t.Errorf("received '%v' expected '%v'", err, errRunIsRunning)
err = rm.StartTask(bt.MetaData.ID)
if !errors.Is(err, errTaskIsRunning) {
t.Errorf("received '%v' expected '%v'", err, errTaskIsRunning)
}
bt.m.Lock()
bt.MetaData.DateEnded = time.Now()
bt.MetaData.Closed = true
bt.shutdown = make(chan struct{})
bt.m.Unlock()
err = rm.StartRun(bt.MetaData.ID)
err = rm.StartTask(bt.MetaData.ID)
if !errors.Is(err, errAlreadyRan) {
t.Errorf("received '%v' expected '%v'", err, errAlreadyRan)
}
rm = nil
err = rm.StartRun(id)
err = rm.StartTask(id)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
@@ -273,8 +282,8 @@ func TestStartRun(t *testing.T) {
func TestStartAllRuns(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
startedRuns, err := rm.StartAllRuns()
rm := NewTaskManager()
startedRuns, err := rm.StartAllTasks()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -283,17 +292,17 @@ func TestStartAllRuns(t *testing.T) {
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
startedRuns, err = rm.StartAllRuns()
startedRuns, err = rm.StartAllTasks()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -302,7 +311,7 @@ func TestStartAllRuns(t *testing.T) {
}
rm = nil
_, err = rm.StartAllRuns()
_, err = rm.StartAllTasks()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
@@ -310,37 +319,41 @@ func TestStartAllRuns(t *testing.T) {
func TestClearRun(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
rm := NewTaskManager()
id, err := uuid.NewV4()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = rm.ClearRun(id)
if !errors.Is(err, errRunNotFound) {
t.Errorf("received '%v' expected '%v'", err, errRunNotFound)
err = rm.ClearTask(id)
if !errors.Is(err, errTaskNotFound) {
t.Errorf("received '%v' expected '%v'", err, errTaskNotFound)
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Now()
err = rm.ClearRun(bt.MetaData.ID)
bt.m.Unlock()
err = rm.ClearTask(bt.MetaData.ID)
if !errors.Is(err, errCannotClear) {
t.Errorf("received '%v' expected '%v'", err, errCannotClear)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Time{}
err = rm.ClearRun(bt.MetaData.ID)
bt.m.Unlock()
err = rm.ClearTask(bt.MetaData.ID)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -353,7 +366,7 @@ func TestClearRun(t *testing.T) {
}
rm = nil
err = rm.ClearRun(id)
err = rm.ClearTask(id)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
@@ -361,9 +374,9 @@ func TestClearRun(t *testing.T) {
func TestClearAllRuns(t *testing.T) {
t.Parallel()
rm := SetupRunManager()
rm := NewTaskManager()
clearedRuns, remainingRuns, err := rm.ClearAllRuns()
clearedRuns, remainingRuns, err := rm.ClearAllTasks()
if len(clearedRuns) != 0 {
t.Errorf("received '%v' expected '%v'", len(clearedRuns), 0)
}
@@ -375,19 +388,21 @@ func TestClearAllRuns(t *testing.T) {
}
bt := &BackTest{
Strategy: &ftxcashandcarry.Strategy{},
Strategy: &binancecashandcarry.Strategy{},
EventQueue: &eventholder.Holder{},
Datas: &data.HandlerPerCurrency{},
DataHolder: &data.HandlerHolder{},
Statistic: &statistics.Statistic{},
shutdown: make(chan struct{}),
}
err = rm.AddRun(bt)
err = rm.AddTask(bt)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Now()
clearedRuns, remainingRuns, err = rm.ClearAllRuns()
bt.m.Unlock()
clearedRuns, remainingRuns, err = rm.ClearAllTasks()
if len(clearedRuns) != 0 {
t.Errorf("received '%v' expected '%v'", len(clearedRuns), 0)
}
@@ -398,8 +413,10 @@ func TestClearAllRuns(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
bt.m.Lock()
bt.MetaData.DateStarted = time.Time{}
clearedRuns, remainingRuns, err = rm.ClearAllRuns()
bt.m.Unlock()
clearedRuns, remainingRuns, err = rm.ClearAllTasks()
if len(clearedRuns) != 1 {
t.Errorf("received '%v' expected '%v'", len(clearedRuns), 1)
}
@@ -418,7 +435,7 @@ func TestClearAllRuns(t *testing.T) {
}
rm = nil
_, _, err = rm.ClearAllRuns()
_, _, err = rm.ClearAllTasks()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}

View File

@@ -2,26 +2,31 @@ package eventholder
import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
)
// Reset returns struct to defaults
func (e *Holder) Reset() {
e.Queue = nil
func (h *Holder) Reset() error {
if h == nil {
return gctcommon.ErrNilPointer
}
h.Queue = nil
return nil
}
// AppendEvent adds and event to the queue
func (e *Holder) AppendEvent(i common.EventHandler) {
e.Queue = append(e.Queue, i)
func (h *Holder) AppendEvent(i common.Event) {
h.Queue = append(h.Queue, i)
}
// NextEvent removes the current event and returns the next event in the queue
func (e *Holder) NextEvent() (i common.EventHandler) {
if len(e.Queue) == 0 {
func (h *Holder) NextEvent() (i common.Event) {
if len(h.Queue) == 0 {
return nil
}
i = e.Queue[0]
e.Queue = e.Queue[1:]
i = h.Queue[0]
h.Queue = h.Queue[1:]
return i
}

View File

@@ -1,24 +1,35 @@
package eventholder
import (
"errors"
"testing"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
)
func TestReset(t *testing.T) {
t.Parallel()
e := Holder{Queue: []common.EventHandler{}}
e.Reset()
e := &Holder{Queue: []common.Event{}}
err := e.Reset()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if e.Queue != nil {
t.Error("expected nil")
}
e = nil
err = e.Reset()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
func TestAppendEvent(t *testing.T) {
t.Parallel()
e := Holder{Queue: []common.EventHandler{}}
e := Holder{Queue: []common.Event{}}
e.AppendEvent(&order.Order{})
if len(e.Queue) != 1 {
t.Error("expected 1")
@@ -27,12 +38,12 @@ func TestAppendEvent(t *testing.T) {
func TestNextEvent(t *testing.T) {
t.Parallel()
e := Holder{Queue: []common.EventHandler{}}
e := Holder{Queue: []common.Event{}}
if ev := e.NextEvent(); ev != nil {
t.Error("expected not ok")
}
e = Holder{Queue: []common.EventHandler{
e = Holder{Queue: []common.Event{
&order.Order{},
&order.Order{},
&order.Order{},

View File

@@ -6,12 +6,12 @@ import (
// Holder contains the event queue for backtester processing
type Holder struct {
Queue []common.EventHandler
Queue []common.Event
}
// EventHolder interface details what is expected of an event holder to perform
type EventHolder interface {
Reset()
AppendEvent(common.EventHandler)
NextEvent() common.EventHandler
Reset() error
AppendEvent(common.Event)
NextEvent() common.Event
}

View File

@@ -2,9 +2,9 @@ package exchange
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/shopspring/decimal"
@@ -14,24 +14,25 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
)
// Reset returns the exchange to initial settings
func (e *Exchange) Reset() {
*e = Exchange{}
func (e *Exchange) Reset() error {
if e == nil {
return gctcommon.ErrNilPointer
}
e.CurrencySettings = nil
return nil
}
// ErrCannotTransact returns when its an issue to do nothing for an event
var ErrCannotTransact = errors.New("cannot transact")
// ExecuteOrder assesses the portfolio manager's order event and if it passes validation
// will send an order to the exchange/fake order manager to be stored and raise a fill event
func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *engine.OrderManager, funds funding.IFundReleaser) (fill.Event, error) {
func (e *Exchange) ExecuteOrder(o order.Event, dh data.Handler, om *engine.OrderManager, funds funding.IFundReleaser) (fill.Event, error) {
f := &fill.Fill{
Base: o.GetBase(),
Direction: o.GetDirection(),
@@ -78,33 +79,18 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
}
return f, nil
}
// get current orderbook
var ob *orderbook.Base
ob, err = orderbook.Get(f.Exchange, f.CurrencyPair, f.AssetType)
if err != nil {
return f, err
}
// calculate an estimated slippage rate
price, amount, err = slippage.CalculateSlippageByOrderbook(ob, o.GetDirection(), allocatedFunds, f.ExchangeFee)
if err != nil {
return f, err
}
f.Slippage = price.Sub(f.ClosePrice).Div(f.ClosePrice).Mul(decimal.NewFromInt(100))
} else {
slippageRate := slippage.EstimateSlippagePercentage(cs.MinimumSlippageRate, cs.MaximumSlippageRate)
if cs.SkipCandleVolumeFitting || o.GetAssetType().IsFutures() {
if cs.SkipCandleVolumeFitting || o.GetAssetType().IsFutures() || o.GetDirection() == gctorder.ClosePosition {
f.VolumeAdjustedPrice = f.ClosePrice
amount = f.Amount
} else {
highStr := data.StreamHigh()
high := highStr[len(highStr)-1]
lowStr := data.StreamLow()
low := lowStr[len(lowStr)-1]
volStr := data.StreamVol()
volume := volStr[len(volStr)-1]
adjustedPrice, adjustedAmount = ensureOrderFitsWithinHLV(price, amount, high, low, volume)
var latest data.Event
latest, err = dh.Latest()
if err != nil {
return nil, err
}
adjustedPrice, adjustedAmount = ensureOrderFitsWithinHLV(price, amount, latest.GetHighPrice(), latest.GetLowPrice(), latest.GetVolume())
if !amount.Equal(adjustedAmount) {
f.AppendReasonf("Order size shrunk from %v to %v to fit candle", amount, adjustedAmount)
amount = adjustedAmount
@@ -115,22 +101,6 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
f.VolumeAdjustedPrice = price
}
}
if amount.LessThanOrEqual(decimal.Zero) && f.GetAmount().GreaterThan(decimal.Zero) {
switch f.GetDirection() {
case gctorder.Buy, gctorder.Bid:
f.SetDirection(gctorder.CouldNotBuy)
case gctorder.Sell, gctorder.Ask:
f.SetDirection(gctorder.CouldNotSell)
case gctorder.Short:
f.SetDirection(gctorder.CouldNotShort)
case gctorder.Long:
f.SetDirection(gctorder.CouldNotLong)
default:
f.SetDirection(gctorder.DoNothing)
}
f.AppendReasonf("amount set to 0, %s", errDataMayBeIncorrect)
return f, err
}
adjustedPrice, err = applySlippageToPrice(f.GetDirection(), price, slippageRate)
if err != nil {
return f, err
@@ -148,14 +118,14 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
amount = adjustedAmount
}
if cs.CanUseExchangeLimits {
if cs.CanUseExchangeLimits || cs.UseRealOrders {
// Conforms the amount to the exchange order defined step amount
// reducing it when needed
adjustedAmount = cs.Limits.ConformToDecimalAmount(amount)
if !adjustedAmount.Equal(amount) {
if !adjustedAmount.Equal(amount) && !adjustedAmount.IsZero() {
f.AppendReasonf("Order size shrunk from %v to %v to remain within exchange step amount limits",
adjustedAmount,
amount)
amount,
adjustedAmount)
amount = adjustedAmount
}
}
@@ -165,12 +135,14 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
}
fee = calculateExchangeFee(price, amount, cs.TakerFee)
orderID, err := e.placeOrder(context.TODO(), price, amount, fee, cs.UseRealOrders, cs.CanUseExchangeLimits, f, orderManager)
orderID, err := e.placeOrder(context.TODO(), price, amount, fee, cs.UseRealOrders, cs.CanUseExchangeLimits, f, om)
if err != nil {
f.AppendReasonf("could not place order: %v", err)
setCannotPurchaseDirection(f)
return f, err
}
ords := orderManager.GetOrdersSnapshot(gctorder.UnknownStatus)
ords := om.GetOrdersSnapshot(gctorder.UnknownStatus)
for i := range ords {
if ords[i].OrderID != orderID {
continue
@@ -187,16 +159,15 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, orderManager *
f.Total = f.PurchasePrice.Mul(f.Amount).Add(f.ExchangeFee)
}
if !o.IsLiquidating() {
err = allocateFundsPostOrder(f, funds, err, o.GetAmount(), allocatedFunds, amount, adjustedPrice, fee)
err = allocateFundsPostOrder(f, funds, err, o.GetAmount(), allocatedFunds, amount, price, fee)
if err != nil {
return f, err
}
}
if f.Order == nil {
return nil, fmt.Errorf("placed order %v not found in order manager", orderID)
}
f.AppendReason(summarisePosition(f.GetDirection(), f.Amount, f.Amount.Mul(f.PurchasePrice), f.ExchangeFee, f.Order.Pair, f.UnderlyingPair))
return f, nil
}
@@ -205,7 +176,7 @@ func allocateFundsPostOrder(f *fill.Fill, funds funding.IFundReleaser, orderErro
return fmt.Errorf("%w: fill event", common.ErrNilEvent)
}
if funds == nil {
return fmt.Errorf("%w: funding", common.ErrNilArguments)
return fmt.Errorf("%w: funding", gctcommon.ErrNilPointer)
}
switch f.AssetType {
@@ -250,7 +221,6 @@ func allocateFundsPostOrder(f *fill.Fill, funds funding.IFundReleaser, orderErro
default:
return fmt.Errorf("%w asset type %v", common.ErrInvalidDataType, f.GetDirection())
}
f.AppendReason(summarisePosition(f.GetDirection(), f.Amount, f.Amount.Mul(f.PurchasePrice), f.ExchangeFee, f.Order.Pair, currency.EMPTYPAIR))
case asset.Futures:
cr, err := funds.CollateralReleaser()
if err != nil {
@@ -261,17 +231,9 @@ func allocateFundsPostOrder(f *fill.Fill, funds funding.IFundReleaser, orderErro
if err != nil {
return err
}
switch f.GetDirection() {
case gctorder.Short:
f.SetDirection(gctorder.CouldNotShort)
case gctorder.Long:
f.SetDirection(gctorder.CouldNotLong)
default:
return fmt.Errorf("%w asset type %v", common.ErrInvalidDataType, f.GetDirection())
}
setCannotPurchaseDirection(f)
return orderError
}
f.AppendReason(summarisePosition(f.GetDirection(), f.Amount, f.Amount.Mul(f.PurchasePrice), f.ExchangeFee, f.Order.Pair, f.UnderlyingPair))
default:
return fmt.Errorf("%w asset type %v", common.ErrInvalidDataType, f.AssetType)
}
@@ -298,6 +260,19 @@ func summarisePosition(direction gctorder.Side, orderAmount, orderTotal, orderFe
)
}
func setCannotPurchaseDirection(f fill.Event) {
switch f.GetDirection() {
case gctorder.Buy, gctorder.Bid:
f.SetDirection(gctorder.CouldNotBuy)
case gctorder.Sell, gctorder.Ask:
f.SetDirection(gctorder.CouldNotSell)
case gctorder.Long:
f.SetDirection(gctorder.CouldNotLong)
case gctorder.Short:
f.SetDirection(gctorder.CouldNotShort)
}
}
// verifyOrderWithinLimits conforms the amount to fall into the minimum size and maximum size limit after reduced
func verifyOrderWithinLimits(f fill.Event, amount decimal.Decimal, cs *Settings) error {
if f == nil {
@@ -310,18 +285,12 @@ func verifyOrderWithinLimits(f fill.Event, amount decimal.Decimal, cs *Settings)
var minMax MinMax
var direction gctorder.Side
switch f.GetDirection() {
case gctorder.Buy, gctorder.Bid:
case gctorder.Buy, gctorder.Bid, gctorder.Long:
minMax = cs.BuySide
direction = gctorder.CouldNotBuy
case gctorder.Sell, gctorder.Ask:
minMax = cs.SellSide
case gctorder.Sell, gctorder.Ask, gctorder.Short:
direction = gctorder.CouldNotSell
case gctorder.Long:
minMax = cs.BuySide
direction = gctorder.CouldNotLong
case gctorder.Short:
minMax = cs.SellSide
direction = gctorder.CouldNotShort
case gctorder.ClosePosition:
return nil
default:
@@ -378,13 +347,15 @@ func (e *Exchange) placeOrder(ctx context.Context, price, amount, fee decimal.De
}
submit := &gctorder.Submit{
Price: price.InexactFloat64(),
Amount: amount.InexactFloat64(),
Exchange: f.GetExchange(),
Side: f.GetDirection(),
AssetType: f.GetAssetType(),
Pair: f.Pair(),
Type: gctorder.Market,
Price: price.InexactFloat64(),
Amount: amount.InexactFloat64(),
Exchange: f.GetExchange(),
Side: f.GetDirection(),
AssetType: f.GetAssetType(),
Pair: f.Pair(),
Type: gctorder.Market,
RetrieveFees: true,
RetrieveFeeDelay: time.Millisecond * 500,
}
var resp *engine.OrderSubmitResponse

View File

@@ -9,57 +9,86 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/ftx"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
const testExchange = "ftx"
const testExchange = "binance"
type fakeFund struct{}
var leet = decimal.NewFromInt(1337)
func (f *fakeFund) GetPairReader() (funding.IPairReader, error) {
return nil, nil
i, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, leet, leet)
if err != nil {
return nil, err
}
j, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, leet, leet)
if err != nil {
return nil, err
}
return funding.CreatePair(i, j)
}
func (f *fakeFund) GetCollateralReader() (funding.ICollateralReader, error) {
return nil, nil
i, err := funding.CreateItem(testExchange, asset.Futures, currency.BTC, leet, leet)
if err != nil {
return nil, err
}
j, err := funding.CreateItem(testExchange, asset.Futures, currency.USDT, leet, leet)
if err != nil {
return nil, err
}
return funding.CreateCollateral(i, j)
}
func (f *fakeFund) PairReleaser() (funding.IPairReleaser, error) {
btc, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, decimal.NewFromInt(9999), decimal.NewFromInt(9999))
btc, err := funding.CreateItem(testExchange, asset.Spot, currency.BTC, leet, leet)
if err != nil {
return nil, err
}
usd, err := funding.CreateItem(testExchange, asset.Spot, currency.USD, decimal.NewFromInt(9999), decimal.NewFromInt(9999))
usdt, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, leet, leet)
if err != nil {
return nil, err
}
p, err := funding.CreatePair(btc, usd)
p, err := funding.CreatePair(btc, usdt)
if err != nil {
return nil, err
}
err = p.Reserve(decimal.NewFromInt(1337), gctorder.Buy)
err = p.Reserve(leet, gctorder.Buy)
if err != nil {
return nil, err
}
err = p.Reserve(decimal.NewFromInt(1337), gctorder.Sell)
err = p.Reserve(leet, gctorder.Sell)
if err != nil {
return nil, err
}
return p, nil
}
func (f *fakeFund) CollateralReleaser() (funding.ICollateralReleaser, error) {
return nil, nil
i, err := funding.CreateItem(testExchange, asset.Futures, currency.BTC, decimal.Zero, decimal.Zero)
if err != nil {
return nil, err
}
j, err := funding.CreateItem(testExchange, asset.Futures, currency.USDT, decimal.Zero, decimal.Zero)
if err != nil {
return nil, err
}
return funding.CreateCollateral(i, j)
}
func (f *fakeFund) IncreaseAvailable(decimal.Decimal, gctorder.Side) {}
@@ -69,12 +98,23 @@ func (f *fakeFund) Release(decimal.Decimal, decimal.Decimal, gctorder.Side) erro
func TestReset(t *testing.T) {
t.Parallel()
e := Exchange{
CurrencySettings: []Settings{},
e := &Exchange{
CurrencySettings: []Settings{
{},
},
}
e.Reset()
if e.CurrencySettings != nil {
t.Error("expected nil")
err := e.Reset()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(e.CurrencySettings) > 0 {
t.Error("expected no entries")
}
e = nil
err = e.Reset()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrNilPointer)
}
}
@@ -85,7 +125,7 @@ func TestSetCurrency(t *testing.T) {
if len(e.CurrencySettings) != 0 {
t.Error("expected 0")
}
f := &ftx.FTX{}
f := &binance.Binance{}
f.Name = testExchange
cs := &Settings{
Exchange: f,
@@ -95,8 +135,8 @@ func TestSetCurrency(t *testing.T) {
}
e.SetExchangeAssetCurrencySettings(asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
result, err := e.GetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !result.UseRealOrders {
t.Error("expected true")
@@ -148,23 +188,17 @@ func TestPlaceOrder(t *testing.T) {
t.Fatal(err)
}
exch.SetDefaults()
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = exch.Setup(cfg)
if err != nil {
t.Fatal(err)
}
exchB := exch.GetBase()
exchB.States = currencystate.NewCurrencyStates()
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false, false, 0)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
err = bot.OrderManager.Start()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
e := Exchange{}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, nil, nil)
@@ -188,13 +222,13 @@ func TestPlaceOrder(t *testing.T) {
f.AssetType = asset.Spot
f.Direction = gctorder.Buy
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, f, bot.OrderManager)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, true, true, f, bot.OrderManager)
if !errors.Is(err, exchange.ErrAuthenticationSupportNotEnabled) {
t.Errorf("received: %v but expected: %v", err, exchange.ErrAuthenticationSupportNotEnabled)
if !errors.Is(err, exchange.ErrCredentialsAreEmpty) {
t.Errorf("received: %v but expected: %v", err, exchange.ErrCredentialsAreEmpty)
}
}
@@ -208,23 +242,17 @@ func TestExecuteOrder(t *testing.T) {
t.Fatal(err)
}
exch.SetDefaults()
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = exch.Setup(cfg)
if err != nil {
t.Fatal(err)
}
exchB := exch.GetBase()
exchB.States = currencystate.NewCurrencyStates()
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false, false, 0)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bot.OrderManager.Start()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
p := currency.NewPair(currency.BTC, currency.USDT)
@@ -233,7 +261,7 @@ func TestExecuteOrder(t *testing.T) {
if err != nil {
t.Fatal(err)
}
f := &ftx.FTX{}
f := &binance.Binance{}
f.Name = testExchange
cs := Settings{
Exchange: f,
@@ -267,21 +295,25 @@ func TestExecuteOrder(t *testing.T) {
Interval: 0,
Candles: []gctkline.Candle{
{
Close: 1,
High: 1,
Low: 1,
Volume: 1,
Close: 1,
High: 1,
Low: 1,
Time: time.Now(),
},
},
}
d := &kline.DataFromKline{
Base: &data.Base{},
Item: item,
}
err = d.Load()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
d.Next()
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, errNoCurrencySettingsFound) {
t.Error(err)
@@ -292,8 +324,31 @@ func TestExecuteOrder(t *testing.T) {
o.Direction = gctorder.Sell
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, exchange.ErrAuthenticationSupportNotEnabled) {
t.Errorf("received: %v but expected: %v", err, exchange.ErrAuthenticationSupportNotEnabled)
if !errors.Is(err, exchange.ErrCredentialsAreEmpty) {
t.Errorf("received: %v but expected: %v", err, exchange.ErrCredentialsAreEmpty)
}
o.LiquidatingPosition = true
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, nil) {
t.Errorf("received: %v but expected: %v", err, nil)
}
o.AssetType = asset.Futures
e.CurrencySettings[0].Asset = asset.Futures
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, nil) {
t.Errorf("received: %v but expected: %v", err, nil)
}
o.LiquidatingPosition = false
o.Amount = decimal.Zero
o.AssetType = asset.Spot
e.CurrencySettings[0].Asset = asset.Spot
e.CurrencySettings[0].UseRealOrders = false
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, gctorder.ErrAmountIsInvalid) {
t.Errorf("received: %v but expected: %v", err, gctorder.ErrAmountIsInvalid)
}
}
@@ -307,24 +362,17 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
t.Fatal(err)
}
exch.SetDefaults()
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = exch.Setup(cfg)
if err != nil {
t.Fatal(err)
}
exchB := exch.GetBase()
exchB.States = currencystate.NewCurrencyStates()
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false, false, 0)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = bot.OrderManager.Start()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
p := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
@@ -342,7 +390,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
if err != nil {
t.Fatal(err)
}
f := &ftx.FTX{}
f := &binance.Binance{}
f.Name = testExchange
cs := Settings{
Exchange: f,
@@ -378,26 +426,31 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
}
d := &kline.DataFromKline{
Base: &data.Base{},
Item: gctkline.Item{
Exchange: "",
Pair: currency.EMPTYPAIR,
Asset: asset.Empty,
Interval: 0,
Exchange: testExchange,
Pair: p,
Asset: asset.Spot,
Interval: gctkline.FifteenMin,
Candles: []gctkline.Candle{
{
Close: 1,
High: 1,
Low: 1,
Volume: 1,
Time: time.Now(),
},
},
},
}
err = d.Load()
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
d.Next()
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
@@ -465,8 +518,8 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, exchange.ErrAuthenticationSupportNotEnabled) {
t.Errorf("received: %v but expected: %v", err, exchange.ErrAuthenticationSupportNotEnabled)
if !errors.Is(err, exchange.ErrCredentialsAreEmpty) {
t.Errorf("received: %v but expected: %v", err, exchange.ErrCredentialsAreEmpty)
}
}
@@ -587,7 +640,7 @@ func TestAllocateFundsPostOrder(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
expectedError = common.ErrNilArguments
expectedError = gctcommon.ErrNilPointer
f := &fill.Fill{
Base: &event.Base{
AssetType: asset.Spot,
@@ -605,7 +658,7 @@ func TestAllocateFundsPostOrder(t *testing.T) {
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
item2, err := funding.CreateItem(testExchange, asset.Spot, currency.USD, decimal.NewFromInt(1337), decimal.Zero)
item2, err := funding.CreateItem(testExchange, asset.Spot, currency.USDT, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
@@ -646,7 +699,7 @@ func TestAllocateFundsPostOrder(t *testing.T) {
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}
item4, err := funding.CreateItem(testExchange, asset.Futures, currency.USD, decimal.NewFromInt(1337), decimal.Zero)
item4, err := funding.CreateItem(testExchange, asset.Futures, currency.USDT, decimal.NewFromInt(1337), decimal.Zero)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v'", err, expectedError)
}

View File

@@ -16,7 +16,9 @@ import (
)
var (
errDataMayBeIncorrect = errors.New("data may be incorrect")
// ErrCannotTransact returns when its an issue to do nothing for an event
ErrCannotTransact = errors.New("cannot transact")
errExceededPortfolioLimit = errors.New("exceeded portfolio limit")
errNilCurrencySettings = errors.New("received nil currency settings")
errInvalidDirection = errors.New("received invalid order direction")
@@ -28,7 +30,7 @@ type ExecutionHandler interface {
SetExchangeAssetCurrencySettings(asset.Item, currency.Pair, *Settings)
GetCurrencySettings(string, asset.Item, currency.Pair) (Settings, error)
ExecuteOrder(order.Event, data.Handler, *engine.OrderManager, funding.IFundReleaser) (fill.Event, error)
Reset()
Reset() error
}
// Exchange contains all the currency settings

View File

@@ -19,23 +19,19 @@ func TestAddSnapshot(t *testing.T) {
}
err = m.AddSnapshot(&Snapshot{
Offset: 0,
Timestamp: tt,
Orders: nil,
}, false)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(m.Snapshots) != 1 {
t.Error("expected 1")
}
err = m.AddSnapshot(&Snapshot{
Offset: 0,
Timestamp: tt,
Orders: nil,
}, true)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(m.Snapshots) != 1 {
t.Error("expected 1")
@@ -57,13 +53,13 @@ func TestGetSnapshotAtTime(t *testing.T) {
},
},
}, false)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
var snappySnap Snapshot
snappySnap, err = m.GetSnapshotAtTime(tt)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if len(snappySnap.Orders) == 0 {
t.Fatal("expected an order")
@@ -90,12 +86,10 @@ func TestGetLatestSnapshot(t *testing.T) {
}
tt := time.Now()
err := m.AddSnapshot(&Snapshot{
Offset: 0,
Timestamp: tt,
Orders: nil,
}, false)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = m.AddSnapshot(&Snapshot{
Offset: 1,

View File

@@ -7,22 +7,24 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// Create makes a Holding struct to track total values of strategy holdings over the course of a backtesting run
func Create(ev ClosePriceReader, fundReader funding.IFundReader) (Holding, error) {
func Create(ev ClosePriceReader, fundReader funding.IFundReader) (*Holding, error) {
if ev == nil {
return Holding{}, common.ErrNilEvent
return nil, common.ErrNilEvent
}
if ev.GetAssetType().IsFutures() {
a := ev.GetAssetType()
switch {
case a.IsFutures():
funds, err := fundReader.GetCollateralReader()
if err != nil {
return Holding{}, err
return nil, err
}
return Holding{
return &Holding{
Offset: ev.GetOffset(),
Pair: ev.Pair(),
Asset: ev.GetAssetType(),
@@ -32,16 +34,16 @@ func Create(ev ClosePriceReader, fundReader funding.IFundReader) (Holding, error
QuoteSize: funds.InitialFunds(),
TotalInitialValue: funds.InitialFunds(),
}, nil
} else if ev.GetAssetType() == asset.Spot {
case a == asset.Spot:
funds, err := fundReader.GetPairReader()
if err != nil {
return Holding{}, err
return nil, err
}
if funds.QuoteInitialFunds().LessThan(decimal.Zero) {
return Holding{}, ErrInitialFundsZero
return nil, ErrInitialFundsZero
}
return Holding{
return &Holding{
Offset: ev.GetOffset(),
Pair: ev.Pair(),
Asset: ev.GetAssetType(),
@@ -53,8 +55,9 @@ func Create(ev ClosePriceReader, fundReader funding.IFundReader) (Holding, error
BaseSize: funds.BaseInitialFunds(),
TotalInitialValue: funds.QuoteInitialFunds().Add(funds.BaseInitialFunds().Mul(ev.GetClosePrice())),
}, nil
default:
return nil, fmt.Errorf("%v %w", ev.GetAssetType(), asset.ErrNotSupported)
}
return Holding{}, fmt.Errorf("%v %w", ev.GetAssetType(), asset.ErrNotSupported)
}
// Update calculates holding statistics for the events time
@@ -65,11 +68,15 @@ func (h *Holding) Update(e fill.Event, f funding.IFundReader) error {
}
// UpdateValue calculates the holding's value for a data event's time and price
func (h *Holding) UpdateValue(d common.DataEventHandler) {
func (h *Holding) UpdateValue(d common.Event) error {
if d == nil {
return fmt.Errorf("%w event", gctcommon.ErrNilPointer)
}
h.Timestamp = d.GetTime()
latest := d.GetClosePrice()
h.Offset = d.GetOffset()
h.scaleValuesToCurrentPrice(latest)
return nil
}
func (h *Holding) update(e fill.Event, f funding.IFundReader) error {

View File

@@ -11,6 +11,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -62,15 +63,15 @@ func TestCreate(t *testing.T) {
_, err = Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, pair(t))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Futures},
}, collateral(t))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -79,8 +80,8 @@ func TestUpdate(t *testing.T) {
h, err := Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, pair(t))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
t1 := h.Timestamp // nolint:ifshort,nolintlint // false positive and triggers only on Windows
err = h.Update(&fill.Fill{
@@ -88,8 +89,8 @@ func TestUpdate(t *testing.T) {
Time: time.Now(),
},
}, pair(t))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if t1.Equal(h.Timestamp) {
t.Errorf("expected '%v' received '%v'", h.Timestamp, t1)
@@ -102,14 +103,23 @@ func TestUpdateValue(t *testing.T) {
h, err := Create(&fill.Fill{
Base: b,
}, pair(t))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = h.UpdateValue(nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNilPointer)
}
h.BaseSize = decimal.NewFromInt(1)
h.UpdateValue(&kline.Kline{
err = h.UpdateValue(&kline.Kline{
Base: b,
Close: decimal.NewFromInt(1337),
})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !h.BaseValue.Equal(decimal.NewFromInt(1337)) {
t.Errorf("expected '%v' received '%v'", h.BaseSize, decimal.NewFromInt(1337))
}
@@ -132,8 +142,8 @@ func TestUpdateBuyStats(t *testing.T) {
h, err := Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, pair(t))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = h.update(&fill.Fill{
@@ -166,8 +176,8 @@ func TestUpdateBuyStats(t *testing.T) {
Fee: 1,
},
}, p)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !h.BaseSize.Equal(p.BaseAvailable()) {
t.Errorf("expected '%v' received '%v'", 1, h.BaseSize)
@@ -221,8 +231,8 @@ func TestUpdateBuyStats(t *testing.T) {
Fee: 0.5,
},
}, p)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !h.BoughtAmount.Equal(decimal.NewFromFloat(1.5)) {
@@ -254,8 +264,8 @@ func TestUpdateSellStats(t *testing.T) {
h, err := Create(&fill.Fill{
Base: &event.Base{AssetType: asset.Spot},
}, p)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = h.update(&fill.Fill{
Base: &event.Base{
@@ -286,8 +296,8 @@ func TestUpdateSellStats(t *testing.T) {
Fee: 1,
},
}, p)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !h.BaseSize.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected '%v' received '%v'", 1, h.BaseSize)
@@ -344,8 +354,8 @@ func TestUpdateSellStats(t *testing.T) {
Fee: 1,
},
}, p)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !h.BoughtAmount.Equal(decimal.NewFromInt(1)) {

View File

@@ -49,6 +49,6 @@ type Holding struct {
// ClosePriceReader is used for holdings calculations
// without needing to consider event types
type ClosePriceReader interface {
common.EventHandler
common.Event
GetClosePrice() decimal.Decimal
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
@@ -16,20 +17,23 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
// OnSignal receives the event from the strategy on whether it has signalled to buy, do nothing or sell
// on buy/sell, the portfolio manager will size the order and assess the risk of the order
// if successful, it will pass on an order.Order to be used by the exchange event handler to place an order based on
// the portfolio manager's recommendations
func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds funding.IFundReserver) (*order.Order, error) {
if ev == nil || cs == nil {
return nil, common.ErrNilArguments
func (p *Portfolio) OnSignal(ev signal.Event, exchangeSettings *exchange.Settings, funds funding.IFundReserver) (*order.Order, error) {
if ev == nil {
return nil, fmt.Errorf("%w signal event", gctcommon.ErrNilPointer)
}
if exchangeSettings == nil {
return nil, fmt.Errorf("%w exchange settings", gctcommon.ErrNilPointer)
}
if p.sizeManager == nil {
return nil, errSizeManagerUnset
@@ -40,7 +44,6 @@ func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds fundi
if funds == nil {
return nil, funding.ErrFundsNotFound
}
o := &order.Order{
Base: ev.GetBase(),
Direction: ev.GetDirection(),
@@ -52,7 +55,7 @@ func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds fundi
return o, errInvalidDirection
}
lookup := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
lookup := p.exchangeAssetPairPortfolioSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair().Base.Item][ev.Pair().Quote.Item]
if lookup == nil {
return nil, fmt.Errorf("%w for %v %v %v",
errNoPortfolioSettings,
@@ -98,11 +101,11 @@ func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds fundi
return nil, errNoHoldings
}
sizingFunds = positions[len(positions)-1].LatestSize
d := positions[len(positions)-1].OpeningDirection
d := positions[len(positions)-1].LatestDirection
switch d {
case gctorder.Short:
case gctorder.Short, gctorder.Sell, gctorder.Ask:
side = gctorder.Long
case gctorder.Long:
case gctorder.Long, gctorder.Buy, gctorder.Bid:
side = gctorder.Short
}
} else {
@@ -116,7 +119,7 @@ func (p *Portfolio) OnSignal(ev signal.Event, cs *exchange.Settings, funds fundi
if sizingFunds.LessThanOrEqual(decimal.Zero) {
return cannotPurchase(ev, o)
}
sizedOrder, err := p.sizeOrder(ev, cs, o, sizingFunds, funds)
sizedOrder, err := p.sizeOrder(ev, exchangeSettings, o, sizingFunds, funds)
if err != nil {
return sizedOrder, err
}
@@ -134,7 +137,7 @@ func cannotPurchase(ev signal.Event, o *order.Order) (*order.Order, error) {
return nil, common.ErrNilEvent
}
if o == nil {
return nil, fmt.Errorf("%w received nil order for %v %v %v", common.ErrNilArguments, ev.GetExchange(), ev.GetAssetType(), ev.Pair())
return nil, fmt.Errorf("%w received nil order for %v %v %v", gctcommon.ErrNilPointer, ev.GetExchange(), ev.GetAssetType(), ev.Pair())
}
o.AppendReason(notEnoughFundsTo + " " + ev.GetDirection().Lower())
switch ev.GetDirection() {
@@ -156,7 +159,7 @@ func cannotPurchase(ev signal.Event, o *order.Order) (*order.Order, error) {
func (p *Portfolio) evaluateOrder(d common.Directioner, originalOrderSignal, ev *order.Order) (*order.Order, error) {
var evaluatedOrder *order.Order
cm, err := p.GetComplianceManager(originalOrderSignal.GetExchange(), originalOrderSignal.GetAssetType(), originalOrderSignal.Pair())
cm, err := p.getComplianceManager(originalOrderSignal.GetExchange(), originalOrderSignal.GetAssetType(), originalOrderSignal.Pair())
if err != nil {
return nil, err
}
@@ -231,46 +234,34 @@ func (p *Portfolio) OnFill(ev fill.Event, funds funding.IFundReleaser) (fill.Eve
if ev == nil {
return nil, common.ErrNilEvent
}
lookup := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
lookup := p.exchangeAssetPairPortfolioSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair().Base.Item][ev.Pair().Quote.Item]
if lookup == nil {
return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, ev.GetExchange(), ev.GetAssetType(), ev.Pair())
}
var err error
// Get the holding from the previous iteration, create it if it doesn't yet have a timestamp
h := lookup.GetHoldingsForTime(ev.GetTime().Add(-ev.GetInterval().Duration()))
if !h.Timestamp.IsZero() {
err = h.Update(ev, funds)
h, err := lookup.GetHoldingsForTime(ev.GetTime().Add(-ev.GetInterval().Duration()))
if err != nil {
if !errors.Is(err, errNoHoldings) {
return nil, err
}
h, err = holdings.Create(ev, funds)
if err != nil {
return nil, err
}
} else {
h = lookup.GetLatestHoldings()
if h.Timestamp.IsZero() {
h, err = holdings.Create(ev, funds)
if err != nil {
return nil, err
}
} else {
err = h.Update(ev, funds)
if err != nil {
return nil, err
}
}
}
err = p.setHoldingsForOffset(&h, true)
if errors.Is(err, errNoHoldings) {
err = p.setHoldingsForOffset(&h, false)
}
if err != nil {
log.Error(common.Portfolio, err)
}
err = h.Update(ev, funds)
if err != nil {
return nil, err
}
err = p.SetHoldingsForTimestamp(h)
if err != nil {
return nil, err
}
err = p.addComplianceSnapshot(ev)
if err != nil {
log.Error(common.Portfolio, err)
return nil, err
}
return ev, nil
}
@@ -280,7 +271,7 @@ func (p *Portfolio) addComplianceSnapshot(fillEvent fill.Event) error {
if fillEvent == nil {
return common.ErrNilEvent
}
complianceManager, err := p.GetComplianceManager(fillEvent.GetExchange(), fillEvent.GetAssetType(), fillEvent.Pair())
complianceManager, err := p.getComplianceManager(fillEvent.GetExchange(), fillEvent.GetAssetType(), fillEvent.Pair())
if err != nil {
return err
}
@@ -306,40 +297,9 @@ func (p *Portfolio) addComplianceSnapshot(fillEvent fill.Event) error {
return complianceManager.AddSnapshot(snap, false)
}
func (p *Portfolio) setHoldingsForOffset(h *holdings.Holding, overwriteExisting bool) error {
if h.Timestamp.IsZero() {
return errHoldingsNoTimestamp
}
lookup, ok := p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair]
if !ok {
return fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, h.Exchange, h.Asset, h.Pair)
}
if overwriteExisting && len(lookup.HoldingsSnapshots) == 0 {
return errNoHoldings
}
for i := len(lookup.HoldingsSnapshots) - 1; i >= 0; i-- {
if lookup.HoldingsSnapshots[i].Offset == h.Offset {
if overwriteExisting {
lookup.HoldingsSnapshots[i] = *h
p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair] = lookup
return nil
}
return errHoldingsAlreadySet
}
}
if overwriteExisting {
return fmt.Errorf("%w at %v", errNoHoldings, h.Timestamp)
}
lookup.HoldingsSnapshots = append(lookup.HoldingsSnapshots, *h)
p.exchangeAssetPairSettings[h.Exchange][h.Asset][h.Pair] = lookup
return nil
}
// 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()]
func (p *Portfolio) GetLatestOrderSnapshotForEvent(e common.Event) (compliance.Snapshot, error) {
eapSettings, ok := p.exchangeAssetPairPortfolioSettings[e.GetExchange()][e.GetAssetType()][e.Pair().Base.Item][e.Pair().Quote.Item]
if !ok {
return compliance.Snapshot{}, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, e.GetExchange(), e.GetAssetType(), e.Pair())
}
@@ -349,10 +309,12 @@ func (p *Portfolio) GetLatestOrderSnapshotForEvent(e common.EventHandler) (compl
// 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 _, exchangeMap := range p.exchangeAssetPairPortfolioSettings {
for _, assetMap := range exchangeMap {
for _, pairMap := range assetMap {
resp = append(resp, pairMap.ComplianceManager.GetLatestSnapshot())
for _, baseMap := range assetMap {
for _, quoteMap := range baseMap {
resp = append(resp, quoteMap.ComplianceManager.GetLatestSnapshot())
}
}
}
}
@@ -362,97 +324,28 @@ func (p *Portfolio) GetLatestOrderSnapshots() ([]compliance.Snapshot, error) {
return resp, nil
}
// GetComplianceManager returns the order snapshots for a given exchange, asset, pair
func (p *Portfolio) GetComplianceManager(exchangeName string, a asset.Item, cp currency.Pair) (*compliance.Manager, error) {
lookup := p.exchangeAssetPairSettings[exchangeName][a][cp]
// GetLatestComplianceSnapshot returns the latest compliance snapshot for a given exchange, asset, pair
func (p *Portfolio) GetLatestComplianceSnapshot(exchangeName string, a asset.Item, cp currency.Pair) (*compliance.Snapshot, error) {
cm, err := p.getComplianceManager(exchangeName, a, cp)
if err != nil {
return nil, err
}
snap := cm.GetLatestSnapshot()
return &snap, nil
}
// getComplianceManager returns the order snapshots for a given exchange, asset, pair
func (p *Portfolio) getComplianceManager(exchangeName string, a asset.Item, cp currency.Pair) (*compliance.Manager, error) {
lookup := p.exchangeAssetPairPortfolioSettings[exchangeName][a][cp.Base.Item][cp.Quote.Item]
if lookup == nil {
return nil, fmt.Errorf("%w for %v %v %v could not retrieve compliance manager", errNoPortfolioSettings, exchangeName, a, cp)
}
return &lookup.ComplianceManager, nil
}
// UpdateHoldings updates the portfolio holdings for the data event
func (p *Portfolio) UpdateHoldings(e common.DataEventHandler, funds funding.IFundReleaser) error {
if e == nil {
return common.ErrNilEvent
}
if funds == nil {
return funding.ErrFundsNotFound
}
settings, err := p.getSettings(e.GetExchange(), e.GetAssetType(), e.Pair())
if err != nil {
return fmt.Errorf("%v %v %v %w", e.GetExchange(), e.GetAssetType(), e.Pair(), err)
}
h := settings.GetLatestHoldings()
if h.Timestamp.IsZero() {
h, err = holdings.Create(e, funds)
if err != nil {
return err
}
}
h.UpdateValue(e)
err = p.setHoldingsForOffset(&h, true)
if errors.Is(err, errNoHoldings) {
err = p.setHoldingsForOffset(&h, false)
}
return err
}
// GetLatestHoldingsForAllCurrencies will return the current holdings for all loaded currencies
// this is useful to assess the position of your entire portfolio in order to help with risk decisions
func (p *Portfolio) GetLatestHoldingsForAllCurrencies() []holdings.Holding {
var resp []holdings.Holding
for _, x := range p.exchangeAssetPairSettings {
for _, y := range x {
for _, z := range y {
holds := z.GetLatestHoldings()
if !holds.Timestamp.IsZero() {
resp = append(resp, holds)
}
}
}
}
return resp
}
// ViewHoldingAtTimePeriod retrieves a snapshot of holdings at a specific time period,
// returning empty when not found
func (p *Portfolio) ViewHoldingAtTimePeriod(ev common.EventHandler) (*holdings.Holding, error) {
exchangeAssetPairSettings := p.exchangeAssetPairSettings[ev.GetExchange()][ev.GetAssetType()][ev.Pair()]
if exchangeAssetPairSettings == nil {
return nil, fmt.Errorf("%w for %v %v %v", errNoHoldings, ev.GetExchange(), ev.GetAssetType(), ev.Pair())
}
for i := len(exchangeAssetPairSettings.HoldingsSnapshots) - 1; i >= 0; i-- {
if ev.GetTime().Equal(exchangeAssetPairSettings.HoldingsSnapshots[i].Timestamp) {
return &exchangeAssetPairSettings.HoldingsSnapshots[i], nil
}
}
return nil, fmt.Errorf("%w for %v %v %v at %v", errNoHoldings, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetTime())
}
// GetLatestHoldings returns the latest holdings after being sorted by time
func (s *Settings) GetLatestHoldings() holdings.Holding {
if len(s.HoldingsSnapshots) == 0 {
return holdings.Holding{}
}
return s.HoldingsSnapshots[len(s.HoldingsSnapshots)-1]
}
// GetHoldingsForTime returns the holdings for a time period, or an empty holding if not found
func (s *Settings) GetHoldingsForTime(t time.Time) holdings.Holding {
for i := len(s.HoldingsSnapshots) - 1; i >= 0; i-- {
if s.HoldingsSnapshots[i].Timestamp.Equal(t) {
return s.HoldingsSnapshots[i]
}
}
return holdings.Holding{}
}
// GetPositions returns all futures positions for an event's exchange, asset, pair
func (p *Portfolio) GetPositions(e common.EventHandler) ([]gctorder.Position, error) {
func (p *Portfolio) GetPositions(e common.Event) ([]gctorder.Position, error) {
settings, err := p.getFuturesSettingsFromEvent(e)
if err != nil {
return nil, err
@@ -461,7 +354,7 @@ func (p *Portfolio) GetPositions(e common.EventHandler) ([]gctorder.Position, er
}
// GetLatestPosition returns all futures positions for an event's exchange, asset, pair
func (p *Portfolio) GetLatestPosition(e common.EventHandler) (*gctorder.Position, error) {
func (p *Portfolio) GetLatestPosition(e common.Event) (*gctorder.Position, error) {
settings, err := p.getFuturesSettingsFromEvent(e)
if err != nil {
return nil, err
@@ -475,7 +368,7 @@ func (p *Portfolio) GetLatestPosition(e common.EventHandler) (*gctorder.Position
// UpdatePNL will analyse any futures orders that have been placed over the backtesting run
// that are not closed and calculate their PNL
func (p *Portfolio) UpdatePNL(e common.EventHandler, closePrice decimal.Decimal) error {
func (p *Portfolio) UpdatePNL(e common.Event, closePrice decimal.Decimal) error {
settings, err := p.getFuturesSettingsFromEvent(e)
if err != nil {
return err
@@ -495,7 +388,7 @@ func (p *Portfolio) TrackFuturesOrder(ev fill.Event, fund funding.IFundReleaser)
return nil, common.ErrNilEvent
}
if fund == nil {
return nil, fmt.Errorf("%w missing funding", common.ErrNilArguments)
return nil, fmt.Errorf("%w missing funding", gctcommon.ErrNilPointer)
}
detail := ev.GetOrder()
if detail == nil {
@@ -511,7 +404,7 @@ func (p *Portfolio) TrackFuturesOrder(ev fill.Event, fund funding.IFundReleaser)
}
settings, err := p.getSettings(detail.Exchange, detail.AssetType, detail.Pair)
if err != nil {
return nil, fmt.Errorf("%v %v %v %w", detail.Exchange, detail.AssetType, detail.Pair, err)
return nil, fmt.Errorf("%w", err)
}
err = settings.FuturesTracker.TrackNewOrder(detail)
@@ -552,13 +445,13 @@ func (p *Portfolio) TrackFuturesOrder(ev fill.Event, fund funding.IFundReleaser)
// GetLatestPNLForEvent takes in an event and returns the latest PNL data
// if it exists
func (p *Portfolio) GetLatestPNLForEvent(e common.EventHandler) (*PNLSummary, error) {
func (p *Portfolio) GetLatestPNLForEvent(e common.Event) (*PNLSummary, error) {
if e == nil {
return nil, common.ErrNilEvent
}
response := &PNLSummary{
Exchange: e.GetExchange(),
Item: e.GetAssetType(),
Asset: e.GetAssetType(),
Pair: e.Pair(),
Offset: e.GetOffset(),
}
@@ -577,15 +470,15 @@ func (p *Portfolio) GetLatestPNLForEvent(e common.EventHandler) (*PNLSummary, er
// CheckLiquidationStatus checks funding against position
// and liquidates and removes funding if position unable to continue
func (p *Portfolio) CheckLiquidationStatus(ev common.DataEventHandler, collateralReader funding.ICollateralReader, pnl *PNLSummary) error {
func (p *Portfolio) CheckLiquidationStatus(ev data.Event, collateralReader funding.ICollateralReader, pnl *PNLSummary) error {
if ev == nil {
return common.ErrNilEvent
}
if collateralReader == nil {
return fmt.Errorf("%w collateral reader missing", common.ErrNilArguments)
return fmt.Errorf("%w collateral reader missing", gctcommon.ErrNilPointer)
}
if pnl == nil {
return fmt.Errorf("%w pnl summary missing", common.ErrNilArguments)
return fmt.Errorf("%w pnl summary missing", gctcommon.ErrNilPointer)
}
availableFunds := collateralReader.AvailableFunds()
position, err := p.GetLatestPosition(ev)
@@ -602,81 +495,87 @@ func (p *Portfolio) CheckLiquidationStatus(ev common.DataEventHandler, collatera
}
// CreateLiquidationOrdersForExchange creates liquidation orders, for any that exist on the same exchange where a liquidation is occurring
func (p *Portfolio) CreateLiquidationOrdersForExchange(ev common.DataEventHandler, funds funding.IFundingManager) ([]order.Event, error) {
func (p *Portfolio) CreateLiquidationOrdersForExchange(ev data.Event, funds funding.IFundingManager) ([]order.Event, error) {
if ev == nil {
return nil, common.ErrNilEvent
}
if funds == nil {
return nil, fmt.Errorf("%w, requires funding manager", common.ErrNilArguments)
return nil, fmt.Errorf("%w, requires funding manager", gctcommon.ErrNilPointer)
}
var closingOrders []order.Event
assetPairSettings, ok := p.exchangeAssetPairSettings[ev.GetExchange()]
assetPairSettings, ok := p.exchangeAssetPairPortfolioSettings[ev.GetExchange()]
if !ok {
return nil, config.ErrExchangeNotFound
}
for item, pairMap := range assetPairSettings {
for pair, settings := range pairMap {
switch {
case item.IsFutures():
positions := settings.FuturesTracker.GetPositions()
if len(positions) == 0 {
continue
}
pos := positions[len(positions)-1]
if !pos.LatestSize.IsPositive() {
continue
}
direction := gctorder.Short
if pos.LatestDirection == gctorder.Short {
direction = gctorder.Long
}
closingOrders = append(closingOrders, &order.Order{
Base: &event.Base{
Offset: ev.GetOffset(),
Exchange: pos.Exchange,
Time: ev.GetTime(),
Interval: ev.GetInterval(),
CurrencyPair: pos.Pair,
UnderlyingPair: ev.GetUnderlyingPair(),
AssetType: pos.Asset,
Reasons: []string{"LIQUIDATED"},
},
Direction: direction,
Status: gctorder.Liquidated,
ClosePrice: ev.GetClosePrice(),
Amount: pos.LatestSize,
AllocatedFunds: pos.LatestSize,
OrderType: gctorder.Market,
LiquidatingPosition: true,
})
case item == asset.Spot:
allFunds := funds.GetAllFunding()
for i := range allFunds {
if allFunds[i].Asset.IsFutures() {
for item, baseMap := range assetPairSettings {
for b, quoteMap := range baseMap {
for q, settings := range quoteMap {
switch {
case item.IsFutures():
positions := settings.FuturesTracker.GetPositions()
if len(positions) == 0 {
continue
}
if allFunds[i].Currency.IsFiatCurrency() || allFunds[i].Currency.IsStableCurrency() {
// close orders for assets
// funding manager will zero for fiat/stable
pos := positions[len(positions)-1]
if !pos.LatestSize.IsPositive() {
continue
}
direction := gctorder.Short
if pos.LatestDirection == gctorder.Short {
direction = gctorder.Long
}
closingOrders = append(closingOrders, &order.Order{
Base: &event.Base{
Offset: ev.GetOffset(),
Exchange: ev.GetExchange(),
Time: ev.GetTime(),
Interval: ev.GetInterval(),
CurrencyPair: pair,
AssetType: item,
Reasons: []string{"LIQUIDATED"},
Offset: ev.GetOffset(),
Exchange: pos.Exchange,
Time: ev.GetTime(),
Interval: ev.GetInterval(),
CurrencyPair: pos.Pair,
UnderlyingPair: ev.GetUnderlyingPair(),
AssetType: pos.Asset,
Reasons: []string{"LIQUIDATED"},
},
Direction: gctorder.Sell,
Direction: direction,
Status: gctorder.Liquidated,
Amount: allFunds[i].Available,
ClosePrice: ev.GetClosePrice(),
Amount: pos.LatestSize,
AllocatedFunds: pos.LatestSize,
OrderType: gctorder.Market,
AllocatedFunds: allFunds[i].Available,
LiquidatingPosition: true,
})
case item == asset.Spot:
allFunds, err := funds.GetAllFunding()
if err != nil {
return nil, err
}
for i := range allFunds {
if allFunds[i].Asset.IsFutures() {
continue
}
if allFunds[i].Currency.IsFiatCurrency() || allFunds[i].Currency.IsStableCurrency() {
// close orders for assets
// funding manager will zero for fiat/stable
continue
}
cp := currency.NewPair(b.Currency(), q.Currency())
closingOrders = append(closingOrders, &order.Order{
Base: &event.Base{
Offset: ev.GetOffset(),
Exchange: ev.GetExchange(),
Time: ev.GetTime(),
Interval: ev.GetInterval(),
CurrencyPair: cp,
AssetType: item,
Reasons: []string{"LIQUIDATED"},
},
Direction: gctorder.Sell,
Status: gctorder.Liquidated,
Amount: allFunds[i].Available,
OrderType: gctorder.Market,
AllocatedFunds: allFunds[i].Available,
LiquidatingPosition: true,
})
}
}
}
}
@@ -685,7 +584,7 @@ func (p *Portfolio) CreateLiquidationOrdersForExchange(ev common.DataEventHandle
return closingOrders, nil
}
func (p *Portfolio) getFuturesSettingsFromEvent(e common.EventHandler) (*Settings, error) {
func (p *Portfolio) getFuturesSettingsFromEvent(e common.Event) (*Settings, error) {
if e == nil {
return nil, common.ErrNilEvent
}
@@ -705,56 +604,159 @@ func (p *Portfolio) getFuturesSettingsFromEvent(e common.EventHandler) (*Setting
}
func (p *Portfolio) getSettings(exch string, item asset.Item, pair currency.Pair) (*Settings, error) {
exchMap, ok := p.exchangeAssetPairSettings[strings.ToLower(exch)]
exch = strings.ToLower(exch)
settings, ok := p.exchangeAssetPairPortfolioSettings[exch][item][pair.Base.Item][pair.Quote.Item]
if !ok {
return nil, errExchangeUnset
return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, exch, item, pair)
}
itemMap, ok := exchMap[item]
if !ok {
return nil, errAssetUnset
}
pairSettings, ok := itemMap[pair]
if !ok {
return nil, errCurrencyPairUnset
}
return pairSettings, nil
return settings, nil
}
// GetLatestPNLs returns all PNL details in one array
func (p *Portfolio) GetLatestPNLs() []PNLSummary {
var result []PNLSummary
for exch, assetPairSettings := range p.exchangeAssetPairSettings {
for ai, pairSettings := range assetPairSettings {
if !ai.IsFutures() {
continue
}
for cp, settings := range pairSettings {
if settings == nil {
continue
}
if settings.FuturesTracker == nil {
continue
}
summary := PNLSummary{
Exchange: exch,
Item: ai,
Pair: cp,
}
positions := settings.FuturesTracker.GetPositions()
if len(positions) > 0 {
pnlHistory := positions[len(positions)-1].PNLHistory
if len(pnlHistory) > 0 {
summary.Result = pnlHistory[len(pnlHistory)-1]
summary.CollateralCurrency = positions[0].CollateralCurrency
}
}
// SetHoldingsForTimestamp stores a holding snapshot for the holding's timestamp
func (p *Portfolio) SetHoldingsForTimestamp(h *holdings.Holding) error {
if h.Timestamp.IsZero() {
return errHoldingsNoTimestamp
}
lookup, err := p.getSettings(h.Exchange, h.Asset, h.Pair)
if err != nil {
return err
}
lookup.HoldingsSnapshots[h.Timestamp.UnixNano()] = h
return nil
}
result = append(result, summary)
// UpdateHoldings updates the portfolio holdings for the data event
func (p *Portfolio) UpdateHoldings(e data.Event, funds funding.IFundReleaser) error {
if e == nil {
return common.ErrNilEvent
}
if funds == nil {
return funding.ErrFundsNotFound
}
settings, err := p.getSettings(e.GetExchange(), e.GetAssetType(), e.Pair())
if err != nil {
return fmt.Errorf("%v %v %v %w", e.GetExchange(), e.GetAssetType(), e.Pair(), err)
}
h, err := settings.GetLatestHoldings()
if err != nil {
if !errors.Is(err, errNoHoldings) {
return err
}
h, err = holdings.Create(e, funds)
if err != nil {
return err
}
}
err = h.UpdateValue(e)
if err != nil {
return err
}
return p.SetHoldingsForTimestamp(h)
}
// GetLatestHoldingsForAllCurrencies will return the current holdings for all loaded currencies
// this is useful to assess the position of your entire portfolio in order to help with risk decisions
func (p *Portfolio) GetLatestHoldingsForAllCurrencies() []holdings.Holding {
var resp []holdings.Holding
for _, exchangeMap := range p.exchangeAssetPairPortfolioSettings {
for _, assetMap := range exchangeMap {
for _, baseMap := range assetMap {
for _, quoteMap := range baseMap {
holds, err := quoteMap.GetLatestHoldings()
if err != nil {
continue
}
resp = append(resp, *holds)
}
}
}
}
return result
return resp
}
// ViewHoldingAtTimePeriod retrieves a snapshot of holdings at a specific time period,
// returning an error if not found
func (p *Portfolio) ViewHoldingAtTimePeriod(ev common.Event) (*holdings.Holding, error) {
settings, err := p.getSettings(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
if err != nil {
return nil, err
}
h, ok := settings.HoldingsSnapshots[ev.GetTime().UnixNano()]
if !ok {
return nil, fmt.Errorf("%w for %v %v %v at %v", errNoHoldings, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetTime())
}
return h, nil
}
// GetLatestHoldings returns the latest holdings after being sorted by time
func (s *Settings) GetLatestHoldings() (*holdings.Holding, error) {
if len(s.HoldingsSnapshots) == 0 {
return nil, errNoHoldings
}
var latestTime int64
for k := range s.HoldingsSnapshots {
if k > latestTime {
latestTime = k
}
}
return s.HoldingsSnapshots[latestTime], nil
}
// GetHoldingsForTime returns the holdings for a time period, or an error holding if not found
func (s *Settings) GetHoldingsForTime(t time.Time) (*holdings.Holding, error) {
h, ok := s.HoldingsSnapshots[t.UnixNano()]
if !ok {
return nil, fmt.Errorf("%w for %v %v %v at %v", errNoHoldings, s.exchangeName, s.assetType, s.pair, t)
}
return h, nil
}
// SetHoldingsForEvent re-sets offset details at the events time,
// based on current funding levels
func (p *Portfolio) SetHoldingsForEvent(fm funding.IFundReader, e common.Event) error {
if fm == nil {
return fmt.Errorf("%w funding manager", gctcommon.ErrNilPointer)
}
if e == nil {
return common.ErrNilEvent
}
settings, err := p.getSettings(e.GetExchange(), e.GetAssetType(), e.Pair())
if err != nil {
return err
}
h, err := settings.GetHoldingsForTime(e.GetTime())
if err != nil {
if !errors.Is(err, errNoHoldings) {
return err
}
h, err = holdings.Create(e, fm)
if err != nil {
return err
}
}
if e.GetAssetType().IsFutures() {
var c funding.ICollateralReader
c, err = fm.GetCollateralReader()
if err != nil {
return err
}
h.BaseSize = c.CurrentHoldings()
h.QuoteSize = c.AvailableFunds()
} else {
var pr funding.IPairReader
pr, err = fm.GetPairReader()
if err != nil {
return err
}
h.BaseSize = pr.BaseAvailable()
h.QuoteSize = pr.QuoteAvailable()
}
err = h.UpdateValue(e)
if err != nil {
return err
}
return p.SetHoldingsForTimestamp(h)
}
// GetUnrealisedPNL returns a basic struct containing unrealised PNL

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
@@ -33,37 +34,37 @@ var (
errNoPortfolioSettings = errors.New("no portfolio settings")
errNoHoldings = errors.New("no holdings found")
errHoldingsNoTimestamp = errors.New("holding with unset timestamp received")
errHoldingsAlreadySet = errors.New("holding already set")
errUnsetFuturesTracker = errors.New("portfolio settings futures tracker unset")
)
// Portfolio stores all holdings and rules to assess orders, allowing the portfolio manager to
// modify, accept or reject strategy signals
type Portfolio struct {
riskFreeRate decimal.Decimal
sizeManager SizeHandler
riskManager risk.Handler
exchangeAssetPairSettings map[string]map[asset.Item]map[currency.Pair]*Settings
riskFreeRate decimal.Decimal
sizeManager SizeHandler
riskManager risk.Handler
exchangeAssetPairPortfolioSettings map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings
}
// Handler contains all functions expected to operate a portfolio manager
type Handler interface {
OnSignal(signal.Event, *exchange.Settings, funding.IFundReserver) (*order.Order, error)
OnFill(fill.Event, funding.IFundReleaser) (fill.Event, error)
GetLatestOrderSnapshotForEvent(common.EventHandler) (compliance.Snapshot, error)
GetLatestOrderSnapshotForEvent(common.Event) (compliance.Snapshot, error)
GetLatestOrderSnapshots() ([]compliance.Snapshot, error)
ViewHoldingAtTimePeriod(common.EventHandler) (*holdings.Holding, error)
setHoldingsForOffset(*holdings.Holding, bool) error
UpdateHoldings(common.DataEventHandler, funding.IFundReleaser) error
GetComplianceManager(string, asset.Item, currency.Pair) (*compliance.Manager, error)
GetPositions(common.EventHandler) ([]gctorder.Position, error)
ViewHoldingAtTimePeriod(common.Event) (*holdings.Holding, error)
SetHoldingsForTimestamp(*holdings.Holding) error
UpdateHoldings(data.Event, funding.IFundReleaser) error
GetPositions(common.Event) ([]gctorder.Position, error)
TrackFuturesOrder(fill.Event, funding.IFundReleaser) (*PNLSummary, error)
UpdatePNL(common.EventHandler, decimal.Decimal) error
GetLatestPNLForEvent(common.EventHandler) (*PNLSummary, error)
GetLatestPNLs() []PNLSummary
CheckLiquidationStatus(common.DataEventHandler, funding.ICollateralReader, *PNLSummary) error
CreateLiquidationOrdersForExchange(common.DataEventHandler, funding.IFundingManager) ([]order.Event, error)
Reset()
UpdatePNL(common.Event, decimal.Decimal) error
GetLatestPNLForEvent(common.Event) (*PNLSummary, error)
CheckLiquidationStatus(data.Event, funding.ICollateralReader, *PNLSummary) error
CreateLiquidationOrdersForExchange(data.Event, funding.IFundingManager) ([]order.Event, error)
GetLatestHoldingsForAllCurrencies() []holdings.Holding
Reset() error
SetHoldingsForEvent(funding.IFundReader, common.Event) error
GetLatestComplianceSnapshot(string, asset.Item, currency.Pair) (*compliance.Snapshot, error)
}
// SizeHandler is the interface to help size orders
@@ -74,10 +75,14 @@ type SizeHandler interface {
// Settings holds all important information for the portfolio manager
// to assess purchasing decisions
type Settings struct {
exchangeName string
assetType asset.Item
pair currency.Pair
BuySideSizing exchange.MinMax
SellSideSizing exchange.MinMax
Leverage exchange.Leverage
HoldingsSnapshots []holdings.Holding
HoldingsSnapshots map[int64]*holdings.Holding
ComplianceManager compliance.Manager
Exchange gctexchange.IBotExchange
FuturesTracker *gctorder.MultiPositionTracker
@@ -87,7 +92,7 @@ type Settings struct {
// exchange details
type PNLSummary struct {
Exchange string
Item asset.Item
Asset asset.Item
Pair currency.Pair
CollateralCurrency currency.Code
Offset int64

View File

@@ -8,6 +8,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
)
@@ -15,7 +16,7 @@ import (
// we are in a position to follow through with an order
func (r *Risk) EvaluateOrder(o order.Event, latestHoldings []holdings.Holding, s compliance.Snapshot) (*order.Order, error) {
if o == nil || latestHoldings == nil {
return nil, common.ErrNilArguments
return nil, gctcommon.ErrNilPointer
}
retOrder, ok := o.(*order.Order)
if !ok {
@@ -23,8 +24,8 @@ func (r *Risk) EvaluateOrder(o order.Event, latestHoldings []holdings.Holding, s
}
ex := o.GetExchange()
a := o.GetAssetType()
p := o.Pair()
lookup, ok := r.CurrencySettings[ex][a][p]
p := o.Pair().Format(currency.EMPTYFORMAT)
lookup, ok := r.CurrencySettings[ex][a][p.Base.Item][p.Quote.Item]
if !ok {
return nil, fmt.Errorf("%v %v %v %w", ex, a, p, errNoCurrencySettings)
}

View File

@@ -5,11 +5,11 @@ import (
"testing"
"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/order"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -54,7 +54,7 @@ func TestEvaluateOrder(t *testing.T) {
t.Parallel()
r := Risk{}
_, err := r.EvaluateOrder(nil, nil, compliance.Snapshot{})
if !errors.Is(err, common.ErrNilArguments) {
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Error(err)
}
p := currency.NewPair(currency.BTC, currency.USDT)
@@ -68,15 +68,16 @@ func TestEvaluateOrder(t *testing.T) {
},
}
h := []holdings.Holding{}
r.CurrencySettings = make(map[string]map[asset.Item]map[currency.Pair]*CurrencySettings)
r.CurrencySettings[e] = make(map[asset.Item]map[currency.Pair]*CurrencySettings)
r.CurrencySettings[e][a] = make(map[currency.Pair]*CurrencySettings)
r.CurrencySettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencySettings)
r.CurrencySettings[e] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencySettings)
r.CurrencySettings[e][a] = make(map[*currency.Item]map[*currency.Item]*CurrencySettings)
r.CurrencySettings[e][a][p.Base.Item] = make(map[*currency.Item]*CurrencySettings)
_, err = r.EvaluateOrder(o, h, compliance.Snapshot{})
if !errors.Is(err, errNoCurrencySettings) {
t.Error(err)
}
r.CurrencySettings[e][a][p] = &CurrencySettings{
r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item] = &CurrencySettings{
MaximumOrdersWithLeverageRatio: decimal.NewFromFloat(0.3),
MaxLeverageRate: decimal.NewFromFloat(0.3),
MaximumHoldingRatio: decimal.NewFromFloat(0.3),
@@ -87,15 +88,15 @@ func TestEvaluateOrder(t *testing.T) {
BaseSize: decimal.NewFromInt(1),
})
_, err = r.EvaluateOrder(o, h, compliance.Snapshot{})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
h = append(h, holdings.Holding{
Pair: currency.NewPair(currency.DOGE, currency.USDT),
})
o.Leverage = decimal.NewFromFloat(1.1)
r.CurrencySettings[e][a][p].MaximumHoldingRatio = decimal.Zero
r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaximumHoldingRatio = decimal.Zero
_, err = r.EvaluateOrder(o, h, compliance.Snapshot{})
if !errors.Is(err, errLeverageNotAllowed) {
t.Error(err)
@@ -107,14 +108,14 @@ func TestEvaluateOrder(t *testing.T) {
}
r.MaximumLeverage = decimal.NewFromInt(33)
r.CurrencySettings[e][a][p].MaxLeverageRate = decimal.NewFromInt(33)
r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaxLeverageRate = decimal.NewFromInt(33)
_, err = r.EvaluateOrder(o, h, compliance.Snapshot{})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
r.MaximumLeverage = decimal.NewFromInt(33)
r.CurrencySettings[e][a][p].MaxLeverageRate = decimal.NewFromInt(33)
r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaxLeverageRate = decimal.NewFromInt(33)
_, err = r.EvaluateOrder(o, h, compliance.Snapshot{
Orders: []compliance.SnapshotOrder{
@@ -130,10 +131,10 @@ func TestEvaluateOrder(t *testing.T) {
}
h = append(h, holdings.Holding{Pair: p, BaseValue: decimal.NewFromInt(1337)}, holdings.Holding{Pair: p, BaseValue: decimal.NewFromFloat(1337.42)})
r.CurrencySettings[e][a][p].MaximumHoldingRatio = decimal.NewFromFloat(0.1)
r.CurrencySettings[e][a][p.Base.Item][p.Quote.Item].MaximumHoldingRatio = decimal.NewFromFloat(0.1)
_, err = r.EvaluateOrder(o, h, compliance.Snapshot{})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
h = append(h, holdings.Holding{Pair: currency.NewPair(currency.DOGE, currency.LTC), BaseValue: decimal.NewFromInt(1337)})

View File

@@ -24,7 +24,7 @@ type Handler interface {
// Risk contains all currency settings in order to evaluate potential orders
type Risk struct {
CurrencySettings map[string]map[asset.Item]map[currency.Pair]*CurrencySettings
CurrencySettings map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencySettings
CanUseLeverage bool
MaximumLeverage decimal.Decimal
}

View File

@@ -5,8 +5,9 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -32,12 +33,19 @@ func Setup(sh SizeHandler, r risk.Handler, riskFreeRate decimal.Decimal) (*Portf
}
// Reset returns the portfolio manager to its default state
func (p *Portfolio) Reset() {
p.exchangeAssetPairSettings = nil
func (p *Portfolio) Reset() error {
if p == nil {
return gctcommon.ErrNilPointer
}
p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings)
p.riskFreeRate = decimal.Zero
p.sizeManager = nil
p.riskManager = nil
return nil
}
// SetupCurrencySettingsMap ensures a map is created and no panics happen
func (p *Portfolio) SetupCurrencySettingsMap(setup *exchange.Settings) error {
// SetCurrencySettingsMap ensures a map is created and no panics happen
func (p *Portfolio) SetCurrencySettingsMap(setup *exchange.Settings) error {
if setup == nil {
return errNoPortfolioSettings
}
@@ -50,31 +58,42 @@ func (p *Portfolio) SetupCurrencySettingsMap(setup *exchange.Settings) error {
if setup.Pair.IsEmpty() {
return errCurrencyPairUnset
}
if p.exchangeAssetPairSettings == nil {
p.exchangeAssetPairSettings = make(map[string]map[asset.Item]map[currency.Pair]*Settings)
if p.exchangeAssetPairPortfolioSettings == nil {
p.exchangeAssetPairPortfolioSettings = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings)
}
name := strings.ToLower(setup.Exchange.GetName())
if p.exchangeAssetPairSettings[name] == nil {
p.exchangeAssetPairSettings[name] = make(map[asset.Item]map[currency.Pair]*Settings)
m, ok := p.exchangeAssetPairPortfolioSettings[name]
if !ok {
m = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Settings)
p.exchangeAssetPairPortfolioSettings[name] = m
}
if p.exchangeAssetPairSettings[name][setup.Asset] == nil {
p.exchangeAssetPairSettings[name][setup.Asset] = make(map[currency.Pair]*Settings)
m2, ok := m[setup.Asset]
if !ok {
m2 = make(map[*currency.Item]map[*currency.Item]*Settings)
m[setup.Asset] = m2
}
if _, ok := p.exchangeAssetPairSettings[name][setup.Asset][setup.Pair]; ok {
return nil
}
collateralCurrency, _, err := setup.Exchange.GetCollateralCurrencyForContract(setup.Asset, setup.Pair)
if err != nil {
return err
m3, ok := m2[setup.Pair.Base.Item]
if !ok {
m3 = make(map[*currency.Item]*Settings)
m2[setup.Pair.Base.Item] = m3
}
settings := &Settings{
Exchange: setup.Exchange,
exchangeName: name,
assetType: setup.Asset,
pair: setup.Pair,
BuySideSizing: setup.BuySide,
SellSideSizing: setup.SellSide,
Leverage: setup.Leverage,
Exchange: setup.Exchange,
ComplianceManager: compliance.Manager{},
HoldingsSnapshots: make(map[int64]*holdings.Holding),
}
if setup.Asset.IsFutures() {
collateralCurrency, _, err := setup.Exchange.GetCollateralCurrencyForContract(setup.Asset, setup.Pair)
if err != nil {
return err
}
futureTrackerSetup := &gctorder.MultiPositionTrackerSetup{
Exchange: name,
Asset: setup.Asset,
@@ -94,6 +113,6 @@ func (p *Portfolio) SetupCurrencySettingsMap(setup *exchange.Settings) error {
}
settings.FuturesTracker = tracker
}
p.exchangeAssetPairSettings[name][setup.Asset][setup.Pair] = settings
m3[setup.Pair.Quote.Item] = settings
return nil
}

View File

@@ -8,13 +8,17 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
// SizeOrder is responsible for ensuring that the order size is within config limits
func (s *Size) SizeOrder(o order.Event, amountAvailable decimal.Decimal, cs *exchange.Settings) (*order.Order, decimal.Decimal, error) {
if o == nil || cs == nil {
return nil, decimal.Zero, common.ErrNilArguments
if o == nil {
return nil, decimal.Zero, fmt.Errorf("%w order event", gctcommon.ErrNilPointer)
}
if cs == nil {
return nil, decimal.Zero, fmt.Errorf("%w exchange settings", gctcommon.ErrNilPointer)
}
if amountAvailable.LessThanOrEqual(decimal.Zero) {
return nil, decimal.Zero, errNoFunds
@@ -37,9 +41,16 @@ func (s *Size) SizeOrder(o order.Event, amountAvailable decimal.Decimal, cs *exc
if err != nil {
return nil, decimal.Zero, err
}
sizedPrice := o.GetClosePrice()
if fde.GetClosePrice().GreaterThan(o.GetClosePrice()) {
// ensure limits are respected by using the largest price
sizedPrice = fde.GetClosePrice()
}
initialAmount := amountAvailable.Mul(scalingInfo.Weighting).Div(fde.GetClosePrice())
oNotionalPosition := initialAmount.Mul(o.GetClosePrice())
sizedAmount, estFee, err := s.calculateAmount(o.GetDirection(), o.GetClosePrice(), oNotionalPosition, cs, o)
oNotionalPosition := initialAmount.Mul(sizedPrice)
sizedAmount, estFee, err := s.calculateAmount(o.GetDirection(), sizedPrice, oNotionalPosition, cs, o)
if err != nil {
return nil, decimal.Zero, err
}
@@ -109,13 +120,15 @@ func (s *Size) calculateAmount(direction gctorder.Side, price, amountAvailable d
return decimal.Zero, decimal.Zero, fmt.Errorf("%w at %v for %v %v %v, no amount sized", errCannotAllocate, o.GetTime(), o.GetExchange(), o.GetAssetType(), o.Pair())
}
if o.GetAmount().IsPositive() && o.GetAmount().LessThanOrEqual(amount) {
// when an order amount is already set
if o.GetAmount().IsPositive() {
// when an order amount is already set and still affordable
// use the pre-set amount and calculate the fee
amount = o.GetAmount()
fee = o.GetAmount().Mul(price).Mul(cs.TakerFee)
if o.GetAmount().Mul(price).Add(o.GetAmount().Mul(price).Mul(cs.TakerFee)).LessThanOrEqual(amountAvailable) {
// TODO: introduce option to fail + cancel original order if this order pricing fails
amount = o.GetAmount()
fee = o.GetAmount().Mul(price).Mul(cs.TakerFee)
}
}
return amount, fee, nil
}

View File

@@ -1,20 +1,19 @@
package size
import (
"context"
"errors"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/ftx"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
@@ -33,8 +32,8 @@ func TestSizingAccuracy(t *testing.T) {
feeRate := decimal.NewFromFloat(0.02)
buyLimit := decimal.NewFromInt(1)
amountWithoutFee, _, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
totalWithFee := (price.Mul(amountWithoutFee)).Add(globalMinMax.MaximumTotal.Mul(feeRate))
if !totalWithFee.Equal(globalMinMax.MaximumTotal) {
@@ -57,8 +56,8 @@ func TestSizingOverMaxSize(t *testing.T) {
feeRate := decimal.NewFromFloat(0.02)
buyLimit := decimal.NewFromInt(1)
amount, _, err := sizer.calculateBuySize(price, availableFunds, feeRate, buyLimit, globalMinMax)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if amount.GreaterThan(globalMinMax.MaximumSize) {
t.Error("greater than max")
@@ -173,8 +172,8 @@ func TestCalculateSellSize(t *testing.T) {
price = decimal.NewFromInt(12)
availableFunds = decimal.NewFromInt(1339)
amount, fee, err := sizer.calculateSellSize(price, availableFunds, feeRate, sellLimit, globalMinMax)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !amount.Equal(sellLimit) {
t.Errorf("received '%v' expected '%v'", amount, sellLimit)
@@ -188,16 +187,16 @@ func TestSizeOrder(t *testing.T) {
t.Parallel()
s := Size{}
_, _, err := s.SizeOrder(nil, decimal.Zero, nil)
if !errors.Is(err, common.ErrNilArguments) {
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Error(err)
}
o := &order.Order{
Base: &event.Base{
Offset: 1,
Exchange: "ftx",
Exchange: "binance",
Time: time.Now(),
CurrencyPair: currency.NewPair(currency.BTC, currency.USD),
UnderlyingPair: currency.NewPair(currency.BTC, currency.USD),
CurrencyPair: currency.NewPair(currency.BTC, currency.USDT),
UnderlyingPair: currency.NewPair(currency.BTC, currency.USDT),
AssetType: asset.Spot,
},
}
@@ -221,28 +220,28 @@ func TestSizeOrder(t *testing.T) {
s.BuySide.MaximumSize = decimal.NewFromInt(1)
s.BuySide.MinimumSize = decimal.NewFromInt(1)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
o.Amount = decimal.NewFromInt(1)
o.Direction = gctorder.Sell
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
s.SellSide.MaximumSize = decimal.NewFromInt(1)
s.SellSide.MinimumSize = decimal.NewFromInt(1)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
o.Direction = gctorder.ClosePosition
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
// spot futures sizing
@@ -251,21 +250,18 @@ func TestSizeOrder(t *testing.T) {
MatchesOrderAmount: true,
ClosePrice: decimal.NewFromInt(1337),
}
exch := ftx.FTX{}
err = exch.LoadCollateralWeightings(context.Background())
if err != nil {
t.Error(err)
}
exch := binance.Binance{}
// TODO adjust when Binance futures wrappers are implemented
cs.Exchange = &exch
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if err != nil {
t.Error(err)
if !errors.Is(err, gctcommon.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNotYetImplemented)
}
o.ClosePrice = decimal.NewFromInt(1000000000)
o.Amount = decimal.NewFromInt(1000000000)
_, _, err = s.SizeOrder(o, decimal.NewFromInt(1337), cs)
if !errors.Is(err, errCannotAllocate) {
t.Errorf("received: %v, expected: %v", err, errCannotAllocate)
if !errors.Is(err, gctcommon.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNotYetImplemented)
}
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/log"
@@ -19,7 +20,7 @@ func fSIL(str string, limit int) string {
}
// CalculateBiggestEventDrawdown calculates the biggest drawdown using a slice of DataEvents
func CalculateBiggestEventDrawdown(closePrices []common.DataEventHandler) (Swing, error) {
func CalculateBiggestEventDrawdown(closePrices []data.Event) (Swing, error) {
if len(closePrices) == 0 {
return Swing{}, fmt.Errorf("%w to calculate drawdowns", errReceivedNoData)
}
@@ -249,7 +250,7 @@ func CalculateRatios(benchmarkRates, returnsPerCandle []decimal.Decimal, riskFre
}
arithmeticCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, arithmeticReturnsPerCandle, riskFreeRateForPeriod)
if err != nil {
return nil, nil, err
log.Warnf(common.Statistics, "%s funding arithmetic calmar ratio %v", logMessage, err)
}
arithmeticStats = &Ratios{}
@@ -284,7 +285,7 @@ func CalculateRatios(benchmarkRates, returnsPerCandle []decimal.Decimal, riskFre
}
geomCalmar, err = gctmath.DecimalCalmarRatio(maxDrawdown.Highest.Value, maxDrawdown.Lowest.Value, geometricReturnsPerCandle, riskFreeRateForPeriod)
if err != nil {
return nil, nil, err
log.Warnf(common.Statistics, "%s funding geometric calmar ratio %v", logMessage, err)
}
geometricStats = &Ratios{}
if !arithmeticSharpe.IsZero() {

View File

@@ -1,10 +1,11 @@
package statistics
import (
"errors"
"fmt"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
gctmath "github.com/thrasher-corp/gocryptotrader/common/math"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
@@ -20,22 +21,20 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
firstPrice := first.ClosePrice
last := c.Events[len(c.Events)-1]
if last.ComplianceSnapshot == nil {
return errMissingSnapshots
}
lastPrice := last.ClosePrice
for i := range last.Transactions.Orders {
switch last.Transactions.Orders[i].Order.Side {
case gctorder.Buy, gctorder.Bid:
for i := range last.ComplianceSnapshot.Orders {
if last.ComplianceSnapshot.Orders[i].Order.Side.IsLong() {
c.BuyOrders++
case gctorder.Sell, gctorder.Ask:
} else {
c.SellOrders++
case gctorder.Long:
c.LongOrders++
case gctorder.Short:
c.ShortOrders++
}
}
for i := range c.Events {
price := c.Events[i].ClosePrice
if price.LessThan(c.LowestClosePrice.Value) || !c.LowestClosePrice.Set {
if (price.LessThan(c.LowestClosePrice.Value) || !c.LowestClosePrice.Set) && !price.IsZero() {
c.LowestClosePrice.Value = price
c.LowestClosePrice.Time = c.Events[i].Time
c.LowestClosePrice.Set = true
@@ -51,7 +50,7 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
if !firstPrice.IsZero() {
c.MarketMovement = lastPrice.Sub(firstPrice).Div(firstPrice).Mul(oneHundred)
}
if first.Holdings.TotalValue.GreaterThan(decimal.Zero) {
if !first.Holdings.TotalValue.IsZero() {
c.StrategyMovement = last.Holdings.TotalValue.Sub(first.Holdings.TotalValue).Div(first.Holdings.TotalValue).Mul(oneHundred)
}
c.analysePNLGrowth()
@@ -62,7 +61,7 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
returnsPerCandle := make([]decimal.Decimal, len(c.Events))
benchmarkRates := make([]decimal.Decimal, len(c.Events))
allDataEvents := make([]common.DataEventHandler, len(c.Events))
allDataEvents := make([]data.Event, len(c.Events))
for i := range c.Events {
returnsPerCandle[i] = c.Events[i].Holdings.ChangeInTotalValuePercent
allDataEvents[i] = c.Events[i].DataEvent
@@ -109,7 +108,7 @@ func (c *CurrencyPairStatistic) CalculateResults(riskFreeRate decimal.Decimal) e
decimal.NewFromFloat(intervalsPerYear),
decimal.NewFromInt(int64(len(c.Events))),
)
if err != nil {
if err != nil && !errors.Is(err, gctmath.ErrPowerDifferenceTooSmall) {
errs = append(errs, err)
}
c.CompoundAnnualGrowthRate = cagr

View File

@@ -45,7 +45,7 @@ func TestCalculateResults(t *testing.T) {
Timestamp: tt1,
QuoteInitialFunds: decimal.NewFromInt(1337),
},
Transactions: compliance.Snapshot{
ComplianceSnapshot: &compliance.Snapshot{
Orders: []compliance.SnapshotOrder{
{
ClosePrice: decimal.NewFromInt(1338),
@@ -88,7 +88,7 @@ func TestCalculateResults(t *testing.T) {
Timestamp: tt2,
QuoteInitialFunds: decimal.NewFromInt(1337),
},
Transactions: compliance.Snapshot{
ComplianceSnapshot: &compliance.Snapshot{
Orders: []compliance.SnapshotOrder{
{
ClosePrice: decimal.NewFromInt(1338),
@@ -123,8 +123,8 @@ func TestCalculateResults(t *testing.T) {
cs.Events = append(cs.Events, ev, ev2)
err := cs.CalculateResults(decimal.NewFromFloat(0.03))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if !cs.MarketMovement.Equal(decimal.NewFromFloat(-33.15)) {
t.Errorf("expected -33.15 received '%v'", cs.MarketMovement)
@@ -143,16 +143,16 @@ func TestCalculateResults(t *testing.T) {
Base: even2,
}
err = cs.CalculateResults(decimal.NewFromFloat(0.03))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
cs.Events[1].DataEvent = &kline.Kline{
Base: even2,
}
err = cs.CalculateResults(decimal.NewFromFloat(0.03))
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -176,7 +176,7 @@ func TestPrintResults(t *testing.T) {
Timestamp: tt1,
QuoteInitialFunds: decimal.NewFromInt(1337),
},
Transactions: compliance.Snapshot{
ComplianceSnapshot: &compliance.Snapshot{
Orders: []compliance.SnapshotOrder{
{
ClosePrice: decimal.NewFromInt(1338),
@@ -215,7 +215,7 @@ func TestPrintResults(t *testing.T) {
Timestamp: tt2,
QuoteInitialFunds: decimal.NewFromInt(1337),
},
Transactions: compliance.Snapshot{
ComplianceSnapshot: &compliance.Snapshot{
Orders: []compliance.SnapshotOrder{
{
ClosePrice: decimal.NewFromInt(1338),
@@ -311,9 +311,8 @@ func TestAnalysePNLGrowth(t *testing.T) {
c.Events = append(c.Events,
DataAtOffset{PNL: &portfolio.PNLSummary{
Exchange: e,
Item: a,
Asset: a,
Pair: p,
Offset: 0,
Result: order.PNLResult{
Time: time.Now(),
UnrealisedPNL: decimal.NewFromInt(1),
@@ -333,9 +332,8 @@ func TestAnalysePNLGrowth(t *testing.T) {
c.Events = append(c.Events,
DataAtOffset{PNL: &portfolio.PNLSummary{
Exchange: e,
Item: a,
Asset: a,
Pair: p,
Offset: 0,
Result: order.PNLResult{
Time: time.Now(),
UnrealisedPNL: decimal.NewFromFloat(0.5),

View File

@@ -1,12 +1,13 @@
package statistics
import (
"errors"
"fmt"
"sort"
"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"
@@ -15,33 +16,47 @@ import (
// 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) {
func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic, riskFreeRate decimal.Decimal, interval gctkline.Interval) (*FundingStatistics, error) {
if currStats == nil {
return nil, common.ErrNilArguments
return nil, gctcommon.ErrNilPointer
}
report, err := funds.GenerateReport()
if err != nil {
return nil, err
}
if report == nil {
return nil, errReceivedNoData
}
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 {
if report.Items[i].AppendedViaAPI {
// items added via API may not have been processed along with typical events
// are not relevant to calculating statistics
continue
}
return nil, fmt.Errorf("%w for %v %v",
errNoRelevantStatsFound,
report.Items[i].Exchange,
report.Items[i].Asset)
}
var relevantStats []relatedCurrencyPairStatistics
for k, v := range exchangeAssetStats {
if k.Base.Equal(report.Items[i].Currency) {
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{isBaseCurrency: true, stat: v})
continue
}
if k.Quote.Equal(report.Items[i].Currency) {
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{stat: v})
for b, baseMap := range exchangeAssetStats {
for q, v := range baseMap {
if b.Currency().Equal(report.Items[i].Currency) {
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{isBaseCurrency: true, stat: v})
continue
}
if q.Currency().Equal(report.Items[i].Currency) {
relevantStats = append(relevantStats, relatedCurrencyPairStatistics{stat: v})
}
}
}
fundingStat, err := CalculateIndividualFundingStatistics(report.DisableUSDTracking, &report.Items[i], relevantStats)
var fundingStat *FundingItemStatistics
fundingStat, err = CalculateIndividualFundingStatistics(report.DisableUSDTracking, &report.Items[i], relevantStats)
if err != nil {
return nil, err
}
@@ -79,12 +94,6 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
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.HoldingValueDifference = report.FinalFunds.Sub(report.InitialFunds).Div(report.InitialFunds).Mul(decimal.NewFromInt(100))
riskFreeRatePerCandle := usdStats.RiskFreeRate.Div(decimal.NewFromFloat(interval.IntervalsPerYear()))
@@ -101,8 +110,9 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
}
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
if !usdStats.HoldingValues[0].Value.IsZero() {
usdStats.BenchmarkMarketMovement = benchmarkMovement.Sub(usdStats.HoldingValues[0].Value).Div(usdStats.HoldingValues[0].Value).Mul(decimal.NewFromInt(100))
}
usdStats.MaxDrawdown, err = CalculateBiggestValueAtTimeDrawdown(usdStats.HoldingValues, interval)
if err != nil {
return nil, err
@@ -114,8 +124,8 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
return nil, err
}
var cagr decimal.Decimal
for i := range response.Items {
var cagr decimal.Decimal
if response.Items[i].ReportItem.InitialFunds.IsZero() {
continue
}
@@ -125,26 +135,25 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
decimal.NewFromFloat(interval.IntervalsPerYear()),
decimal.NewFromInt(int64(len(usdStats.HoldingValues))),
)
if err != nil {
if err != nil && !errors.Is(err, gctmath.ErrPowerDifferenceTooSmall) {
return nil, err
}
response.Items[i].CompoundAnnualGrowthRate = cagr
}
if !usdStats.HoldingValues[0].Value.IsZero() {
var cagr decimal.Decimal
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 {
if err != nil && !errors.Is(err, gctmath.ErrPowerDifferenceTooSmall) {
return nil, err
}
usdStats.CompoundAnnualGrowthRate = cagr
}
usdStats.DidStrategyMakeProfit = usdStats.HoldingValues[len(usdStats.HoldingValues)-1].Value.GreaterThan(usdStats.HoldingValues[0].Value)
usdStats.DidStrategyBeatTheMarket = usdStats.StrategyMovement.GreaterThan(usdStats.BenchmarkMarketMovement)
usdStats.DidStrategyMakeProfit = report.FinalFunds.GreaterThan(report.InitialFunds)
usdStats.DidStrategyBeatTheMarket = usdStats.HoldingValueDifference.GreaterThan(usdStats.BenchmarkMarketMovement)
response.TotalUSDStatistics = usdStats
return response, nil
@@ -153,15 +162,15 @@ func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[str
// 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)
return nil, fmt.Errorf("%w - nil report item", gctcommon.ErrNilPointer)
}
item := &FundingItemStatistics{
ReportItem: reportItem,
}
if disableUSDTracking {
if disableUSDTracking || reportItem.AppendedViaAPI {
return item, nil
}
closePrices := reportItem.Snapshots
if len(closePrices) == 0 {
return nil, errMissingSnapshots
@@ -175,7 +184,7 @@ func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *f
Value: closePrices[len(closePrices)-1].USDClosePrice,
}
for i := range closePrices {
if closePrices[i].USDClosePrice.LessThan(item.LowestClosePrice.Value) || !item.LowestClosePrice.Set {
if (closePrices[i].USDClosePrice.LessThan(item.LowestClosePrice.Value) || !item.LowestClosePrice.Set) && !closePrices[i].USDClosePrice.IsZero() {
item.LowestClosePrice.Value = closePrices[i].USDClosePrice
item.LowestClosePrice.Time = closePrices[i].Time
item.LowestClosePrice.Set = true
@@ -220,7 +229,7 @@ func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *f
if !reportItem.IsCollateral {
for i := range relatedStats {
if relatedStats[i].stat == nil {
return nil, fmt.Errorf("%w related stats", common.ErrNilArguments)
return nil, fmt.Errorf("%w related stats", gctcommon.ErrNilPointer)
}
if relatedStats[i].isBaseCurrency {
item.BuyOrders += relatedStats[i].stat.BuyOrders
@@ -262,14 +271,16 @@ func CalculateIndividualFundingStatistics(disableUSDTracking bool, reportItem *f
if item.ReportItem.USDPairCandle == nil && !reportItem.IsCollateral {
return nil, fmt.Errorf("%w usd candles missing", errMissingSnapshots)
}
s := item.ReportItem.USDPairCandle.GetStream()
s, err := item.ReportItem.USDPairCandle.GetStream()
if err != nil {
return nil, err
}
if len(s) == 0 {
return nil, fmt.Errorf("%w stream missing", errMissingSnapshots)
}
if reportItem.IsCollateral {
return item, nil
}
var err error
item.MaxDrawdown, err = CalculateBiggestEventDrawdown(s)
return item, err
}

View File

@@ -5,10 +5,12 @@ import (
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
@@ -18,10 +20,10 @@ import (
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)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received %v expected %v", err, common.ErrNilPointer)
}
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, true)
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, true, false)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
@@ -44,8 +46,8 @@ func TestCalculateFundingStatistics(t *testing.T) {
}
_, err = CalculateFundingStatistics(f, nil, decimal.Zero, gctkline.OneHour)
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received %v expected %v", err, common.ErrNilPointer)
}
usdKline := gctkline.Item{
@@ -63,6 +65,7 @@ func TestCalculateFundingStatistics(t *testing.T) {
},
}
dfk := &kline.DataFromKline{
Base: &data.Base{},
Item: usdKline,
}
err = dfk.Load()
@@ -74,13 +77,13 @@ func TestCalculateFundingStatistics(t *testing.T) {
t.Errorf("received %v expected %v", err, funding.ErrUSDTrackingDisabled)
}
cs := make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
cs := make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
_, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour)
if !errors.Is(err, errNoRelevantStatsFound) {
t.Errorf("received %v expected %v", err, errNoRelevantStatsFound)
}
f, err = funding.SetupFundingManager(&engine.ExchangeManager{}, true, false)
f, err = funding.SetupFundingManager(&engine.ExchangeManager{}, true, false, false)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
@@ -96,16 +99,24 @@ func TestCalculateFundingStatistics(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
cs["binance"] = make(map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
cs["binance"][asset.Spot] = make(map[currency.Pair]*CurrencyPairStatistic)
cs["binance"][asset.Spot][currency.NewPair(currency.LTC, currency.USD)] = &CurrencyPairStatistic{}
cs["binance"] = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
cs["binance"][asset.Spot] = make(map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
cs["binance"][asset.Spot][currency.LTC.Item] = make(map[*currency.Item]*CurrencyPairStatistic)
cs["binance"][asset.Spot][currency.LTC.Item][currency.USD.Item] = &CurrencyPairStatistic{}
_, 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 = f.CreateSnapshot(usdKline.Candles[0].Time)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
err = f.CreateSnapshot(usdKline.Candles[1].Time)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
cs["binance"][asset.Spot][currency.BTC.Item] = make(map[*currency.Item]*CurrencyPairStatistic)
cs["binance"][asset.Spot][currency.BTC.Item][currency.USDT.Item] = &CurrencyPairStatistic{}
_, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour)
if !errors.Is(err, nil) {
@@ -115,8 +126,8 @@ func TestCalculateFundingStatistics(t *testing.T) {
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)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received %v expected %v", err, common.ErrNilPointer)
}
_, err = CalculateIndividualFundingStatistics(true, &funding.ReportItem{}, nil)
@@ -148,8 +159,8 @@ func TestCalculateIndividualFundingStatistics(t *testing.T) {
},
}
_, err = CalculateIndividualFundingStatistics(false, ri, rs)
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received %v expected %v", err, common.ErrNilPointer)
}
rs[0].stat = &CurrencyPairStatistic{}
@@ -159,10 +170,15 @@ func TestCalculateIndividualFundingStatistics(t *testing.T) {
if !errors.Is(err, errMissingSnapshots) {
t.Errorf("received %v expected %v", err, errMissingSnapshots)
}
cp := currency.NewPair(currency.BTC, currency.USD)
ri.USDPairCandle = &kline.DataFromKline{
Base: &data.Base{},
Item: gctkline.Item{
Interval: gctkline.OneHour,
Exchange: testExchange,
Pair: cp,
UnderlyingPair: cp,
Asset: asset.Spot,
Interval: gctkline.OneHour,
Candles: []gctkline.Candle{
{
Time: time.Now().Add(-time.Hour),
@@ -171,6 +187,8 @@ func TestCalculateIndividualFundingStatistics(t *testing.T) {
Time: time.Now(),
},
},
SourceJobID: uuid.UUID{},
ValidationJobID: uuid.UUID{},
},
}
err = ri.USDPairCandle.Load()
@@ -198,11 +216,11 @@ func TestCalculateIndividualFundingStatistics(t *testing.T) {
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)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received %v expected %v", err, common.ErrNilPointer)
}
funds, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, true)
funds, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, true, false)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
@@ -222,7 +240,10 @@ func TestFundingStatisticsPrintResults(t *testing.T) {
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
f.Report = funds.GenerateReport()
f.Report, err = funds.GenerateReport()
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
err = f.PrintResults(false)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
@@ -231,8 +252,8 @@ func TestFundingStatisticsPrintResults(t *testing.T) {
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)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received %v expected %v", err, common.ErrNilPointer)
}
f.TotalUSDStatistics = &TotalFundingStatistics{

View File

@@ -3,10 +3,14 @@ package statistics
import (
"fmt"
"sort"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
data2 "github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
@@ -66,80 +70,41 @@ func (s *Statistic) PrintTotalResults() {
// rather than separated by exchange, asset and currency pair, it's
// grouped by time to allow a clearer picture of events
func (s *Statistic) PrintAllEventsChronologically() {
var results []eventOutputHolder
log.Info(common.Statistics, common.CMDColours.H1+"------------------Events-------------------------------------"+common.CMDColours.Default)
var errs gctcommon.Errors
colour := common.CMDColours.Default
for exch, x := range s.ExchangeAssetPairStatistics {
for a, y := range x {
for pair, currencyStatistic := range y {
for i := range currencyStatistic.Events {
switch {
case currencyStatistic.Events[i].FillEvent != nil:
direction := currencyStatistic.Events[i].FillEvent.GetDirection()
if direction == order.CouldNotBuy ||
direction == order.CouldNotSell ||
direction == order.MissingData ||
direction == order.DoNothing ||
direction == order.TransferredFunds ||
direction == order.UnknownSide {
if direction == order.DoNothing {
colour = common.CMDColours.DarkGrey
var err error
var results []eventOutputHolder
for _, exchangeMap := range s.ExchangeAssetPairStatistics {
for _, assetMap := range exchangeMap {
for _, baseMap := range assetMap {
for _, currencyStatistic := range baseMap {
for i := range currencyStatistic.Events {
var result string
var tt time.Time
switch {
case currencyStatistic.Events[i].FillEvent != nil:
result, err = s.CreateLog(currencyStatistic.Events[i].FillEvent)
if err != nil {
errs = append(errs, err)
continue
}
msg := fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection: %v",
currencyStatistic.Events[i].FillEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].FillEvent.Pair().String(), limit14),
currencyStatistic.Events[i].FillEvent.GetClosePrice().Round(8),
currencyStatistic.Events[i].FillEvent.GetDirection())
msg = addReason(currencyStatistic.Events[i].FillEvent.GetConcatReasons(), msg)
msg += common.CMDColours.Default
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(), msg)
} else {
// successful order!
colour = common.CMDColours.Success
if currencyStatistic.Events[i].FillEvent.IsLiquidated() {
colour = common.CMDColours.Error
tt = currencyStatistic.Events[i].FillEvent.GetTime()
case currencyStatistic.Events[i].SignalEvent != nil:
result, err = s.CreateLog(currencyStatistic.Events[i].SignalEvent)
if err != nil {
errs = append(errs, err)
continue
}
msg := fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection %v\tOrder placed: Amount: %v\tFee: %v\tTotal: %v",
currencyStatistic.Events[i].FillEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].FillEvent.Pair().String(), limit14),
currencyStatistic.Events[i].FillEvent.GetPurchasePrice().Round(8),
currencyStatistic.Events[i].FillEvent.GetDirection(),
currencyStatistic.Events[i].FillEvent.GetAmount().Round(8),
currencyStatistic.Events[i].FillEvent.GetExchangeFee(),
currencyStatistic.Events[i].FillEvent.GetTotal().Round(8))
msg = addReason(currencyStatistic.Events[i].FillEvent.GetConcatReasons(), msg)
msg += common.CMDColours.Default
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(), msg)
tt = currencyStatistic.Events[i].SignalEvent.GetTime()
case currencyStatistic.Events[i].DataEvent != nil:
result, err = s.CreateLog(currencyStatistic.Events[i].DataEvent)
if err != nil {
errs = append(errs, err)
continue
}
tt = currencyStatistic.Events[i].DataEvent.GetTime()
}
case currencyStatistic.Events[i].SignalEvent != nil:
msg := fmt.Sprintf("%v %v%v%v| Price: $%v",
currencyStatistic.Events[i].SignalEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].SignalEvent.Pair().String(), limit14),
currencyStatistic.Events[i].SignalEvent.GetClosePrice().Round(8))
msg = addReason(currencyStatistic.Events[i].SignalEvent.GetConcatReasons(), msg)
msg += common.CMDColours.Default
results = addEventOutputToTime(results, currencyStatistic.Events[i].SignalEvent.GetTime(), msg)
case currencyStatistic.Events[i].DataEvent != nil:
msg := fmt.Sprintf("%v %v%v%v| Price: $%v",
currencyStatistic.Events[i].DataEvent.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(exch, limit12),
fSIL(a.String(), limit10),
fSIL(currencyStatistic.Events[i].DataEvent.Pair().String(), limit14),
currencyStatistic.Events[i].DataEvent.GetClosePrice().Round(8))
msg = addReason(currencyStatistic.Events[i].DataEvent.GetConcatReasons(), msg)
msg += common.CMDColours.Default
results = addEventOutputToTime(results, currencyStatistic.Events[i].DataEvent.GetTime(), msg)
default:
errs = append(errs, fmt.Errorf(common.CMDColours.Error+"%v%v%v unexpected data received %+v"+common.CMDColours.Default, exch, a, fSIL(pair.String(), limit14), currencyStatistic.Events[i]))
results = addEventOutputToTime(results, tt, result)
}
}
}
@@ -164,6 +129,81 @@ func (s *Statistic) PrintAllEventsChronologically() {
}
}
// CreateLog renders a string log depending on what events are populated
// at a given offset. Can render logs live, or at the end of a backtesting run
func (s *Statistic) CreateLog(data common.Event) (string, error) {
var (
result string
colour = common.CMDColours.Default
)
switch ev := data.(type) {
case fill.Event:
direction := ev.GetDirection()
if direction == order.CouldNotBuy ||
direction == order.CouldNotSell ||
direction == order.CouldNotLong ||
direction == order.CouldNotShort ||
direction == order.MissingData ||
direction == order.DoNothing ||
direction == order.TransferredFunds ||
direction == order.UnknownSide {
if direction == order.DoNothing {
colour = common.CMDColours.DarkGrey
}
result = fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection: %v",
ev.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(ev.GetExchange(), limit12),
fSIL(ev.GetAssetType().String(), limit10),
fSIL(ev.Pair().String(), limit14),
ev.GetClosePrice().Round(8),
ev.GetDirection())
result = addReason(ev.GetConcatReasons(), result)
result += common.CMDColours.Default
} else {
// successful order!
colour = common.CMDColours.Success
if ev.IsLiquidated() {
colour = common.CMDColours.Error
}
result = fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection %v\tOrder placed: Amount: %v\tFee: %v\tTotal: %v",
ev.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(ev.GetExchange(), limit12),
fSIL(ev.GetAssetType().String(), limit10),
fSIL(ev.Pair().String(), limit14),
ev.GetPurchasePrice().Round(8),
ev.GetDirection(),
ev.GetAmount().Round(8),
ev.GetExchangeFee(),
ev.GetTotal().Round(8))
result = addReason(ev.GetConcatReasons(), result)
result += common.CMDColours.Default
}
case signal.Event:
result = fmt.Sprintf("%v %v%v%v| Price: $%v",
ev.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(ev.GetExchange(), limit12),
fSIL(ev.GetAssetType().String(), limit10),
fSIL(ev.Pair().String(), limit14),
ev.GetClosePrice().Round(8))
result = addReason(ev.GetConcatReasons(), result)
result += common.CMDColours.Default
case data2.Event:
result = fmt.Sprintf("%v %v%v%v| Price: $%v",
ev.GetTime().Format(gctcommon.SimpleTimeFormat),
fSIL(ev.GetExchange(), limit12),
fSIL(ev.GetAssetType().String(), limit10),
fSIL(ev.Pair().String(), limit14),
ev.GetClosePrice().Round(8))
result = addReason(ev.GetConcatReasons(), result)
result += common.CMDColours.Default
default:
return "", fmt.Errorf(common.CMDColours.Error+"unexpected data received %T %+v"+common.CMDColours.Default, data, data)
}
return result, 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
@@ -176,14 +216,14 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
c.StartingClosePrice.Time = first.Time
c.EndingClosePrice.Value = last.DataEvent.GetClosePrice()
c.EndingClosePrice.Time = last.Time
c.TotalOrders = c.BuyOrders + c.SellOrders + c.ShortOrders + c.LongOrders
c.TotalOrders = c.BuyOrders + c.SellOrders
last.Holdings.TotalValueLost = last.Holdings.TotalValueLostToSlippage.Add(last.Holdings.TotalValueLostToVolumeSizing)
sep := fmt.Sprintf("%v %v %v |\t", fSIL(e, limit12), fSIL(a.String(), limit10), fSIL(p.String(), limit14))
currStr := fmt.Sprintf(common.CMDColours.H1+"------------------Stats for %v %v %v------------------------------------------------------"+common.CMDColours.Default, e, a, p)
log.Infof(common.CurrencyStatistics, currStr[:70])
if a.IsFutures() {
log.Infof(common.CurrencyStatistics, "%s Long orders: %s", sep, convert.IntToHumanFriendlyString(c.LongOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Short orders: %s", sep, convert.IntToHumanFriendlyString(c.ShortOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Long orders: %s", sep, convert.IntToHumanFriendlyString(c.BuyOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Short orders: %s", sep, convert.IntToHumanFriendlyString(c.SellOrders, ","))
log.Infof(common.CurrencyStatistics, "%s Highest Unrealised PNL: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestUnrealisedPNL.Value, 8, ".", ","), c.HighestUnrealisedPNL.Time)
log.Infof(common.CurrencyStatistics, "%s Lowest Unrealised PNL: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.LowestUnrealisedPNL.Value, 8, ".", ","), c.LowestUnrealisedPNL.Time)
log.Infof(common.CurrencyStatistics, "%s Highest Realised PNL: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.HighestRealisedPNL.Value, 8, ".", ","), c.HighestRealisedPNL.Time)
@@ -205,7 +245,7 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
log.Infof(common.CurrencyStatistics, "%s Calculated Drawdown: %s%%", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.DrawdownPercent, 8, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Difference: %s", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value.Sub(c.MaxDrawdown.Lowest.Value), 2, ".", ","))
log.Infof(common.CurrencyStatistics, "%s Drawdown length: %s", sep, convert.IntToHumanFriendlyString(c.MaxDrawdown.IntervalDuration, ","))
if !usingExchangeLevelFunding {
if !usingExchangeLevelFunding && c.TotalOrders > 1 {
log.Info(common.CurrencyStatistics, common.CMDColours.H2+"------------------Ratios------------------------------------------------"+common.CMDColours.Default)
log.Info(common.CurrencyStatistics, common.CMDColours.H3+"------------------Rates-------------------------------------------------"+common.CMDColours.Default)
log.Infof(common.CurrencyStatistics, "%s Compound Annual Growth Rate: %s", sep, convert.DecimalToHumanFriendlyString(c.CompoundAnnualGrowthRate, 2, ".", ","))
@@ -273,7 +313,7 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
// 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)
return fmt.Errorf("%w requires report to be generated", gctcommon.ErrNilPointer)
}
var spotResults, futuresResults []FundingItemStatistics
for i := range f.Items {
@@ -289,6 +329,9 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
if len(spotResults) > 0 {
log.Info(common.FundingStatistics, common.CMDColours.H2+"------------------Funding Spot Item Results------------------"+common.CMDColours.Default)
for i := range spotResults {
if spotResults[i].ReportItem.AppendedViaAPI {
continue
}
sep := fmt.Sprintf("%v%v%v| ", fSIL(spotResults[i].ReportItem.Exchange, limit12), fSIL(spotResults[i].ReportItem.Asset.String(), limit10), fSIL(spotResults[i].ReportItem.Currency.String(), limit14))
if !spotResults[i].ReportItem.PairedWith.IsEmpty() {
log.Infof(common.FundingStatistics, "%s Paired with: %v", sep, spotResults[i].ReportItem.PairedWith)
@@ -316,6 +359,9 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
if len(futuresResults) > 0 {
log.Info(common.FundingStatistics, common.CMDColours.H2+"------------------Funding Futures Item Results---------------"+common.CMDColours.Default)
for i := range futuresResults {
if futuresResults[i].ReportItem.AppendedViaAPI {
continue
}
sep := fmt.Sprintf("%v%v%v| ", fSIL(futuresResults[i].ReportItem.Exchange, limit12), fSIL(futuresResults[i].ReportItem.Asset.String(), limit10), fSIL(futuresResults[i].ReportItem.Currency.String(), limit14))
log.Infof(common.FundingStatistics, "%s Is Collateral: %v", sep, futuresResults[i].IsCollateral)
if futuresResults[i].IsCollateral {
@@ -346,7 +392,7 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
log.Infof(common.FundingStatistics, "%s Initial value: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.InitialFunds, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Final value: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.FinalFunds, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Benchmark Market Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.BenchmarkMarketMovement, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Strategy Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.StrategyMovement, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Strategy Movement: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.HoldingValueDifference, 8, ".", ","))
log.Infof(common.FundingStatistics, "%s Did strategy make a profit: %v", sep, f.TotalUSDStatistics.DidStrategyMakeProfit)
log.Infof(common.FundingStatistics, "%s Did strategy beat the benchmark: %v", sep, f.TotalUSDStatistics.DidStrategyBeatTheMarket)
log.Infof(common.FundingStatistics, "%s Highest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.HighestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.HighestHoldingValue.Time)
@@ -357,7 +403,7 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
log.Infof(common.FundingStatistics, "%s Risk free rate: %s%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.RiskFreeRate.Mul(decimal.NewFromInt(100)), 2, ".", ","))
log.Infof(common.FundingStatistics, "%s Compound Annual Growth Rate: %v%%", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.CompoundAnnualGrowthRate, 8, ".", ","))
if f.TotalUSDStatistics.ArithmeticRatios == nil || f.TotalUSDStatistics.GeometricRatios == nil {
return fmt.Errorf("%w missing ratio calculations", common.ErrNilArguments)
return fmt.Errorf("%w missing ratio calculations", gctcommon.ErrNilPointer)
}
log.Info(common.FundingStatistics, common.CMDColours.H4+"------------------Arithmetic--------------------------------------------"+common.CMDColours.Default)
if wasAnyDataMissing {

View File

@@ -5,112 +5,138 @@ import (
"fmt"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/log"
)
// Reset returns the struct to defaults
func (s *Statistic) Reset() {
*s = Statistic{}
func (s *Statistic) Reset() error {
if s == nil {
return gctcommon.ErrNilPointer
}
s.StrategyName = ""
s.StrategyDescription = ""
s.StrategyNickname = ""
s.StrategyGoal = ""
s.StartDate = time.Time{}
s.EndDate = time.Time{}
s.CandleInterval = 0
s.RiskFreeRate = decimal.Zero
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
s.CurrencyStatistics = nil
s.TotalBuyOrders = 0
s.TotalLongOrders = 0
s.TotalShortOrders = 0
s.TotalSellOrders = 0
s.TotalOrders = 0
s.BiggestDrawdown = nil
s.BestStrategyResults = nil
s.BestMarketMovement = nil
s.WasAnyDataMissing = false
s.FundingStatistics = nil
s.FundManager = nil
s.HasCollateral = false
return nil
}
// SetupEventForTime sets up the big map for to store important data at each time interval
func (s *Statistic) SetupEventForTime(ev common.DataEventHandler) error {
// SetEventForOffset sets up the big map for to store important data at each time interval
func (s *Statistic) SetEventForOffset(ev common.Event) error {
if ev == nil {
return common.ErrNilEvent
}
if ev.GetBase() == nil {
return fmt.Errorf("%w event base", common.ErrNilEvent)
}
ex := ev.GetExchange()
a := ev.GetAssetType()
p := ev.Pair()
s.setupMap(ex, a)
lookup := s.ExchangeAssetPairStatistics[ex][a][p]
if lookup == nil {
if s.ExchangeAssetPairStatistics == nil {
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
}
m, ok := s.ExchangeAssetPairStatistics[ex]
if !ok {
m = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
s.ExchangeAssetPairStatistics[ex] = m
}
m2, ok := m[a]
if !ok {
m2 = make(map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
m[a] = m2
}
m3, ok := m2[p.Base.Item]
if !ok {
m3 = make(map[*currency.Item]*CurrencyPairStatistic)
m2[p.Base.Item] = m3
}
lookup, ok := m3[p.Quote.Item]
if !ok {
lookup = &CurrencyPairStatistic{
Exchange: ev.GetExchange(),
Asset: ev.GetAssetType(),
Currency: ev.Pair(),
UnderlyingPair: ev.GetUnderlyingPair(),
}
m3[p.Quote.Item] = lookup
}
for i := range lookup.Events {
if lookup.Events[i].Offset == ev.GetOffset() {
return ErrAlreadyProcessed
if lookup.Events[i].Offset != ev.GetOffset() {
continue
}
return applyEventAtOffset(ev, &lookup.Events[i])
}
lookup.Events = append(lookup.Events,
DataAtOffset{
DataEvent: ev,
Offset: ev.GetOffset(),
Time: ev.GetTime(),
},
)
s.ExchangeAssetPairStatistics[ex][a][p] = lookup
// add to events and then apply the supplied event to it
lookup.Events = append(lookup.Events, DataAtOffset{
Offset: ev.GetOffset(),
Time: ev.GetTime(),
})
err := applyEventAtOffset(ev, &lookup.Events[len(lookup.Events)-1])
if err != nil {
return err
}
return nil
}
func (s *Statistic) setupMap(ex string, a asset.Item) {
if s.ExchangeAssetPairStatistics == nil {
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]*CurrencyPairStatistic)
}
if s.ExchangeAssetPairStatistics[ex][a] == nil {
s.ExchangeAssetPairStatistics[ex][a] = make(map[currency.Pair]*CurrencyPairStatistic)
}
}
// SetEventForOffset sets the event for the time period in the event
func (s *Statistic) SetEventForOffset(ev common.EventHandler) error {
if ev == nil {
return common.ErrNilEvent
}
if s.ExchangeAssetPairStatistics == nil {
return errExchangeAssetPairStatsUnset
}
exch := ev.GetExchange()
a := ev.GetAssetType()
p := ev.Pair()
offset := ev.GetOffset()
lookup := s.ExchangeAssetPairStatistics[exch][a][p]
if lookup == nil {
return fmt.Errorf("%w for %v %v %v to set signal event", errCurrencyStatisticsUnset, exch, a, p)
}
for i := len(lookup.Events) - 1; i >= 0; i-- {
if lookup.Events[i].Offset == offset {
return applyEventAtOffset(ev, lookup, i)
}
}
return fmt.Errorf("%w for event %v %v %v at offset %v", errNoRelevantStatsFound, exch, a, p, ev.GetOffset())
}
func applyEventAtOffset(ev common.EventHandler, lookup *CurrencyPairStatistic, i int) error {
func applyEventAtOffset(ev common.Event, data *DataAtOffset) error {
switch t := ev.(type) {
case common.DataEventHandler:
lookup.Events[i].DataEvent = t
case kline.Event:
// using kline.Event as signal.Event also matches data.Event
if data.DataEvent != nil && data.DataEvent != ev {
return fmt.Errorf("kline event %w %v %v %v %v", ErrAlreadyProcessed, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetOffset())
}
data.DataEvent = t
case signal.Event:
lookup.Events[i].SignalEvent = t
if data.SignalEvent != nil {
return fmt.Errorf("signal event %w %v %v %v %v", ErrAlreadyProcessed, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetOffset())
}
data.SignalEvent = t
case order.Event:
lookup.Events[i].OrderEvent = t
if data.OrderEvent != nil {
return fmt.Errorf("order event %w %v %v %v %v", ErrAlreadyProcessed, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetOffset())
}
data.OrderEvent = t
case fill.Event:
lookup.Events[i].FillEvent = t
if data.FillEvent != nil {
return fmt.Errorf("fill event %w %v %v %v %v", ErrAlreadyProcessed, ev.GetExchange(), ev.GetAssetType(), ev.Pair(), ev.GetOffset())
}
data.FillEvent = t
default:
return fmt.Errorf("unknown event type received: %v", ev)
}
lookup.Events[i].Time = ev.GetTime()
lookup.Events[i].ClosePrice = ev.GetClosePrice()
lookup.Events[i].Offset = ev.GetOffset()
data.Time = ev.GetTime()
data.ClosePrice = ev.GetClosePrice()
return nil
}
@@ -120,7 +146,7 @@ func (s *Statistic) AddHoldingsForTime(h *holdings.Holding) error {
if s.ExchangeAssetPairStatistics == nil {
return errExchangeAssetPairStatsUnset
}
lookup := s.ExchangeAssetPairStatistics[h.Exchange][h.Asset][h.Pair]
lookup := s.ExchangeAssetPairStatistics[h.Exchange][h.Asset][h.Pair.Base.Item][h.Pair.Quote.Item]
if lookup == nil {
return fmt.Errorf("%w for %v %v %v to set holding event", errCurrencyStatisticsUnset, h.Exchange, h.Asset, h.Pair)
}
@@ -136,14 +162,14 @@ func (s *Statistic) AddHoldingsForTime(h *holdings.Holding) error {
// AddPNLForTime stores PNL data for tracking purposes
func (s *Statistic) AddPNLForTime(pnl *portfolio.PNLSummary) error {
if pnl == nil {
return fmt.Errorf("%w requires PNL", common.ErrNilArguments)
return fmt.Errorf("%w requires PNL", gctcommon.ErrNilPointer)
}
if s.ExchangeAssetPairStatistics == nil {
return errExchangeAssetPairStatsUnset
}
lookup := s.ExchangeAssetPairStatistics[pnl.Exchange][pnl.Item][pnl.Pair]
lookup := s.ExchangeAssetPairStatistics[pnl.Exchange][pnl.Asset][pnl.Pair.Base.Item][pnl.Pair.Quote.Item]
if lookup == nil {
return fmt.Errorf("%w for %v %v %v to set pnl", errCurrencyStatisticsUnset, pnl.Exchange, pnl.Item, pnl.Pair)
return fmt.Errorf("%w for %v %v %v to set pnl", errCurrencyStatisticsUnset, pnl.Exchange, pnl.Asset, pnl.Pair)
}
for i := len(lookup.Events) - 1; i >= 0; i-- {
if lookup.Events[i].Offset == pnl.Offset {
@@ -152,13 +178,16 @@ func (s *Statistic) AddPNLForTime(pnl *portfolio.PNLSummary) error {
return nil
}
}
return fmt.Errorf("%v %v %v %w %v", pnl.Exchange, pnl.Item, pnl.Pair, errNoDataAtOffset, pnl.Offset)
return fmt.Errorf("%v %v %v %w %v", pnl.Exchange, pnl.Asset, pnl.Pair, errNoDataAtOffset, pnl.Offset)
}
// AddComplianceSnapshotForTime adds the compliance snapshot to the statistics at the time period
func (s *Statistic) AddComplianceSnapshotForTime(c compliance.Snapshot, e fill.Event) error {
func (s *Statistic) AddComplianceSnapshotForTime(c *compliance.Snapshot, e common.Event) error {
if c == nil {
return fmt.Errorf("%w compliance snapshot", common.ErrNilEvent)
}
if e == nil {
return common.ErrNilEvent
return fmt.Errorf("%w fill event", common.ErrNilEvent)
}
if s.ExchangeAssetPairStatistics == nil {
return errExchangeAssetPairStatsUnset
@@ -166,13 +195,13 @@ func (s *Statistic) AddComplianceSnapshotForTime(c compliance.Snapshot, e fill.E
exch := e.GetExchange()
a := e.GetAssetType()
p := e.Pair()
lookup := s.ExchangeAssetPairStatistics[exch][a][p]
lookup := s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item]
if lookup == nil {
return fmt.Errorf("%w for %v %v %v to set compliance snapshot", errCurrencyStatisticsUnset, exch, a, p)
}
for i := len(lookup.Events) - 1; i >= 0; i-- {
if lookup.Events[i].Offset == e.GetOffset() {
lookup.Events[i].Transactions = c
lookup.Events[i].ComplianceSnapshot = c
return nil
}
}
@@ -189,38 +218,47 @@ func (s *Statistic) CalculateAllResults() error {
var err error
for exchangeName, exchangeMap := range s.ExchangeAssetPairStatistics {
for assetItem, assetMap := range exchangeMap {
for pair, stats := range assetMap {
currCount++
last := stats.Events[len(stats.Events)-1]
if last.PNL != nil {
s.HasCollateral = true
}
err = stats.CalculateResults(s.RiskFreeRate)
if err != nil {
log.Error(common.Statistics, err)
}
stats.FinalHoldings = last.Holdings
stats.InitialHoldings = stats.Events[0].Holdings
stats.FinalOrders = last.Transactions
s.StartDate = stats.Events[0].Time
s.EndDate = last.Time
stats.PrintResults(exchangeName, assetItem, pair, s.FundManager.IsUsingExchangeLevelFunding())
for b, baseMap := range assetMap {
for q, stats := range baseMap {
currCount++
last := stats.Events[len(stats.Events)-1]
if last.PNL != nil {
s.HasCollateral = true
}
err = stats.CalculateResults(s.RiskFreeRate)
if err != nil {
log.Error(common.Statistics, err)
}
stats.FinalHoldings = last.Holdings
stats.InitialHoldings = stats.Events[0].Holdings
if last.ComplianceSnapshot == nil {
return errMissingSnapshots
}
stats.FinalOrders = *last.ComplianceSnapshot
s.StartDate = stats.Events[0].Time
s.EndDate = last.Time
cp := currency.NewPair(b.Currency(), q.Currency())
stats.PrintResults(exchangeName, assetItem, cp, s.FundManager.IsUsingExchangeLevelFunding())
finalResults = append(finalResults, FinalResultsHolder{
Exchange: exchangeName,
Asset: assetItem,
Pair: pair,
MaxDrawdown: stats.MaxDrawdown,
MarketMovement: stats.MarketMovement,
StrategyMovement: stats.StrategyMovement,
})
s.TotalLongOrders += stats.LongOrders
s.TotalShortOrders += stats.ShortOrders
s.TotalBuyOrders += stats.BuyOrders
s.TotalSellOrders += stats.SellOrders
s.TotalOrders += stats.TotalOrders
if stats.ShowMissingDataWarning {
s.WasAnyDataMissing = true
finalResults = append(finalResults, FinalResultsHolder{
Exchange: exchangeName,
Asset: assetItem,
Pair: cp,
MaxDrawdown: stats.MaxDrawdown,
MarketMovement: stats.MarketMovement,
StrategyMovement: stats.StrategyMovement,
})
if assetItem.IsFutures() {
s.TotalLongOrders += stats.BuyOrders
s.TotalShortOrders += stats.SellOrders
} else {
s.TotalBuyOrders += stats.BuyOrders
s.TotalSellOrders += stats.SellOrders
}
s.TotalOrders += stats.TotalOrders
if stats.ShowMissingDataWarning {
s.WasAnyDataMissing = true
}
}
}
}
@@ -300,8 +338,10 @@ func (s *Statistic) Serialise() (string, error) {
s.CurrencyStatistics = nil
for _, exchangeMap := range s.ExchangeAssetPairStatistics {
for _, assetMap := range exchangeMap {
for _, stats := range assetMap {
s.CurrencyStatistics = append(s.CurrencyStatistics, stats)
for _, baseMap := range assetMap {
for _, stats := range baseMap {
s.CurrencyStatistics = append(s.CurrencyStatistics, stats)
}
}
}
}

View File

@@ -7,6 +7,8 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
@@ -34,13 +36,22 @@ var (
func TestReset(t *testing.T) {
t.Parallel()
s := Statistic{
s := &Statistic{
TotalOrders: 1,
}
s.Reset()
err := s.Reset()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if s.TotalOrders != 0 {
t.Error("expected 0")
}
s = nil
err = s.Reset()
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNilPointer)
}
}
func TestAddDataEventForTime(t *testing.T) {
@@ -50,11 +61,11 @@ func TestAddDataEventForTime(t *testing.T) {
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
s := Statistic{}
err := s.SetupEventForTime(nil)
err := s.SetEventForOffset(nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt,
@@ -68,13 +79,13 @@ func TestAddDataEventForTime(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
if s.ExchangeAssetPairStatistics == nil {
t.Error("expected not nil")
}
if len(s.ExchangeAssetPairStatistics[exch][a][p].Events) != 1 {
if len(s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item].Events) != 1 {
t.Error("expected 1 event")
}
}
@@ -91,24 +102,23 @@ func TestAddSignalEventForTime(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetEventForOffset(&signal.Signal{})
if !errors.Is(err, errExchangeAssetPairStatsUnset) {
t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
b := &event.Base{}
err = s.SetEventForOffset(&signal.Signal{
Base: b,
})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
b.Exchange = exch
b.Time = tt
b.Interval = gctkline.OneDay
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: b,
Open: eleet,
Close: eleet,
@@ -116,16 +126,16 @@ func TestAddSignalEventForTime(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&signal.Signal{
Base: b,
ClosePrice: eleet,
Direction: gctorder.Buy,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -141,24 +151,18 @@ func TestAddExchangeEventForTime(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetEventForOffset(&order.Order{})
if !errors.Is(err, errExchangeAssetPairStatsUnset) {
t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
b := &event.Base{}
err = s.SetEventForOffset(&order.Order{
Base: b,
})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
}
b.Exchange = exch
b.Time = tt
b.Interval = gctkline.OneDay
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: b,
Open: eleet,
Close: eleet,
@@ -166,8 +170,8 @@ func TestAddExchangeEventForTime(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&order.Order{
Base: b,
@@ -179,8 +183,8 @@ func TestAddExchangeEventForTime(t *testing.T) {
OrderType: gctorder.Stop,
Leverage: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -196,17 +200,16 @@ func TestAddFillEventForTime(t *testing.T) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetEventForOffset(&fill.Fill{})
if err != nil && err.Error() != "exchangeAssetPairStatistics not setup" {
t.Error(err)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
b := &event.Base{}
err = s.SetEventForOffset(&fill.Fill{
Base: b,
})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
b.Exchange = exch
@@ -215,7 +218,7 @@ func TestAddFillEventForTime(t *testing.T) {
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: b,
Open: eleet,
Close: eleet,
@@ -223,8 +226,8 @@ func TestAddFillEventForTime(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&fill.Fill{
Base: b,
@@ -236,8 +239,8 @@ func TestAddFillEventForTime(t *testing.T) {
ExchangeFee: eleet,
Slippage: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -252,13 +255,13 @@ 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]*CurrencyPairStatistic)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
err = s.AddHoldingsForTime(&holdings.Holding{})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
}
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt,
@@ -272,8 +275,8 @@ func TestAddHoldingsForTime(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.AddHoldingsForTime(&holdings.Holding{
Pair: p,
@@ -295,8 +298,8 @@ func TestAddHoldingsForTime(t *testing.T) {
TotalValueLostToSlippage: eleet,
TotalValueLost: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -308,18 +311,22 @@ func TestAddComplianceSnapshotForTime(t *testing.T) {
p := currency.NewPair(currency.BTC, currency.USDT)
s := Statistic{}
err := s.AddComplianceSnapshotForTime(compliance.Snapshot{}, nil)
err := s.AddComplianceSnapshotForTime(nil, nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.AddComplianceSnapshotForTime(compliance.Snapshot{}, &fill.Fill{})
err = s.AddComplianceSnapshotForTime(nil, &fill.Fill{})
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.AddComplianceSnapshotForTime(&compliance.Snapshot{}, &fill.Fill{})
if !errors.Is(err, errExchangeAssetPairStatsUnset) {
t.Errorf("received: %v, expected: %v", err, errExchangeAssetPairStatsUnset)
}
s.setupMap(exch, a)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic)
s.ExchangeAssetPairStatistics = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic)
b := &event.Base{}
err = s.AddComplianceSnapshotForTime(compliance.Snapshot{}, &fill.Fill{Base: b})
err = s.AddComplianceSnapshotForTime(&compliance.Snapshot{}, &fill.Fill{Base: b})
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received: %v, expected: %v", err, errCurrencyStatisticsUnset)
}
@@ -328,7 +335,7 @@ func TestAddComplianceSnapshotForTime(t *testing.T) {
b.Interval = gctkline.OneDay
b.CurrencyPair = p
b.AssetType = a
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: b,
Open: eleet,
Close: eleet,
@@ -336,16 +343,16 @@ func TestAddComplianceSnapshotForTime(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.AddComplianceSnapshotForTime(compliance.Snapshot{
err = s.AddComplianceSnapshotForTime(&compliance.Snapshot{
Timestamp: tt,
}, &fill.Fill{
Base: b,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -488,11 +495,11 @@ func TestPrintAllEventsChronologically(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
err := s.SetupEventForTime(nil)
err := s.SetEventForOffset(nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt,
@@ -506,8 +513,8 @@ func TestPrintAllEventsChronologically(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&fill.Fill{
@@ -526,8 +533,8 @@ func TestPrintAllEventsChronologically(t *testing.T) {
ExchangeFee: eleet,
Slippage: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&signal.Signal{
@@ -541,8 +548,8 @@ func TestPrintAllEventsChronologically(t *testing.T) {
ClosePrice: eleet,
Direction: gctorder.Buy,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
s.PrintAllEventsChronologically()
@@ -552,8 +559,8 @@ func TestCalculateTheResults(t *testing.T) {
t.Parallel()
s := Statistic{}
err := s.CalculateAllResults()
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilArguments)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNilPointer)
}
tt := time.Now().Add(-gctkline.OneDay.Duration() * 7)
@@ -562,11 +569,11 @@ func TestCalculateTheResults(t *testing.T) {
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
p2 := currency.NewPair(currency.XRP, currency.DOGE)
err = s.SetupEventForTime(nil)
err = s.SetEventForOffset(nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt,
@@ -581,8 +588,8 @@ func TestCalculateTheResults(t *testing.T) {
High: eleet,
Volume: eleet,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&signal.Signal{
Base: &event.Base{
@@ -600,10 +607,10 @@ func TestCalculateTheResults(t *testing.T) {
Volume: eleet,
Direction: gctorder.Buy,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt,
@@ -618,8 +625,8 @@ func TestCalculateTheResults(t *testing.T) {
High: eleeb,
Volume: eleeb,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&signal.Signal{
@@ -638,11 +645,11 @@ func TestCalculateTheResults(t *testing.T) {
Volume: eleet,
Direction: gctorder.Buy,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt2,
@@ -657,8 +664,8 @@ func TestCalculateTheResults(t *testing.T) {
High: eleeb,
Volume: eleeb,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&signal.Signal{
Base: &event.Base{
@@ -676,11 +683,11 @@ func TestCalculateTheResults(t *testing.T) {
Volume: eleeb,
Direction: gctorder.Buy,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetupEventForTime(&kline.Kline{
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt2,
@@ -695,10 +702,10 @@ func TestCalculateTheResults(t *testing.T) {
High: eleeb,
Volume: eleeb,
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.SetEventForOffset(&signal.Signal{
signal4 := &signal.Signal{
Base: &event.Base{
Exchange: exch,
Time: tt2,
@@ -713,17 +720,18 @@ func TestCalculateTheResults(t *testing.T) {
ClosePrice: eleeb,
Volume: eleeb,
Direction: gctorder.Buy,
})
if err != nil {
t.Error(err)
}
err = s.SetEventForOffset(signal4)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
s.ExchangeAssetPairStatistics[exch][a][p].Events[1].Holdings.QuoteInitialFunds = eleet
s.ExchangeAssetPairStatistics[exch][a][p].Events[1].Holdings.TotalValue = eleeet
s.ExchangeAssetPairStatistics[exch][a][p2].Events[1].Holdings.QuoteInitialFunds = eleet
s.ExchangeAssetPairStatistics[exch][a][p2].Events[1].Holdings.TotalValue = eleeet
s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item].Events[1].Holdings.QuoteInitialFunds = eleet
s.ExchangeAssetPairStatistics[exch][a][p.Base.Item][p.Quote.Item].Events[1].Holdings.TotalValue = eleeet
s.ExchangeAssetPairStatistics[exch][a][p2.Base.Item][p2.Quote.Item].Events[1].Holdings.QuoteInitialFunds = eleet
s.ExchangeAssetPairStatistics[exch][a][p2.Base.Item][p2.Quote.Item].Events[1].Holdings.TotalValue = eleeet
funds, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, false)
funds, err := funding.SetupFundingManager(&engine.ExchangeManager{}, false, false, false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -770,7 +778,7 @@ func TestCalculateTheResults(t *testing.T) {
t.Errorf("received '%v' expected '%v'", err, errMissingSnapshots)
}
funds, err = funding.SetupFundingManager(&engine.ExchangeManager{}, false, true)
funds, err = funding.SetupFundingManager(&engine.ExchangeManager{}, false, true, false)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -784,6 +792,11 @@ func TestCalculateTheResults(t *testing.T) {
}
s.FundManager = funds
err = s.CalculateAllResults()
if !errors.Is(err, errMissingSnapshots) {
t.Errorf("received '%v' expected '%v'", err, errMissingSnapshots)
}
err = s.AddComplianceSnapshotForTime(&compliance.Snapshot{Timestamp: tt2}, signal4)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
@@ -794,7 +807,7 @@ func TestCalculateBiggestEventDrawdown(t *testing.T) {
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
var events []common.DataEventHandler
var events []data.Event
for i := int64(0); i < 100; i++ {
tt1 = tt1.Add(gctkline.OneDay.Duration())
even := &event.Base{
@@ -881,7 +894,7 @@ func TestCalculateBiggestEventDrawdown(t *testing.T) {
}
// bogus scenario
bogusEvent := []common.DataEventHandler{
bogusEvent := []data.Event{
&kline.Kline{
Base: &event.Base{
Exchange: exch,
@@ -911,3 +924,60 @@ func TestCalculateBiggestValueAtTimeDrawdown(t *testing.T) {
t.Errorf("received %v expected %v", err, errReceivedNoData)
}
}
func TestAddPNLForTime(t *testing.T) {
t.Parallel()
s := &Statistic{}
err := s.AddPNLForTime(nil)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received %v expected %v", err, gctcommon.ErrNilPointer)
}
sum := &portfolio.PNLSummary{}
err = s.AddPNLForTime(sum)
if !errors.Is(err, errExchangeAssetPairStatsUnset) {
t.Errorf("received %v expected %v", err, errExchangeAssetPairStatsUnset)
}
tt := time.Now().Add(-gctkline.OneDay.Duration() * 7)
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
err = s.SetEventForOffset(&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt,
Interval: gctkline.OneDay,
CurrencyPair: p,
AssetType: a,
Offset: 1,
},
Open: eleet,
Close: eleet,
Low: eleet,
High: eleet,
Volume: eleet,
})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
err = s.AddPNLForTime(sum)
if !errors.Is(err, errCurrencyStatisticsUnset) {
t.Errorf("received %v expected %v", err, errCurrencyStatisticsUnset)
}
sum.Exchange = exch
sum.Asset = a
sum.Pair = p
err = s.AddPNLForTime(sum)
if !errors.Is(err, errNoDataAtOffset) {
t.Errorf("received %v expected %v", err, errNoDataAtOffset)
}
sum.Offset = 1
err = s.AddPNLForTime(sum)
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
@@ -33,28 +34,28 @@ var (
// Statistic holds all statistical information for a backtester run, from drawdowns to ratios.
// Any currency specific information is handled in currencystatistics
type Statistic struct {
StrategyName string `json:"strategy-name"`
StrategyDescription string `json:"strategy-description"`
StrategyNickname string `json:"strategy-nickname"`
StrategyGoal string `json:"strategy-goal"`
StartDate time.Time `json:"start-date"`
EndDate time.Time `json:"end-date"`
CandleInterval gctkline.Interval `json:"candle-interval"`
RiskFreeRate decimal.Decimal `json:"risk-free-rate"`
ExchangeAssetPairStatistics map[string]map[asset.Item]map[currency.Pair]*CurrencyPairStatistic `json:"-"`
CurrencyStatistics []*CurrencyPairStatistic `json:"currency-statistics"`
TotalBuyOrders int64 `json:"total-buy-orders"`
TotalLongOrders int64 `json:"total-long-orders"`
TotalShortOrders int64 `json:"total-short-orders"`
TotalSellOrders int64 `json:"total-sell-orders"`
TotalOrders int64 `json:"total-orders"`
BiggestDrawdown *FinalResultsHolder `json:"biggest-drawdown,omitempty"`
BestStrategyResults *FinalResultsHolder `json:"best-start-results,omitempty"`
BestMarketMovement *FinalResultsHolder `json:"best-market-movement,omitempty"`
WasAnyDataMissing bool `json:"was-any-data-missing"`
FundingStatistics *FundingStatistics `json:"funding-statistics"`
FundManager funding.IFundingManager `json:"-"`
HasCollateral bool `json:"has-collateral"`
StrategyName string `json:"strategy-name"`
StrategyDescription string `json:"strategy-description"`
StrategyNickname string `json:"strategy-nickname"`
StrategyGoal string `json:"strategy-goal"`
StartDate time.Time `json:"start-date"`
EndDate time.Time `json:"end-date"`
CandleInterval gctkline.Interval `json:"candle-interval"`
RiskFreeRate decimal.Decimal `json:"risk-free-rate"`
ExchangeAssetPairStatistics map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*CurrencyPairStatistic `json:"exchange-asset-pair-statistics"`
CurrencyStatistics []*CurrencyPairStatistic `json:"currency-statistics"`
TotalBuyOrders int64 `json:"total-buy-orders"`
TotalLongOrders int64 `json:"total-long-orders"`
TotalShortOrders int64 `json:"total-short-orders"`
TotalSellOrders int64 `json:"total-sell-orders"`
TotalOrders int64 `json:"total-orders"`
BiggestDrawdown *FinalResultsHolder `json:"biggest-drawdown,omitempty"`
BestStrategyResults *FinalResultsHolder `json:"best-start-results,omitempty"`
BestMarketMovement *FinalResultsHolder `json:"best-market-movement,omitempty"`
WasAnyDataMissing bool `json:"was-any-data-missing"`
FundingStatistics *FundingStatistics `json:"funding-statistics"`
FundManager funding.IFundingManager `json:"-"`
HasCollateral bool `json:"has-collateral"`
}
// FinalResultsHolder holds important stats about a currency's performance
@@ -70,14 +71,14 @@ type FinalResultsHolder struct {
// Handler interface details what a statistic is expected to do
type Handler interface {
SetStrategyName(string)
SetupEventForTime(common.DataEventHandler) error
SetEventForOffset(common.EventHandler) error
SetEventForOffset(common.Event) error
AddHoldingsForTime(*holdings.Holding) error
AddComplianceSnapshotForTime(compliance.Snapshot, fill.Event) error
AddComplianceSnapshotForTime(*compliance.Snapshot, common.Event) error
CalculateAllResults() error
Reset()
Reset() error
Serialise() (string, error)
AddPNLForTime(*portfolio.PNLSummary) error
CreateLog(common.Event) (string, error)
}
// Results holds some statistics on results
@@ -122,16 +123,16 @@ type CurrencyStats interface {
// DataAtOffset is used to hold all event information
// at a time interval
type DataAtOffset struct {
Offset int64
ClosePrice decimal.Decimal
Time time.Time
Holdings holdings.Holding
Transactions compliance.Snapshot
DataEvent common.DataEventHandler
SignalEvent signal.Event
OrderEvent order.Event
FillEvent fill.Event
PNL portfolio.IPNL
Offset int64
ClosePrice decimal.Decimal
Time time.Time
Holdings holdings.Holding
ComplianceSnapshot *compliance.Snapshot
DataEvent data.Event
SignalEvent signal.Event
OrderEvent order.Event
FillEvent fill.Event
PNL portfolio.IPNL
}
// CurrencyPairStatistic Holds all events and statistics relevant to an exchange, asset type and currency pair
@@ -146,8 +147,6 @@ type CurrencyPairStatistic struct {
DoesPerformanceBeatTheMarket bool `json:"does-performance-beat-the-market"`
BuyOrders int64 `json:"buy-orders"`
LongOrders int64 `json:"long-orders"`
ShortOrders int64 `json:"short-orders"`
SellOrders int64 `json:"sell-orders"`
TotalOrders int64 `json:"total-orders"`
@@ -254,7 +253,6 @@ type TotalFundingStatistics struct {
HighestHoldingValue ValueAtTime `json:"highest-holding-value"`
LowestHoldingValue ValueAtTime `json:"lowest-holding-value"`
BenchmarkMarketMovement decimal.Decimal `json:"benchmark-market-movement"`
StrategyMovement decimal.Decimal `json:"strategy-movement"`
RiskFreeRate decimal.Decimal `json:"risk-free-rate"`
CompoundAnnualGrowthRate decimal.Decimal `json:"compound-annual-growth-rate"`
MaxDrawdown Swing `json:"max-drawdown"`

View File

@@ -3,21 +3,25 @@ package base
import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
)
// Strategy is base implementation of the Handler interface
type Strategy struct {
useSimultaneousProcessing bool
usingExchangeLevelFunding bool
}
// GetBaseData returns the non-interface version of the Handler
func (s *Strategy) GetBaseData(d data.Handler) (signal.Signal, error) {
if d == nil {
return signal.Signal{}, common.ErrNilArguments
return signal.Signal{}, gctcommon.ErrNilPointer
}
latest, err := d.Latest()
if err != nil {
return signal.Signal{}, err
}
latest := d.Latest()
if latest == nil {
return signal.Signal{}, common.ErrNilEvent
}
@@ -40,12 +44,10 @@ func (s *Strategy) SetSimultaneousProcessing(b bool) {
s.useSimultaneousProcessing = b
}
// UsingExchangeLevelFunding returns whether funding is based on currency pairs or individual currencies at the exchange level
func (s *Strategy) UsingExchangeLevelFunding() bool {
return s.usingExchangeLevelFunding
}
// SetExchangeLevelFunding sets whether funding is based on currency pairs or individual currencies at the exchange level
func (s *Strategy) SetExchangeLevelFunding(b bool) {
s.usingExchangeLevelFunding = b
// CloseAllPositions sends a closing signal to supported
// strategies, allowing them to sell off any positions held
// default use-case is for when a user closes the application when running
// a live strategy
func (s *Strategy) CloseAllPositions([]holdings.Holding, []data.Event) ([]signal.Event, error) {
return nil, gctcommon.ErrFunctionNotSupported
}

View File

@@ -11,6 +11,7 @@ import (
datakline "github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -20,11 +21,11 @@ func TestGetBase(t *testing.T) {
t.Parallel()
s := Strategy{}
_, err := s.GetBaseData(nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilArguments)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrNilPointer)
}
_, err = s.GetBaseData(&datakline.DataFromKline{})
_, err = s.GetBaseData(datakline.NewDataFromKline())
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
@@ -32,8 +33,8 @@ func TestGetBase(t *testing.T) {
exch := "binance"
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&kline.Kline{
d := &data.Base{}
err = d.SetStream([]data.Event{&kline.Kline{
Base: &event.Base{
Exchange: exch,
Time: tt,
@@ -47,15 +48,21 @@ func TestGetBase(t *testing.T) {
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
d.Next()
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
_, err = s.GetBaseData(&datakline.DataFromKline{
Item: gctkline.Item{},
Base: d,
RangeHolder: &gctkline.IntervalRangeHolder{},
})
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
}
@@ -73,26 +80,11 @@ func TestSetSimultaneousProcessing(t *testing.T) {
}
}
func TestUsingExchangeLevelFunding(t *testing.T) {
func TestCloseAllPositions(t *testing.T) {
t.Parallel()
s := &Strategy{}
if s.UsingExchangeLevelFunding() {
t.Error("expected false")
}
s.usingExchangeLevelFunding = true
if !s.UsingExchangeLevelFunding() {
t.Error("expected true")
}
}
func TestSetExchangeLevelFunding(t *testing.T) {
t.Parallel()
s := &Strategy{}
s.SetExchangeLevelFunding(true)
if !s.UsingExchangeLevelFunding() {
t.Error("expected true")
}
if !s.UsingExchangeLevelFunding() {
t.Error("expected true")
_, err := s.CloseAllPositions(nil, nil)
if !errors.Is(err, gctcommon.ErrFunctionNotSupported) {
t.Errorf("received '%v' expected '%v'", err, gctcommon.ErrFunctionNotSupported)
}
}

View File

@@ -16,4 +16,6 @@ var (
ErrInvalidCustomSettings = errors.New("invalid custom settings in config")
// ErrTooMuchBadData used when there is too much missing data
ErrTooMuchBadData = errors.New("backtesting cannot continue as there is too much invalid data. Please review your dataset")
// ErrNoDataToProcess is returned when simultaneous signal processing is enabled, but no events are passed in
ErrNoDataToProcess = errors.New("no kline data to process")
)

View File

@@ -1,16 +1,16 @@
# GoCryptoTrader Backtester: Ftxcashandcarry package
# GoCryptoTrader Backtester: Binancecashandcarry package
<img src="/backtester/common/backtester.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/ftxcashandcarry)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/binancecashandcarry)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This ftxcashandcarry package is part of the GoCryptoTrader codebase.
This binancecashandcarry package is part of the GoCryptoTrader codebase.
## This is still in active development
@@ -18,22 +18,25 @@ You can track ideas, planned features and what's in progress on this Trello boar
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## FTX Cash and carry strategy overview
## Binance Cash and carry strategy overview
## Important
This strategy was initially designed for the exchange FTX. It is currently being ported to Binance. It does not work at present.
### Description
Cash and carry is a strategy which takes advantage of the difference in pricing between a long-dated futures contract and a SPOT asset.
By default, this cash and carry strategy will, upon the first data event, purchase BTC-USD SPOT asset from FTX exchange and then, once filled, raise a SHORT for BTC-20210924 FUTURES contract.
By default, this cash and carry strategy will, upon the first data event, purchase BTC-USD SPOT asset from Binance exchange and then, once filled, raise a SHORT for BTC-20210924 FUTURES contract.
On the last event, the strategy will close the SHORT position by raising a LONG of the same contract amount, thereby netting the difference in prices
### Requirements
- At this time of writing, this strategy is only compatible with FTX
- At this time of writing, this strategy is only compatible with Binance
- This strategy *requires* `Simultaneous Signal Processing` aka [use-simultaneous-signal-processing](/backtester/config/README.md).
- This strategy *requires* `Exchange Level Funding` aka [use-exchange-level-funding](/backtester/config/README.md).
### Creating a strategy config
- The long-dated futures contract will need to be part of the `currency-settings` of the contract
- Funding for purchasing SPOT assets will need to be part of `funding-settings`
- See the [example config](./config/examples/ftx-cash-carry.strat)
- See the [example config](./config/strategyexamples/binance-cash-carry.strat)
### Customisation
This strategy does support strategy customisation in the following ways:

View File

@@ -1,17 +1,20 @@
package ftxcashandcarry
package binancecashandcarry
import (
"errors"
"fmt"
"strings"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -50,13 +53,13 @@ var errNotSetup = errors.New("sent incomplete signals")
// in allowing a strategy to only place an order for X currency if Y currency's price is Z
func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTransferer, p portfolio.Handler) ([]signal.Event, error) {
if len(d) == 0 {
return nil, errNoSignals
return nil, base.ErrNoDataToProcess
}
if f == nil {
return nil, fmt.Errorf("%w missing funding transferred", common.ErrNilArguments)
return nil, fmt.Errorf("%w missing funding transferred", gctcommon.ErrNilPointer)
}
if p == nil {
return nil, fmt.Errorf("%w missing portfolio handler", common.ErrNilArguments)
return nil, fmt.Errorf("%w missing portfolio handler", gctcommon.ErrNilPointer)
}
var response []signal.Event
sortedSignals, err := sortSignals(d)
@@ -64,24 +67,35 @@ func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTra
return nil, err
}
for _, v := range sortedSignals {
pos, err := p.GetPositions(v.futureSignal.Latest())
for i := range sortedSignals {
var latestSpot, latestFuture data.Event
latestSpot, err = sortedSignals[i].spotSignal.Latest()
if err != nil {
return nil, err
}
spotSignal, err := s.GetBaseData(v.spotSignal)
latestFuture, err = sortedSignals[i].futureSignal.Latest()
if err != nil {
return nil, err
}
futuresSignal, err := s.GetBaseData(v.futureSignal)
var pos []order.Position
pos, err = p.GetPositions(latestFuture)
if err != nil {
return nil, err
}
var spotSignal, futuresSignal signal.Signal
spotSignal, err = s.GetBaseData(sortedSignals[i].spotSignal)
if err != nil {
return nil, err
}
futuresSignal, err = s.GetBaseData(sortedSignals[i].futureSignal)
if err != nil {
return nil, err
}
spotSignal.SetDirection(order.DoNothing)
futuresSignal.SetDirection(order.DoNothing)
fp := v.futureSignal.Latest().GetClosePrice()
sp := v.spotSignal.Latest().GetClosePrice()
fp := latestFuture.GetClosePrice()
sp := latestSpot.GetClosePrice()
diffBetweenFuturesSpot := fp.Sub(sp).Div(sp).Mul(decimal.NewFromInt(100))
futuresSignal.AppendReasonf("Futures Spot Difference: %v%%", diffBetweenFuturesSpot)
if len(pos) > 0 && pos[len(pos)-1].Status == order.Open {
@@ -93,7 +107,13 @@ func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTra
response = append(response, &spotSignal, &futuresSignal)
continue
}
signals, err := s.createSignals(pos, &spotSignal, &futuresSignal, diffBetweenFuturesSpot, v.futureSignal.IsLastEvent())
var isLastEvent bool
var signals []signal.Event
isLastEvent, err = sortedSignals[i].futureSignal.IsLastEvent()
if err != nil {
return nil, err
}
signals, err = s.createSignals(pos, &spotSignal, &futuresSignal, diffBetweenFuturesSpot, isLastEvent)
if err != nil {
return nil, err
}
@@ -102,14 +122,57 @@ func (s *Strategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTra
return response, nil
}
// CloseAllPositions is this strategy's implementation on how to
// unwind all positions in the event of a closure
func (s *Strategy) CloseAllPositions(holdings []holdings.Holding, prices []data.Event) ([]signal.Event, error) {
var spotSignals, futureSignals []signal.Event
signalTime := time.Now().UTC()
for i := range holdings {
for j := range prices {
if prices[j].GetExchange() != holdings[i].Exchange ||
prices[j].GetAssetType() != holdings[i].Asset ||
!prices[j].Pair().Equal(holdings[i].Pair) {
continue
}
sig := &signal.Signal{
Base: &event.Base{
Offset: holdings[i].Offset + 1,
Exchange: holdings[i].Exchange,
Time: signalTime,
Interval: prices[j].GetInterval(),
CurrencyPair: holdings[i].Pair,
UnderlyingPair: prices[j].GetUnderlyingPair(),
AssetType: holdings[i].Asset,
Reasons: []string{"closing position on close"},
},
OpenPrice: prices[j].GetOpenPrice(),
HighPrice: prices[j].GetHighPrice(),
LowPrice: prices[j].GetLowPrice(),
ClosePrice: prices[j].GetClosePrice(),
Volume: prices[j].GetVolume(),
Amount: holdings[i].BaseSize,
Direction: order.ClosePosition,
CollateralCurrency: holdings[i].Pair.Base,
}
if prices[j].GetAssetType().IsFutures() {
futureSignals = append(futureSignals, sig)
} else {
spotSignals = append(spotSignals, sig)
}
}
}
// close out future positions first
return append(futureSignals, spotSignals...), nil
}
// createSignals creates signals based on the relationships between
// futures and spot signals
func (s *Strategy) createSignals(pos []order.Position, spotSignal, futuresSignal *signal.Signal, diffBetweenFuturesSpot decimal.Decimal, isLastEvent bool) ([]signal.Event, error) {
if spotSignal == nil {
return nil, fmt.Errorf("%w missing spot signal", common.ErrNilArguments)
return nil, fmt.Errorf("%w missing spot signal", gctcommon.ErrNilPointer)
}
if futuresSignal == nil {
return nil, fmt.Errorf("%w missing futures signal", common.ErrNilArguments)
return nil, fmt.Errorf("%w missing futures signal", gctcommon.ErrNilPointer)
}
var response []signal.Event
switch {
@@ -159,42 +222,58 @@ func (s *Strategy) createSignals(pos []order.Position, spotSignal, futuresSignal
// sortSignals links spot and futures signals in order to create cash
// and carry signals
func sortSignals(d []data.Handler) (map[currency.Pair]cashCarrySignals, error) {
func sortSignals(d []data.Handler) ([]cashCarrySignals, error) {
if len(d) == 0 {
return nil, errNoSignals
return nil, base.ErrNoDataToProcess
}
var response = make(map[currency.Pair]cashCarrySignals, len(d))
var carryMap = make(map[*currency.Item]map[*currency.Item]cashCarrySignals, len(d))
for i := range d {
l := d[i].Latest()
l, err := d[i].Latest()
if err != nil {
return nil, err
}
if !strings.EqualFold(l.GetExchange(), exchangeName) {
return nil, fmt.Errorf("%w, received '%v'", errOnlyFTXSupported, l.GetExchange())
return nil, fmt.Errorf("%w, received '%v'", errOnlyBinanceSupported, l.GetExchange())
}
a := l.GetAssetType()
switch {
case a == asset.Spot:
entry := response[l.Pair().Format(currency.EMPTYFORMAT)]
b := carryMap[l.Pair().Base.Item]
if b == nil {
carryMap[l.Pair().Base.Item] = make(map[*currency.Item]cashCarrySignals)
}
entry := carryMap[l.Pair().Base.Item][l.Pair().Quote.Item]
entry.spotSignal = d[i]
response[l.Pair().Format(currency.EMPTYFORMAT)] = entry
carryMap[l.Pair().Base.Item][l.Pair().Quote.Item] = entry
case a.IsFutures():
u := l.GetUnderlyingPair()
entry := response[u.Format(currency.EMPTYFORMAT)]
b := carryMap[u.Base.Item]
if b == nil {
carryMap[u.Base.Item] = make(map[*currency.Item]cashCarrySignals)
}
entry := carryMap[u.Base.Item][u.Quote.Item]
entry.futureSignal = d[i]
response[u.Format(currency.EMPTYFORMAT)] = entry
carryMap[u.Base.Item][u.Quote.Item] = entry
default:
return nil, errFuturesOnly
}
}
var resp []cashCarrySignals
// validate that each set of signals is matched
for _, v := range response {
if v.futureSignal == nil {
return nil, fmt.Errorf("%w missing future signal", errNotSetup)
}
if v.spotSignal == nil {
return nil, fmt.Errorf("%w missing spot signal", errNotSetup)
for _, b := range carryMap {
for _, v := range b {
if v.futureSignal == nil {
return nil, fmt.Errorf("%w missing future signal", errNotSetup)
}
if v.spotSignal == nil {
return nil, fmt.Errorf("%w missing spot signal", errNotSetup)
}
resp = append(resp, v)
}
}
return response, nil
return resp, nil
}
// SetCustomSettings can override default settings

View File

@@ -1,4 +1,4 @@
package ftxcashandcarry
package binancecashandcarry
import (
"errors"
@@ -10,17 +10,21 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/data"
datakline "github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
eventkline "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"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"
)
const testExchange = "binance"
func TestName(t *testing.T) {
t.Parallel()
d := Strategy{}
@@ -49,8 +53,8 @@ func TestSetCustomSettings(t *testing.T) {
t.Parallel()
s := Strategy{}
err := s.SetCustomSettings(nil)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
float14 := float64(14)
mappalopalous := make(map[string]interface{})
@@ -58,8 +62,8 @@ func TestSetCustomSettings(t *testing.T) {
mappalopalous[closeShortDistancePercentageString] = float14
err = s.SetCustomSettings(mappalopalous)
if err != nil {
t.Error(err)
if !errors.Is(err, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
mappalopalous[openShortDistancePercentageString] = "14"
@@ -109,11 +113,11 @@ func TestSetDefaults(t *testing.T) {
func TestSortSignals(t *testing.T) {
t.Parallel()
dInsert := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
exch := "ftx"
exch := testExchange
a := asset.Spot
p := currency.NewPair(currency.BTC, currency.USDT)
d := data.Base{}
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
d := &data.Base{}
err := d.SetStream([]data.Event{&eventkline.Kline{
Base: &event.Base{
Exchange: exch,
Time: dInsert,
@@ -127,19 +131,25 @@ func TestSortSignals(t *testing.T) {
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
d.Next()
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
da := &datakline.DataFromKline{
Item: gctkline.Item{},
Base: d,
RangeHolder: &gctkline.IntervalRangeHolder{},
}
_, err := sortSignals([]data.Handler{da})
_, err = sortSignals([]data.Handler{da})
if !errors.Is(err, errNotSetup) {
t.Errorf("received: %v, expected: %v", err, errNotSetup)
}
d2 := data.Base{}
d2.SetStream([]common.DataEventHandler{&eventkline.Kline{
d2 := &data.Base{}
err = d2.SetStream([]data.Event{&eventkline.Kline{
Base: &event.Base{
Exchange: exch,
Time: dInsert,
@@ -154,7 +164,13 @@ func TestSortSignals(t *testing.T) {
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
d2.Next()
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
_, err = d2.Next()
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
da2 := &datakline.DataFromKline{
Item: gctkline.Item{},
Base: d2,
@@ -169,7 +185,7 @@ func TestSortSignals(t *testing.T) {
func TestCreateSignals(t *testing.T) {
t.Parallel()
s := Strategy{}
var expectedError = common.ErrNilArguments
var expectedError = gctcommon.ErrNilPointer
_, err := s.createSignals(nil, nil, nil, decimal.Zero, false)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
@@ -282,14 +298,14 @@ func TestCreateSignals(t *testing.T) {
}
}
// funderino overrides default implementation
type funderino struct {
// fakeFunds overrides default implementation
type fakeFunds struct {
funding.FundManager
hasBeenLiquidated bool
}
// HasExchangeBeenLiquidated overrides default implementation
func (f funderino) HasExchangeBeenLiquidated(_ common.EventHandler) bool {
func (f fakeFunds) HasExchangeBeenLiquidated(_ common.Event) bool {
return f.hasBeenLiquidated
}
@@ -299,7 +315,7 @@ type portfolerino struct {
}
// GetPositions overrides default implementation
func (p portfolerino) GetPositions(common.EventHandler) ([]gctorder.Position, error) {
func (p portfolerino) GetPositions(common.Event) ([]gctorder.Position, error) {
return []gctorder.Position{
{
Exchange: exchangeName,
@@ -314,16 +330,14 @@ func (p portfolerino) GetPositions(common.EventHandler) ([]gctorder.Position, er
func TestOnSimultaneousSignals(t *testing.T) {
t.Parallel()
s := Strategy{}
var expectedError = errNoSignals
_, err := s.OnSimultaneousSignals(nil, nil, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
if !errors.Is(err, base.ErrNoDataToProcess) {
t.Errorf("received '%v' expected '%v", err, base.ErrNoDataToProcess)
}
expectedError = common.ErrNilArguments
cp := currency.NewPair(currency.BTC, currency.USD)
d := &datakline.DataFromKline{
Base: data.Base{},
Base: &data.Base{},
Item: gctkline.Item{
Exchange: exchangeName,
Asset: asset.Spot,
@@ -332,7 +346,7 @@ func TestOnSimultaneousSignals(t *testing.T) {
},
}
tt := time.Now()
d.SetStream([]common.DataEventHandler{&eventkline.Kline{
err = d.SetStream([]data.Event{&eventkline.Kline{
Base: &event.Base{
Exchange: exchangeName,
Time: tt,
@@ -346,27 +360,32 @@ func TestOnSimultaneousSignals(t *testing.T) {
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
_, err = d.Next()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
d.Next()
signals := []data.Handler{
d,
}
f := &funderino{}
f := &fakeFunds{}
_, err = s.OnSimultaneousSignals(signals, f, nil)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
if !errors.Is(err, gctcommon.ErrNilPointer) {
t.Errorf("received '%v' expected '%v", err, gctcommon.ErrNilPointer)
}
p := &portfolerino{}
expectedError = errNotSetup
_, err = s.OnSimultaneousSignals(signals, f, p)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
if !errors.Is(err, errNotSetup) {
t.Errorf("received '%v' expected '%v", err, errNotSetup)
}
expectedError = nil
d2 := &datakline.DataFromKline{
Base: data.Base{},
Base: &data.Base{},
Item: gctkline.Item{
Exchange: exchangeName,
Asset: asset.Futures,
@@ -374,7 +393,7 @@ func TestOnSimultaneousSignals(t *testing.T) {
UnderlyingPair: cp,
},
}
d2.SetStream([]common.DataEventHandler{&eventkline.Kline{
err = d2.SetStream([]data.Event{&eventkline.Kline{
Base: &event.Base{
Exchange: exchangeName,
Time: tt,
@@ -389,14 +408,21 @@ func TestOnSimultaneousSignals(t *testing.T) {
High: decimal.NewFromInt(1337),
Volume: decimal.NewFromInt(1337),
}})
d2.Next()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
_, err = d2.Next()
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
signals = []data.Handler{
d,
d2,
}
resp, err := s.OnSimultaneousSignals(signals, f, p)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
if len(resp) != 2 {
t.Errorf("received '%v' expected '%v", len(resp), 2)
@@ -404,8 +430,8 @@ func TestOnSimultaneousSignals(t *testing.T) {
f.hasBeenLiquidated = true
resp, err = s.OnSimultaneousSignals(signals, f, p)
if !errors.Is(err, expectedError) {
t.Errorf("received '%v' expected '%v", err, expectedError)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
if len(resp) != 2 {
t.Fatalf("received '%v' expected '%v", len(resp), 2)
@@ -414,3 +440,83 @@ func TestOnSimultaneousSignals(t *testing.T) {
t.Errorf("received '%v' expected '%v", resp[0].GetDirection(), gctorder.DoNothing)
}
}
func TestCloseAllPositions(t *testing.T) {
t.Parallel()
s := Strategy{}
_, err := s.CloseAllPositions(nil, nil)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
leet := decimal.NewFromInt(1337)
cp := currency.NewPair(currency.BTC, currency.USD)
h := []holdings.Holding{
{
Offset: 1,
Item: cp.Base,
Pair: cp,
Asset: asset.Spot,
Exchange: testExchange,
},
{
Offset: 1,
Item: cp.Base,
Pair: cp,
Asset: asset.Futures,
Exchange: testExchange,
},
}
p := []data.Event{
&signal.Signal{
Base: &event.Base{
Offset: 1,
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.OneDay,
CurrencyPair: cp,
UnderlyingPair: cp,
AssetType: asset.Spot,
},
OpenPrice: leet,
HighPrice: leet,
LowPrice: leet,
ClosePrice: leet,
Volume: leet,
BuyLimit: leet,
SellLimit: leet,
Amount: leet,
Direction: gctorder.Buy,
},
&signal.Signal{
Base: &event.Base{
Offset: 1,
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.OneDay,
CurrencyPair: cp,
UnderlyingPair: cp,
AssetType: asset.Futures,
},
OpenPrice: leet,
HighPrice: leet,
LowPrice: leet,
ClosePrice: leet,
Volume: leet,
BuyLimit: leet,
SellLimit: leet,
Amount: leet,
Direction: gctorder.Buy,
},
}
positionsToClose, err := s.CloseAllPositions(h, p)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v", err, nil)
}
if len(positionsToClose) != 2 {
t.Errorf("received '%v' expected '%v", len(positionsToClose), 2)
}
if !positionsToClose[0].GetAssetType().IsFutures() {
t.Errorf("received '%v' expected '%v", positionsToClose[0].GetAssetType(), asset.Futures)
}
}

View File

@@ -1,4 +1,4 @@
package ftxcashandcarry
package binancecashandcarry
import (
"errors"
@@ -9,17 +9,16 @@ import (
const (
// Name is the strategy name
Name = "ftx-cash-carry"
Name = "binance-cash-carry"
description = `A cash and carry trade (or basis trading) consists in taking advantage of the premium of a futures contract over the spot price. For example if Ethereum Futures are trading well above its Spot price (contango) you could perform an arbitrage and take advantage of this opportunity.`
exchangeName = "ftx"
exchangeName = "binance"
openShortDistancePercentageString = "openShortDistancePercentage"
closeShortDistancePercentageString = "closeShortDistancePercentage"
)
var (
errFuturesOnly = errors.New("can only work with futures")
errOnlyFTXSupported = errors.New("only FTX supported for this strategy")
errNoSignals = errors.New("no data signals to process")
errFuturesOnly = errors.New("can only work with futures")
errOnlyBinanceSupported = errors.New("only Binance supported for this strategy")
)
// Strategy is an implementation of the Handler interface

View File

@@ -44,13 +44,21 @@ func (s *Strategy) OnSignal(d data.Handler, _ funding.IFundingTransferer, _ port
return nil, err
}
if !d.HasDataAtTime(d.Latest().GetTime()) {
latest, err := d.Latest()
if err != nil {
return nil, err
}
hasDataAtTime, err := d.HasDataAtTime(latest.GetTime())
if err != nil {
return nil, err
}
if !hasDataAtTime {
es.SetDirection(order.MissingData)
es.AppendReasonf("missing data at %v, cannot perform any actions", d.Latest().GetTime())
es.AppendReasonf("missing data at %v, cannot perform any actions", latest.GetTime())
return &es, nil
}
es.SetPrice(d.Latest().GetClosePrice())
es.SetPrice(latest.GetClosePrice())
es.SetDirection(order.Buy)
es.AppendReason("DCA purchases on every iteration")
return &es, nil

Some files were not shown because too many files have changed in this diff Show More