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?
This commit is contained in:
Scott
2022-08-23 14:22:06 +10:00
committed by GitHub
parent ae02f168a9
commit 10f7ff3236
17 changed files with 829 additions and 86 deletions

View File

@@ -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 |

View File

@@ -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()

View File

@@ -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"
}
}

View File

@@ -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),
}
}
)

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -0,0 +1,70 @@
# GoCryptoTrader Backtester: Plugins 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/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
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,64 @@
# GoCryptoTrader Backtester: Strategies 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/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
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,71 @@
# GoCryptoTrader Backtester: Example 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/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
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -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() {}

View File

@@ -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
}

View File

@@ -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() {}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}