From 10f7ff323664a4ff2c74987fb91c5cfe644e553c Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 23 Aug 2022 14:22:06 +1000 Subject: [PATCH] backtester: custom strategy plugins (#989) * Adds custom strategy * docs and structure * docs * rn * Documents plugins, adds custom strat config * mini fixes. Fleshes strategy test * docgen * Updates plugins to allow for multiple strategies to be loaded * docs * docs regen * fix doc accuracy * why did I add the word custom? --- README.md | 8 +- backtester/config/config_test.go | 61 +++++++ .../examples/custom-plugin-strategy.strat | 71 ++++++++ .../eventhandlers/strategies/strategies.go | 44 ++++- .../strategies/strategies_test.go | 51 ++++++ .../strategies/strategies_types.go | 8 + backtester/eventtypes/event/event.go | 2 +- backtester/main.go | 164 ++++++++++-------- backtester/plugins/README.md | 70 ++++++++ backtester/plugins/strategies/README.md | 64 +++++++ .../plugins/strategies/example/README.md | 71 ++++++++ .../strategies/example/example-strategy.go | 89 ++++++++++ backtester/plugins/strategies/loader.go | 44 +++++ backtester/plugins/strategies/loader_test.go | 65 +++++++ .../backtester_plugins_readme.tmpl | 36 ++++ ...ter_plugins_strategies_example_readme.tmpl | 37 ++++ .../backtester_plugins_strategies_readme.tmpl | 30 ++++ 17 files changed, 829 insertions(+), 86 deletions(-) create mode 100644 backtester/config/examples/custom-plugin-strategy.strat create mode 100644 backtester/plugins/README.md create mode 100644 backtester/plugins/strategies/README.md create mode 100644 backtester/plugins/strategies/example/README.md create mode 100644 backtester/plugins/strategies/example/example-strategy.go create mode 100644 backtester/plugins/strategies/loader.go create mode 100644 backtester/plugins/strategies/loader_test.go create mode 100644 cmd/documentation/backtester_templates/backtester_plugins_readme.tmpl create mode 100644 cmd/documentation/backtester_templates/backtester_plugins_strategies_example_readme.tmpl create mode 100644 cmd/documentation/backtester_templates/backtester_plugins_strategies_readme.tmpl diff --git a/README.md b/README.md index 518d6ad9..5a4e1e28 100644 --- a/README.md +++ b/README.md @@ -145,10 +145,10 @@ Binaries will be published once the codebase reaches a stable condition. |User|Contribution Amount| |--|--| -| [thrasher-](https://github.com/thrasher-) | 666 | -| [shazbert](https://github.com/shazbert) | 258 | -| [gloriousCode](https://github.com/gloriousCode) | 197 | -| [dependabot[bot]](https://github.com/apps/dependabot) | 89 | +| [thrasher-](https://github.com/thrasher-) | 667 | +| [shazbert](https://github.com/shazbert) | 260 | +| [gloriousCode](https://github.com/gloriousCode) | 199 | +| [dependabot[bot]](https://github.com/apps/dependabot) | 99 | | [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 | | [xtda](https://github.com/xtda) | 47 | | [lrascao](https://github.com/lrascao) | 27 | diff --git a/backtester/config/config_test.go b/backtester/config/config_test.go index 31f33e95..5aa25d90 100644 --- a/backtester/config/config_test.go +++ b/backtester/config/config_test.go @@ -536,6 +536,67 @@ func TestGenerateConfigForDCAAPICandles(t *testing.T) { } } +func TestGenerateConfigForPluginStrategy(t *testing.T) { + if !saveConfig { + t.Skip() + } + cfg := Config{ + Nickname: "ExamplePluginStrategy", + Goal: "To demonstrate that custom strategies can be used", + StrategySettings: StrategySettings{ + Name: "custom-strategy", + }, + CurrencySettings: []CurrencySettings{ + { + ExchangeName: testExchange, + Asset: asset.Spot, + Base: currency.BTC, + Quote: currency.USDT, + SpotDetails: &SpotDetails{ + InitialQuoteFunds: initialFunds1000000, + }, + BuySide: minMax, + SellSide: minMax, + MakerFee: &makerFee, + TakerFee: &takerFee, + }, + }, + DataSettings: DataSettings{ + Interval: kline.OneDay, + DataType: common.CandleStr, + APIData: &APIData{ + StartDate: startDate, + EndDate: endDate, + InclusiveEndDate: false, + }, + }, + PortfolioSettings: PortfolioSettings{ + BuySide: minMax, + SellSide: minMax, + Leverage: Leverage{ + CanUseLeverage: false, + }, + }, + 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, "examples", "custom-plugin-strategy.strat"), result, file.DefaultPermissionOctal) + if err != nil { + t.Error(err) + } + } +} + func TestGenerateConfigForDCAAPICandlesExchangeLevelFunding(t *testing.T) { if !saveConfig { t.Skip() diff --git a/backtester/config/examples/custom-plugin-strategy.strat b/backtester/config/examples/custom-plugin-strategy.strat new file mode 100644 index 00000000..e8739742 --- /dev/null +++ b/backtester/config/examples/custom-plugin-strategy.strat @@ -0,0 +1,71 @@ +{ + "nickname": "ExamplePluginStrategy", + "goal": "To demonstrate that custom strategies can be used", + "strategy-settings": { + "name": "custom-strategy", + "use-simultaneous-signal-processing": false, + "disable-usd-tracking": false + }, + "funding-settings": { + "use-exchange-level-funding": false + }, + "currency-settings": [ + { + "exchange-name": "ftx", + "asset": "spot", + "base": "BTC", + "quote": "USDT", + "spot-details": { + "initial-quote-funds": "1000000" + }, + "buy-side": { + "minimum-size": "0.005", + "maximum-size": "2", + "maximum-total": "40000" + }, + "sell-side": { + "minimum-size": "0.005", + "maximum-size": "2", + "maximum-total": "40000" + }, + "min-slippage-percent": "0", + "max-slippage-percent": "0", + "maker-fee-override": "0.0002", + "taker-fee-override": "0.0007", + "maximum-holdings-ratio": "0", + "skip-candle-volume-fitting": false, + "use-exchange-order-limits": false, + "use-exchange-pnl-calculation": false + } + ], + "data-settings": { + "interval": 86400000000000, + "data-type": "candle", + "api-data": { + "start-date": "2021-08-01T00:00:00+10:00", + "end-date": "2021-12-01T00:00:00+11:00", + "inclusive-end-date": false + } + }, + "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.005", + "maximum-size": "2", + "maximum-total": "40000" + }, + "sell-side": { + "minimum-size": "0.005", + "maximum-size": "2", + "maximum-total": "40000" + } + }, + "statistic-settings": { + "risk-free-rate": "0.03" + } +} \ No newline at end of file diff --git a/backtester/eventhandlers/strategies/strategies.go b/backtester/eventhandlers/strategies/strategies.go index e5e8d50c..c0575c31 100644 --- a/backtester/eventhandlers/strategies/strategies.go +++ b/backtester/eventhandlers/strategies/strategies.go @@ -3,42 +3,68 @@ package strategies import ( "fmt" "strings" + "sync" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/ftxcashandcarry" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/rsi" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/top2bottom2" + "github.com/thrasher-corp/gocryptotrader/common" ) // LoadStrategyByName returns the strategy by its name func LoadStrategyByName(name string, useSimultaneousProcessing bool) (Handler, error) { - strats := GetStrategies() - for i := range strats { - if !strings.EqualFold(name, strats[i].Name()) { + strategies := GetStrategies() + for i := range strategies { + if !strings.EqualFold(name, strategies[i].Name()) { continue } if useSimultaneousProcessing { - if !strats[i].SupportsSimultaneousProcessing() { + if !strategies[i].SupportsSimultaneousProcessing() { return nil, fmt.Errorf( "strategy '%v' %w", name, base.ErrSimultaneousProcessingNotSupported) } - strats[i].SetSimultaneousProcessing(useSimultaneousProcessing) + strategies[i].SetSimultaneousProcessing(useSimultaneousProcessing) } - return strats[i], nil + return strategies[i], nil } return nil, fmt.Errorf("strategy '%v' %w", name, base.ErrStrategyNotFound) } // GetStrategies returns a static list of set strategies // they must be set in here for the backtester to recognise them -func GetStrategies() []Handler { - return []Handler{ +func GetStrategies() StrategyHolder { + m.Lock() + defer m.Unlock() + return strategyHolder +} + +// AddStrategy will add a strategy to the list of strategies +func AddStrategy(strategy Handler) error { + if strategy == nil { + return fmt.Errorf("%w strategy handler", common.ErrNilPointer) + } + m.Lock() + defer m.Unlock() + for i := range strategyHolder { + if strings.EqualFold(strategyHolder[i].Name(), strategy.Name()) { + return fmt.Errorf("'%v' %w", strategy.Name(), ErrStrategyAlreadyExists) + } + } + strategyHolder = append(strategyHolder, strategy) + return nil +} + +var ( + m sync.Mutex + + strategyHolder = StrategyHolder{ new(dollarcostaverage.Strategy), new(rsi.Strategy), new(top2bottom2.Strategy), new(ftxcashandcarry.Strategy), } -} +) diff --git a/backtester/eventhandlers/strategies/strategies_test.go b/backtester/eventhandlers/strategies/strategies_test.go index 4fd37049..d3b0f6b5 100644 --- a/backtester/eventhandlers/strategies/strategies_test.go +++ b/backtester/eventhandlers/strategies/strategies_test.go @@ -4,9 +4,14 @@ import ( "errors" "testing" + "github.com/thrasher-corp/gocryptotrader/backtester/data" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/rsi" + "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal" + "github.com/thrasher-corp/gocryptotrader/backtester/funding" + "github.com/thrasher-corp/gocryptotrader/common" ) func TestGetStrategies(t *testing.T) { @@ -55,3 +60,49 @@ func TestLoadStrategyByName(t *testing.T) { t.Errorf("received: %v, expected: %v", err, nil) } } + +type customStrategy struct { + base.Strategy +} + +func (s *customStrategy) Name() string { + return "custom-strategy" +} +func (s *customStrategy) Description() string { + return "this is a demonstration of loading strategies via custom plugins" +} +func (s *customStrategy) SupportsSimultaneousProcessing() bool { + return true +} +func (s *customStrategy) OnSignal(d data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) (signal.Event, error) { + return s.createSignal(d) +} +func (s *customStrategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTransferer, p portfolio.Handler) ([]signal.Event, error) { + return nil, nil +} +func (s *customStrategy) createSignal(d data.Handler) (*signal.Signal, error) { + return nil, nil +} +func (s *customStrategy) SetCustomSettings(map[string]interface{}) error { + return nil +} + +// SetDefaults sets default values for overridable custom settings +func (s *customStrategy) SetDefaults() {} + +func TestAddStrategy(t *testing.T) { + t.Parallel() + err := AddStrategy(nil) + if !errors.Is(err, common.ErrNilPointer) { + t.Errorf("received '%v' expected '%v'", err, common.ErrNilPointer) + } + err = AddStrategy(new(dollarcostaverage.Strategy)) + if !errors.Is(err, ErrStrategyAlreadyExists) { + t.Errorf("received '%v' expected '%v'", err, ErrStrategyAlreadyExists) + } + + err = AddStrategy(new(customStrategy)) + if !errors.Is(err, nil) { + t.Errorf("received '%v' expected '%v'", err, nil) + } +} diff --git a/backtester/eventhandlers/strategies/strategies_types.go b/backtester/eventhandlers/strategies/strategies_types.go index f7fdd4a5..792ba1d1 100644 --- a/backtester/eventhandlers/strategies/strategies_types.go +++ b/backtester/eventhandlers/strategies/strategies_types.go @@ -1,12 +1,20 @@ package strategies import ( + "errors" + "github.com/thrasher-corp/gocryptotrader/backtester/data" "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio" "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal" "github.com/thrasher-corp/gocryptotrader/backtester/funding" ) +// ErrStrategyAlreadyExists returned when a strategy matches the same name +var ErrStrategyAlreadyExists = errors.New("strategy already exists") + +// StrategyHolder holds strategies +type StrategyHolder []Handler + // Handler defines all functions required to run strategies against data events type Handler interface { Name() string diff --git a/backtester/eventtypes/event/event.go b/backtester/eventtypes/event/event.go index b9611562..57014878 100644 --- a/backtester/eventtypes/event/event.go +++ b/backtester/eventtypes/event/event.go @@ -77,7 +77,7 @@ func (b *Base) GetReasons() []string { return b.Reasons } -// GetBase returns an event base +// GetBase returns the underlying base func (b *Base) GetBase() *Base { return b } diff --git a/backtester/main.go b/backtester/main.go index ae1c9491..099905f2 100644 --- a/backtester/main.go +++ b/backtester/main.go @@ -9,19 +9,104 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/common" "github.com/thrasher-corp/gocryptotrader/backtester/config" backtest "github.com/thrasher-corp/gocryptotrader/backtester/engine" + "github.com/thrasher-corp/gocryptotrader/backtester/plugins/strategies" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/signaler" ) +var configPath, templatePath, reportOutput, strategyPluginPath string +var printLogo, generateReport, darkReport, verbose, colourOutput, logSubHeader bool + func main() { - var configPath, templatePath, reportOutput string - var printLogo, generateReport, darkReport, verbose, colourOutput, logSubHeader bool wd, err := os.Getwd() if err != nil { fmt.Printf("Could not get working directory. Error: %v.\n", err) os.Exit(1) } + parseFlags(wd) + if !colourOutput { + common.PurgeColours() + } + var bt *backtest.BackTest + var cfg *config.Config + log.GlobalLogConfig = log.GenDefaultSettings() + log.GlobalLogConfig.AdvancedSettings.ShowLogSystemName = convert.BoolPtr(logSubHeader) + log.GlobalLogConfig.AdvancedSettings.Headers.Info = common.ColourInfo + "[INFO]" + common.ColourDefault + log.GlobalLogConfig.AdvancedSettings.Headers.Warn = common.ColourWarn + "[WARN]" + common.ColourDefault + log.GlobalLogConfig.AdvancedSettings.Headers.Debug = common.ColourDebug + "[DEBUG]" + common.ColourDefault + log.GlobalLogConfig.AdvancedSettings.Headers.Error = common.ColourError + "[ERROR]" + common.ColourDefault + err = log.SetupGlobalLogger() + if err != nil { + fmt.Printf("Could not setup global logger. Error: %v.\n", err) + os.Exit(1) + } + + err = common.RegisterBacktesterSubLoggers() + if err != nil { + fmt.Printf("Could not register subloggers. Error: %v.\n", err) + os.Exit(1) + } + + if strategyPluginPath != "" { + err = strategies.LoadCustomStrategies(strategyPluginPath) + if err != nil { + fmt.Printf("Could not load custom strategies. Error: %v.\n", err) + os.Exit(1) + } + } + + cfg, err = config.ReadConfigFromFile(configPath) + if err != nil { + fmt.Printf("Could not read config. Error: %v.\n", err) + os.Exit(1) + } + if printLogo { + fmt.Println(common.Logo()) + } + + err = cfg.Validate() + if err != nil { + fmt.Printf("Could not read config. Error: %v.\n", err) + os.Exit(1) + } + + bt, err = backtest.NewFromConfig(cfg, templatePath, reportOutput, verbose) + if err != nil { + fmt.Printf("Could not setup backtester from config. Error: %v.\n", err) + os.Exit(1) + } + if cfg.DataSettings.LiveData != nil { + go func() { + err = bt.RunLive() + if err != nil { + fmt.Printf("Could not complete live run. Error: %v.\n", err) + os.Exit(-1) + } + }() + interrupt := signaler.WaitForInterrupt() + log.Infof(log.Global, "Captured %v, shutdown requested.\n", interrupt) + bt.Stop() + } else { + bt.Run() + } + + err = bt.Statistic.CalculateAllResults() + if err != nil { + log.Error(log.Global, err) + os.Exit(1) + } + + if generateReport { + bt.Reports.UseDarkMode(darkReport) + err = bt.Reports.GenerateReport() + if err != nil { + log.Error(log.Global, err) + } + } +} + +func parseFlags(wd string) { flag.StringVar( &configPath, "configpath", @@ -76,75 +161,10 @@ func main() { "logsubheader", true, "displays logging subheader to track where activity originates") + flag.StringVar( + &strategyPluginPath, + "strategypluginpath", + "", + "example path: "+filepath.Join(wd, "plugins", "strategies", "example", "example.so")) flag.Parse() - if !colourOutput { - common.PurgeColours() - } - var bt *backtest.BackTest - var cfg *config.Config - log.GlobalLogConfig = log.GenDefaultSettings() - log.GlobalLogConfig.AdvancedSettings.ShowLogSystemName = convert.BoolPtr(logSubHeader) - log.GlobalLogConfig.AdvancedSettings.Headers.Info = common.ColourInfo + "[INFO]" + common.ColourDefault - log.GlobalLogConfig.AdvancedSettings.Headers.Warn = common.ColourWarn + "[WARN]" + common.ColourDefault - log.GlobalLogConfig.AdvancedSettings.Headers.Debug = common.ColourDebug + "[DEBUG]" + common.ColourDefault - log.GlobalLogConfig.AdvancedSettings.Headers.Error = common.ColourError + "[ERROR]" + common.ColourDefault - err = log.SetupGlobalLogger() - if err != nil { - fmt.Printf("Could not setup global logger. Error: %v.\n", err) - os.Exit(1) - } - - err = common.RegisterBacktesterSubLoggers() - if err != nil { - fmt.Printf("Could not register subloggers. Error: %v.\n", err) - os.Exit(1) - } - - cfg, err = config.ReadConfigFromFile(configPath) - if err != nil { - fmt.Printf("Could not read config. Error: %v.\n", err) - os.Exit(1) - } - if printLogo { - fmt.Println(common.Logo()) - } - - err = cfg.Validate() - if err != nil { - fmt.Printf("Could not read config. Error: %v.\n", err) - os.Exit(1) - } - bt, err = backtest.NewFromConfig(cfg, templatePath, reportOutput, verbose) - if err != nil { - fmt.Printf("Could not setup backtester from config. Error: %v.\n", err) - os.Exit(1) - } - if cfg.DataSettings.LiveData != nil { - go func() { - err = bt.RunLive() - if err != nil { - fmt.Printf("Could not complete live run. Error: %v.\n", err) - os.Exit(-1) - } - }() - interrupt := signaler.WaitForInterrupt() - log.Infof(log.Global, "Captured %v, shutdown requested.\n", interrupt) - bt.Stop() - } else { - bt.Run() - } - - err = bt.Statistic.CalculateAllResults() - if err != nil { - log.Error(log.Global, err) - os.Exit(1) - } - - if generateReport { - bt.Reports.UseDarkMode(darkReport) - err = bt.Reports.GenerateReport() - if err != nil { - log.Error(log.Global, err) - } - } } diff --git a/backtester/plugins/README.md b/backtester/plugins/README.md new file mode 100644 index 00000000..17c0e81c --- /dev/null +++ b/backtester/plugins/README.md @@ -0,0 +1,70 @@ +# GoCryptoTrader Backtester: Plugins package + + + + +[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/plugins) +[![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 plugins package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Plugins package overview + +Golang Plugins are supported by the GoCryptoTrader Backtester. At present, only custom strategies are supported. + +Please read the Golang documentation on [plugins](https://golang.org/pkg/plugin/) for more information. + +## Building Golang Plugins + +### Windows +Plugin support is not yet available for Windows. However, you can still build via WSL. See below for instructions on a basic setup for WSL. Once completed, follow the instructions for Linux. +#### WSL Setup +The following is a basic setup for WSL: [here](https://pureinfotech.com/install-wsl-windows-11/) + +### Linux, macOS & WSL +A plugin is a Go main package with exported functions and variables that has been built with: + +```bash +go build -buildmode=plugin +``` + +This outputs a file named `plugins.so` which can be loaded by the backtester. At present, only custom strategies can be loaded. See [here](/strategies/example/README.md) for more information on building custom strategies via plugins. + +You must ensure that the plugin is built with the same version of code as the GoCryptoTrader Backtester. Otherwise the plugin will refuse to load. + + + +#### Installing Golang in WSL +See the following for instructions on installing Golang in WSL: [here](https://ao.ms/how-to-install-golang-on-wsl-wsl2/) + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/backtester/plugins/strategies/README.md b/backtester/plugins/strategies/README.md new file mode 100644 index 00000000..8f2b5892 --- /dev/null +++ b/backtester/plugins/strategies/README.md @@ -0,0 +1,64 @@ +# GoCryptoTrader Backtester: Strategies package + + + + +[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/plugins/strategies) +[![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 strategies package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Strategies package overview + +### Designing a strategy +- File must contain `main` package +- Custom strategy plugins must adhere to the strategy.Handler interface. See the [strategy.Handler interface documentation](./backtester/eventhandlers/strategies/README.md) for more information. +- Must contain function `func GetStrategies() []strategy.Handler` to return a slice of implemented `strategy.Handler`. + - If only using one custom strategy, can simply `return []strategy.Handler{&customStrategy{}}`. + + +### Building +See [here](./backtester/plugins/README.md) for details on how to build the plugin file. + +### Running +Plugins can only be loaded via Linux, macOS and WSL. Windows itself is not supported. + +To run a strategy you will need to use the following flags when running the GoCryptoTrader Backtester: + +```bash +./backtester -strategypluginpath="path/to/strategy/example.so" +``` + +Upon startup, the GoCryptoTrader Backtester will load the strategy and run it for all events. + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/backtester/plugins/strategies/example/README.md b/backtester/plugins/strategies/example/README.md new file mode 100644 index 00000000..5ae90d2c --- /dev/null +++ b/backtester/plugins/strategies/example/README.md @@ -0,0 +1,71 @@ +# GoCryptoTrader Backtester: Example package + + + + +[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/plugins/strategies/example) +[![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 example package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) + +## Example package overview + +This is a custom strategy for the GoCryptoTrader Backtester. It is a simple example of a strategy that trades a pair of assets and is used to highlight how strategies can be loaded from external sources. + +### Designing a strategy +- File must contain `main` package. +- Custom strategy plugins must adhere to the strategy.Handler interface. See the [strategy.Handler interface documentation](./backtester/eventhandlers/strategies/README.md) for more information. +- Must contain function `func GetStrategies() []strategy.Handler` to return a slice of implemented `strategy.Handler`. + - If only using one custom strategy, can simply `return []strategy.Handler{&customStrategy{}}`. + +### Building +See [here](./backtester/plugins/README.md) for details on how to build the plugin file. + +### Running +Plugins can only be loaded via Linux, macOS and WSL. Windows itself is not supported. + +To run this strategy you will need to use the following flags when running the GoCryptoTrader Backtester: + +```bash +./backtester -strategypluginpath="path/to/strategy/example.so" +``` + +To run this specific example strategy, use: + +```bash +./backtester --strategypluginpath="./plugins/strategies/example/example.so" +``` + +Upon startup, the GoCryptoTrader Backtester will load the strategy and run it for all events. + + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/backtester/plugins/strategies/example/example-strategy.go b/backtester/plugins/strategies/example/example-strategy.go new file mode 100644 index 00000000..f5ad6b8f --- /dev/null +++ b/backtester/plugins/strategies/example/example-strategy.go @@ -0,0 +1,89 @@ +package main + +import ( + "github.com/thrasher-corp/gocryptotrader/backtester/data" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base" + "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal" + "github.com/thrasher-corp/gocryptotrader/backtester/funding" + gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +func main() { + // required for plugin system +} + +// CustomStrategy the type used to define custom strategy functions +type CustomStrategy struct { + base.Strategy +} + +// GetStrategies is required to load the strategy or strategies into the GoCryptoTrader Backtester +func GetStrategies() []strategies.Handler { + return []strategies.Handler{&CustomStrategy{}} +} + +// Name returns the name of the strategy +func (s *CustomStrategy) Name() string { + return "custom-strategy" +} + +// Description describes the strategy +func (s *CustomStrategy) Description() string { + return "this is a demonstration of loading strategies via custom plugins" +} + +// SupportsSimultaneousProcessing this strategy only supports simultaneous signal processing +func (s *CustomStrategy) SupportsSimultaneousProcessing() bool { + return true +} + +// OnSignal handles a data event and returns what action the strategy believes should occur +func (s *CustomStrategy) OnSignal(d data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) (signal.Event, error) { + return s.createSignal(d) +} + +// OnSimultaneousSignals analyses multiple data points simultaneously, allowing flexibility +// in allowing a strategy to only place an order for X currency if Y currency's price is Z +func (s *CustomStrategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTransferer, p portfolio.Handler) ([]signal.Event, error) { + response := make([]signal.Event, len(d)) + for i := range d { + sig, err := s.createSignal(d[i]) + if err != nil { + return nil, err + } + response[i] = sig + } + return response, nil +} + +func (s *CustomStrategy) createSignal(d data.Handler) (*signal.Signal, error) { + es, err := s.GetBaseData(d) + if err != nil { + return nil, err + } + + sig := &signal.Signal{ + Base: es.Base, + OpenPrice: es.OpenPrice, + HighPrice: es.HighPrice, + LowPrice: es.LowPrice, + ClosePrice: es.ClosePrice, + Volume: es.Volume, + BuyLimit: es.BuyLimit, + SellLimit: es.SellLimit, + Amount: es.Amount, + Direction: gctorder.Buy, + } + sig.AppendReasonf("Signalling purchase of %s", es.Base.Pair()) + return sig, nil +} + +// SetCustomSettings can override default settings +func (s *CustomStrategy) SetCustomSettings(map[string]interface{}) error { + return base.ErrCustomSettingsUnsupported +} + +// SetDefaults sets default values for overridable custom settings +func (s *CustomStrategy) SetDefaults() {} diff --git a/backtester/plugins/strategies/loader.go b/backtester/plugins/strategies/loader.go new file mode 100644 index 00000000..2696342f --- /dev/null +++ b/backtester/plugins/strategies/loader.go @@ -0,0 +1,44 @@ +package strategies + +import ( + "errors" + "fmt" + "plugin" + + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies" + gctcommon "github.com/thrasher-corp/gocryptotrader/common" +) + +var errNoStrategies = errors.New("no strategies contained in plugin. please refer to docs") + +// LoadCustomStrategies utilises Go's plugin system to load +// custom strategies into the backtester. +func LoadCustomStrategies(strategyPluginPath string) error { + p, err := plugin.Open(strategyPluginPath) + if err != nil { + return fmt.Errorf("could not open plugin: %w", err) + } + v, err := p.Lookup("GetStrategies") + if err != nil { + return fmt.Errorf("could not lookup plugin. Plugin must have function `GetStrategy`. Error: %w", err) + } + customStrategies, ok := v.(func() []strategies.Handler) + if !ok { + return gctcommon.GetAssertError("[]strategies.Handler", customStrategies) + } + return addStrategies(customStrategies()) +} + +func addStrategies(s []strategies.Handler) error { + if len(s) == 0 { + return errNoStrategies + } + var err error + for i := range s { + err = strategies.AddStrategy(s[i]) + if err != nil { + return err + } + } + return nil +} diff --git a/backtester/plugins/strategies/loader_test.go b/backtester/plugins/strategies/loader_test.go new file mode 100644 index 00000000..129dd96f --- /dev/null +++ b/backtester/plugins/strategies/loader_test.go @@ -0,0 +1,65 @@ +package strategies + +import ( + "errors" + "testing" + + "github.com/thrasher-corp/gocryptotrader/backtester/data" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base" + "github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/dollarcostaverage" + "github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal" + "github.com/thrasher-corp/gocryptotrader/backtester/funding" +) + +func TestAddStrategies(t *testing.T) { + t.Parallel() + err := addStrategies(nil) + if !errors.Is(err, errNoStrategies) { + t.Error(err) + } + + err = addStrategies([]strategies.Handler{&dollarcostaverage.Strategy{}}) + if !errors.Is(err, strategies.ErrStrategyAlreadyExists) { + t.Error(err) + } + + err = addStrategies([]strategies.Handler{&CustomStrategy{}}) + if !errors.Is(err, nil) { + t.Error(err) + } +} + +type CustomStrategy struct { + base.Strategy +} + +func (s *CustomStrategy) Name() string { + return "custom-strategy" +} + +func (s *CustomStrategy) Description() string { + return "this is a demonstration of loading strategies via custom plugins" +} + +func (s *CustomStrategy) SupportsSimultaneousProcessing() bool { + return true +} + +func (s *CustomStrategy) OnSignal(d data.Handler, _ funding.IFundingTransferer, _ portfolio.Handler) (signal.Event, error) { + return s.createSignal(d) +} +func (s *CustomStrategy) OnSimultaneousSignals(d []data.Handler, f funding.IFundingTransferer, p portfolio.Handler) ([]signal.Event, error) { + return nil, nil +} + +func (s *CustomStrategy) createSignal(d data.Handler) (*signal.Signal, error) { + return nil, nil +} + +func (s *CustomStrategy) SetCustomSettings(map[string]interface{}) error { + return nil +} + +func (s *CustomStrategy) SetDefaults() {} diff --git a/cmd/documentation/backtester_templates/backtester_plugins_readme.tmpl b/cmd/documentation/backtester_templates/backtester_plugins_readme.tmpl new file mode 100644 index 00000000..1ac4c758 --- /dev/null +++ b/cmd/documentation/backtester_templates/backtester_plugins_readme.tmpl @@ -0,0 +1,36 @@ +{{define "backtester plugins" -}} +{{template "backtester-header" .}} +## {{.CapitalName}} package overview + +Golang Plugins are supported by the GoCryptoTrader Backtester. At present, only custom strategies are supported. + +Please read the Golang documentation on [plugins](https://golang.org/pkg/plugin/) for more information. + +## Building Golang Plugins + +### Windows +Plugin support is not yet available for Windows. However, you can still build via WSL. See below for instructions on a basic setup for WSL. Once completed, follow the instructions for Linux. +#### WSL Setup +The following is a basic setup for WSL: [here](https://pureinfotech.com/install-wsl-windows-11/) + +### Linux, macOS & WSL +A plugin is a Go main package with exported functions and variables that has been built with: + +```bash +go build -buildmode=plugin +``` + +This outputs a file named `{{.Name}}.so` which can be loaded by the backtester. At present, only custom strategies can be loaded. See [here](/strategies/example/README.md) for more information on building custom strategies via plugins. + +You must ensure that the plugin is built with the same version of code as the GoCryptoTrader Backtester. Otherwise the plugin will refuse to load. + + + +#### Installing Golang in WSL +See the following for instructions on installing Golang in WSL: [here](https://ao.ms/how-to-install-golang-on-wsl-wsl2/) + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} \ No newline at end of file diff --git a/cmd/documentation/backtester_templates/backtester_plugins_strategies_example_readme.tmpl b/cmd/documentation/backtester_templates/backtester_plugins_strategies_example_readme.tmpl new file mode 100644 index 00000000..39c1138f --- /dev/null +++ b/cmd/documentation/backtester_templates/backtester_plugins_strategies_example_readme.tmpl @@ -0,0 +1,37 @@ +{{define "backtester plugins strategies example" -}} +{{template "backtester-header" .}} +## {{.CapitalName}} package overview + +This is a custom strategy for the GoCryptoTrader Backtester. It is a simple example of a strategy that trades a pair of assets and is used to highlight how strategies can be loaded from external sources. + +### Designing a strategy +- File must contain `main` package. +- Custom strategy plugins must adhere to the strategy.Handler interface. See the [strategy.Handler interface documentation](./backtester/eventhandlers/strategies/README.md) for more information. +- Must contain function `func GetStrategies() []strategy.Handler` to return a slice of implemented `strategy.Handler`. + - If only using one custom strategy, can simply `return []strategy.Handler{&customStrategy{}}`. + +### Building +See [here](./backtester/plugins/README.md) for details on how to build the plugin file. + +### Running +Plugins can only be loaded via Linux, macOS and WSL. Windows itself is not supported. + +To run this strategy you will need to use the following flags when running the GoCryptoTrader Backtester: + +```bash +./backtester -strategypluginpath="path/to/strategy/example.so" +``` + +To run this specific example strategy, use: + +```bash +./backtester --strategypluginpath="./plugins/strategies/example/example.so" +``` + +Upon startup, the GoCryptoTrader Backtester will load the strategy and run it for all events. + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} \ No newline at end of file diff --git a/cmd/documentation/backtester_templates/backtester_plugins_strategies_readme.tmpl b/cmd/documentation/backtester_templates/backtester_plugins_strategies_readme.tmpl new file mode 100644 index 00000000..9ec62634 --- /dev/null +++ b/cmd/documentation/backtester_templates/backtester_plugins_strategies_readme.tmpl @@ -0,0 +1,30 @@ +{{define "backtester plugins strategies" -}} +{{template "backtester-header" .}} +## {{.CapitalName}} package overview + +### Designing a strategy +- File must contain `main` package +- Custom strategy plugins must adhere to the strategy.Handler interface. See the [strategy.Handler interface documentation](./backtester/eventhandlers/strategies/README.md) for more information. +- Must contain function `func GetStrategies() []strategy.Handler` to return a slice of implemented `strategy.Handler`. + - If only using one custom strategy, can simply `return []strategy.Handler{&customStrategy{}}`. + + +### Building +See [here](./backtester/plugins/README.md) for details on how to build the plugin file. + +### Running +Plugins can only be loaded via Linux, macOS and WSL. Windows itself is not supported. + +To run a strategy you will need to use the following flags when running the GoCryptoTrader Backtester: + +```bash +./backtester -strategypluginpath="path/to/strategy/example.so" +``` + +Upon startup, the GoCryptoTrader Backtester will load the strategy and run it for all events. + + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations" .}} +{{end}} \ No newline at end of file