backtester: standalone application (#988)

* Ramshackle early leads to GRPC backtester

* Adds GRPC server, default config generation

* Partial support for GRPC backtester config

* Update to use Buf, merge fixes

* Full config for GRPC

* Adds new commands, causes big panic

* Fixes panics

* Setup for the future

* Docs update

* test

* grpc tests

* Fix merge issues. Lint and test

* minor fixes after rebase

* Docs, formatting and main fixes

* Change buf owner

* shazNits

* test-123

* rpc fixes

* string fixes

* Removes --singlerun flag and just relies on --singlerunstrategypath

* fixes test

* initial post merge compatability fixes

* this actually all seems to work? unexpected

* adds pluginpath to config

* rm unused func. add gitignore

* rm unused func. add gitignore

* lintle

* tITLE cASE lOG fIX,rm auth package, gitignore, tmpdir fix

* buf updates + gen. go mod tidy

* x2

* Update default port, update error text
This commit is contained in:
Scott
2022-09-08 16:22:30 +10:00
committed by GitHub
parent 8a68a1d682
commit 1461cba363
76 changed files with 7932 additions and 2193 deletions

View File

@@ -35,3 +35,14 @@ jobs:
- name: buf format
run: buf format --diff --exit-code
- name: buf generate backtester
working-directory: ./backtester/btrpc
run: buf generate
- uses: bufbuild/buf-lint-action@v1
with:
input: ./backtester/btrpc
- name: buf format backtester
run: buf format --diff --exit-code

2
.gitignore vendored
View File

@@ -16,6 +16,8 @@ vendor/
# Binaries for programs and plugins
gocryptotrader
cmd/gctcli/gctcli
backtester/backtester
backtester/btcli/btcli
*.exe
*.exe~
*.dll

View File

@@ -45,13 +45,14 @@ 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
## 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 |
|---------|-------------|
| Long-running application | Transform the Backtester to run a GRPC server, where commands can be sent to run Backtesting operations. Allowing for many strategies to be run, analysed and tweaked in a more efficient manner |
| Leverage support | Leverage is a good way to enhance profit and loss and is important to include in strategies |
| 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 |

View File

@@ -0,0 +1,51 @@
# GoCryptoTrader Backtester: Btcli 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/btcli)
[![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 btcli 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)
## Btcli overview
This folder contains the GoCryptoTrader Backtester CMD CLI application. It can be used to interact
with the GoCryptoTrader Backtester's GRPC server and send commands to be processed server-side.
For a list of commands, you can run the following
```
go run .
```
### 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,252 @@
package main
import (
"fmt"
"path/filepath"
"github.com/thrasher-corp/gocryptotrader/backtester/btrpc"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"github.com/urfave/cli/v2"
"google.golang.org/protobuf/types/known/timestamppb"
)
var executeStrategyFromFileCommand = &cli.Command{
Name: "executestrategyfromfile",
Usage: "runs the strategy from a config file",
ArgsUsage: "<path>",
Action: executeStrategyFromFile,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "path",
Aliases: []string{"p"},
Usage: "the filepath to a strategy to execute",
},
},
}
func executeStrategyFromFile(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
if c.NArg() == 0 && c.NumFlags() == 0 {
return cli.ShowCommandHelp(c, "executestrategyfromfile")
}
var path string
if c.IsSet("path") {
path = c.String("path")
} else {
path = c.Args().First()
}
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.ExecuteStrategyFromFile(
c.Context,
&btrpc.ExecuteStrategyFromFileRequest{
StrategyFilePath: path,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}
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",
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,
}
// executeStrategyFromConfig this is a proof of concept command
// it demonstrates that a user can send complex strategies via GRPC
// and have them execute. The ultimate goal is to allow a user to continuously
// tweak values and send them via GRPC and determine the best returns then test them across
// large ranges of data to avoid over fitting
func executeStrategyFromConfig(c *cli.Context) error {
conn, cancel, err := setupClient(c)
if err != nil {
return err
}
defer closeConn(conn, cancel)
defaultPath := filepath.Join(
"..",
"config",
"strategyexamples",
"ftx-cash-carry.strat")
defaultConfig, err := config.ReadStrategyConfigFromFile(defaultPath)
if err != nil {
return err
}
customSettings := make([]*btrpc.CustomSettings, len(defaultConfig.StrategySettings.CustomSettings))
x := 0
for k, v := range defaultConfig.StrategySettings.CustomSettings {
customSettings[x] = &btrpc.CustomSettings{
KeyField: k,
KeyValue: fmt.Sprintf("%v", v),
}
x++
}
currencySettings := make([]*btrpc.CurrencySettings, len(defaultConfig.CurrencySettings))
for i := range defaultConfig.CurrencySettings {
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()
}
var fd *btrpc.FuturesDetails
if defaultConfig.CurrencySettings[i].FuturesDetails != nil {
fd.Leverage = &btrpc.Leverage{
CanUseLeverage: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.CanUseLeverage,
MaximumOrdersWithLeverageRatio: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio.String(),
MaximumLeverageRate: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate.String(),
MaximumCollateralLeverageRate: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.MaximumCollateralLeverageRate.String(),
}
}
currencySettings[i] = &btrpc.CurrencySettings{
ExchangeName: defaultConfig.CurrencySettings[i].ExchangeName,
Asset: defaultConfig.CurrencySettings[i].Asset.String(),
Base: defaultConfig.CurrencySettings[i].Base.String(),
Quote: defaultConfig.CurrencySettings[i].Quote.String(),
BuySide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.CurrencySettings[i].BuySide.MinimumSize.String(),
MaximumSize: defaultConfig.CurrencySettings[i].BuySide.MaximumSize.String(),
MaximumTotal: defaultConfig.CurrencySettings[i].BuySide.MaximumTotal.String(),
},
SellSide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.CurrencySettings[i].SellSide.MinimumSize.String(),
MaximumSize: defaultConfig.CurrencySettings[i].SellSide.MaximumSize.String(),
MaximumTotal: defaultConfig.CurrencySettings[i].SellSide.MaximumTotal.String(),
},
MinSlippagePercent: defaultConfig.CurrencySettings[i].MinimumSlippagePercent.String(),
MaxSlippagePercent: defaultConfig.CurrencySettings[i].MaximumSlippagePercent.String(),
MakerFeeOverride: defaultConfig.CurrencySettings[i].MakerFee.String(),
TakerFeeOverride: defaultConfig.CurrencySettings[i].TakerFee.String(),
MaximumHoldingsRatio: defaultConfig.CurrencySettings[i].MaximumHoldingsRatio.String(),
SkipCandleVolumeFitting: defaultConfig.CurrencySettings[i].SkipCandleVolumeFitting,
UseExchangeOrderLimits: defaultConfig.CurrencySettings[i].CanUseExchangeLimits,
UseExchangePnlCalculation: defaultConfig.CurrencySettings[i].UseExchangePNLCalculation,
SpotDetails: sd,
FuturesDetails: fd,
}
}
exchangeLevelFunding := make([]*btrpc.ExchangeLevelFunding, len(defaultConfig.FundingSettings.ExchangeLevelFunding))
for i := range defaultConfig.FundingSettings.ExchangeLevelFunding {
exchangeLevelFunding[i] = &btrpc.ExchangeLevelFunding{
ExchangeName: defaultConfig.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
Asset: defaultConfig.FundingSettings.ExchangeLevelFunding[i].Asset.String(),
Currency: defaultConfig.FundingSettings.ExchangeLevelFunding[i].Currency.String(),
InitialFunds: defaultConfig.FundingSettings.ExchangeLevelFunding[i].InitialFunds.String(),
TransferFee: defaultConfig.FundingSettings.ExchangeLevelFunding[i].TransferFee.String(),
}
}
dataSettings := &btrpc.DataSettings{
Interval: uint64(defaultConfig.DataSettings.Interval.Duration().Nanoseconds()),
Datatype: defaultConfig.DataSettings.DataType,
}
if defaultConfig.DataSettings.APIData != nil {
dataSettings.ApiData = &btrpc.ApiData{
StartDate: timestamppb.New(defaultConfig.DataSettings.APIData.StartDate),
EndDate: timestamppb.New(defaultConfig.DataSettings.APIData.EndDate),
InclusiveEndDate: defaultConfig.DataSettings.APIData.InclusiveEndDate,
}
}
if defaultConfig.DataSettings.LiveData != nil {
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,
}
}
if defaultConfig.DataSettings.CSVData != nil {
dataSettings.CsvData = &btrpc.CSVData{
Path: defaultConfig.DataSettings.CSVData.FullPath,
}
}
if defaultConfig.DataSettings.DatabaseData != nil {
dbConnectionDetails := &btrpc.DatabaseConnectionDetails{
Host: defaultConfig.DataSettings.DatabaseData.Config.Host,
Port: uint32(defaultConfig.DataSettings.DatabaseData.Config.Port),
Password: defaultConfig.DataSettings.DatabaseData.Config.Password,
Database: defaultConfig.DataSettings.DatabaseData.Config.Database,
SslMode: defaultConfig.DataSettings.DatabaseData.Config.SSLMode,
UserName: defaultConfig.DataSettings.DatabaseData.Config.Username,
}
dbConfig := &btrpc.DatabaseConfig{
Config: dbConnectionDetails,
}
dataSettings.DatabaseData = &btrpc.DatabaseData{
StartDate: timestamppb.New(defaultConfig.DataSettings.DatabaseData.StartDate),
EndDate: timestamppb.New(defaultConfig.DataSettings.DatabaseData.EndDate),
Config: dbConfig,
Path: defaultConfig.DataSettings.DatabaseData.Path,
InclusiveEndDate: defaultConfig.DataSettings.DatabaseData.InclusiveEndDate,
}
}
cfg := &btrpc.Config{
Nickname: defaultConfig.Nickname,
Goal: defaultConfig.Goal,
StrategySettings: &btrpc.StrategySettings{
Name: defaultConfig.StrategySettings.Name,
UseSimultaneousSignalProcessing: defaultConfig.StrategySettings.SimultaneousSignalProcessing,
DisableUsdTracking: defaultConfig.StrategySettings.DisableUSDTracking,
CustomSettings: customSettings,
},
FundingSettings: &btrpc.FundingSettings{
UseExchangeLevelFunding: defaultConfig.FundingSettings.UseExchangeLevelFunding,
ExchangeLevelFunding: exchangeLevelFunding,
},
CurrencySettings: currencySettings,
DataSettings: dataSettings,
PortfolioSettings: &btrpc.PortfolioSettings{
Leverage: &btrpc.Leverage{
CanUseLeverage: defaultConfig.PortfolioSettings.Leverage.CanUseLeverage,
MaximumOrdersWithLeverageRatio: defaultConfig.PortfolioSettings.Leverage.MaximumOrdersWithLeverageRatio.String(),
MaximumLeverageRate: defaultConfig.PortfolioSettings.Leverage.MaximumOrderLeverageRate.String(),
MaximumCollateralLeverageRate: defaultConfig.PortfolioSettings.Leverage.MaximumCollateralLeverageRate.String(),
},
BuySide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.PortfolioSettings.BuySide.MinimumSize.String(),
MaximumSize: defaultConfig.PortfolioSettings.BuySide.MaximumSize.String(),
MaximumTotal: defaultConfig.PortfolioSettings.BuySide.MaximumTotal.String(),
},
SellSide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.PortfolioSettings.SellSide.MinimumSize.String(),
MaximumSize: defaultConfig.PortfolioSettings.SellSide.MaximumSize.String(),
MaximumTotal: defaultConfig.PortfolioSettings.SellSide.MaximumTotal.String(),
},
},
StatisticSettings: &btrpc.StatisticSettings{
RiskFreeRate: defaultConfig.StatisticSettings.RiskFreeRate.String(),
},
}
client := btrpc.NewBacktesterServiceClient(conn)
result, err := client.ExecuteStrategyFromConfig(
c.Context,
&btrpc.ExecuteStrategyFromConfigRequest{
Config: cfg,
},
)
if err != nil {
return err
}
jsonOutput(result)
return nil
}

View File

@@ -0,0 +1,17 @@
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
)
func closeConn(conn *grpc.ClientConn, cancel context.CancelFunc) {
if err := conn.Close(); err != nil {
fmt.Println(err)
}
if cancel != nil {
cancel()
}
}

130
backtester/btcli/main.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/gctrpc/auth"
"github.com/thrasher-corp/gocryptotrader/signaler"
"github.com/urfave/cli/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
defaultUsername = "rpcuser"
defaultPassword = "helloImTheDefaultPassword"
)
var (
host string
username string
password string
pairDelimiter string
certPath string
timeout time.Duration
)
const defaultTimeout = time.Second * 30
func jsonOutput(in interface{}) {
j, err := json.MarshalIndent(in, "", " ")
if err != nil {
return
}
fmt.Print(string(j))
}
func setupClient(c *cli.Context) (*grpc.ClientConn, context.CancelFunc, error) {
creds, err := credentials.NewClientTLSFromFile(certPath, "")
if err != nil {
return nil, nil, err
}
opts := []grpc.DialOption{grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(auth.BasicAuth{
Username: username,
Password: password,
}),
}
var cancel context.CancelFunc
c.Context, cancel = context.WithTimeout(c.Context, timeout)
conn, err := grpc.DialContext(c.Context, host, opts...)
return conn, cancel, err
}
func main() {
version := core.Version(true)
version = strings.Replace(version, "GoCryptoTrader", "GoCryptoTrader Backtester", 1)
app := cli.NewApp()
app.Name = "btcli"
app.Version = version
app.EnableBashCompletion = true
app.Usage = "command line interface for managing the backtester daemon"
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "rpchost",
Value: "localhost:9054",
Usage: "the gRPC host to connect to",
Destination: &host,
},
&cli.StringFlag{
Name: "rpcuser",
Value: defaultUsername,
Usage: "the gRPC username",
Destination: &username,
},
&cli.StringFlag{
Name: "rpcpassword",
Value: defaultPassword,
Usage: "the gRPC password",
Destination: &password,
},
&cli.StringFlag{
Name: "delimiter",
Value: "-",
Usage: "the default currency pair delimiter used to standardise currency pair input",
Destination: &pairDelimiter,
},
&cli.StringFlag{
Name: "cert",
Value: filepath.Join(common.GetDefaultDataDir(runtime.GOOS), "backtester", "tls", "cert.pem"),
Usage: "the path to TLS cert of the gRPC server",
Destination: &certPath,
},
&cli.DurationFlag{
Name: "timeout",
Value: defaultTimeout,
Usage: "the default context timeout value for requests",
Destination: &timeout,
},
}
app.Commands = []*cli.Command{
executeStrategyFromFileCommand,
executeStrategyFromConfigCommand,
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
// Capture cancel for interrupt
signaler.WaitForInterrupt()
cancel()
fmt.Println("rpc process interrupted")
os.Exit(1)
}()
err := app.RunContext(ctx, os.Args)
if err != nil {
log.Fatal(err)
}
}

View File

@@ -0,0 +1,95 @@
# GoCryptoTrader Backtester: Btrpc 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/btrpc)
[![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 btrpc 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)
## Btrpc overview
The GoCryptoTrader Backtester utilises gRPC for client/server interaction. Authentication is done
by a self signed TLS cert, which only supports connections from localhost and also
through basic authorisation specified by the users config file.
The GoCryptoTrader Backtester also supports a gRPC JSON proxy service for applications which can
be toggled on or off depending on the users preference. This can be found in your config file
under `grpcProxyEnabled` `grpcProxyListenAddress`. See `btrpc.swagger.json` for endpoint definitions
## Installation
The GoCryptoTrader Backtester requires a local installation of the Google protocol buffers
compiler `protoc` v3.0.0 or above. Please install this via your local package
manager or by downloading one of the releases from the official repository:
[protoc releases](https://github.com/protocolbuffers/protobuf/releases)
Then use `go install` to download the following packages:
```bash
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
```
This will place the following binaries in your `$GOBIN`;
* `protoc-gen-grpc-gateway`
* `protoc-gen-openapiv2`
* `protoc-gen-go`
* `protoc-gen-go-grpc`
Make sure that your `$GOBIN` is in your `$PATH`.
### Linux / macOS / Windows
The GoCryptoTrader Backtester requires a local installation of the `buf` cli tool that tries to make Protobuf handling more easier and reliable,
after [installation](https://docs.buf.build/installation) you'll need to run:
```shell
buf mod update
```
After previous command, make necessary changes to the `rpc.proto` spec file and run the generation command:
```shell
buf generate
```
If any changes were made, ensure that the `rpc.proto` file is formatted correctly by using `buf format -w`
### 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***

2548
backtester/btrpc/btrpc.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,256 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: btrpc.proto
/*
Package btrpc is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package btrpc
import (
"context"
"io"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = metadata.Join
var (
filter_BacktesterService_ExecuteStrategyFromFile_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_BacktesterService_ExecuteStrategyFromFile_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ExecuteStrategyFromFileRequest
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_ExecuteStrategyFromFile_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.ExecuteStrategyFromFile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_ExecuteStrategyFromFile_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ExecuteStrategyFromFileRequest
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_ExecuteStrategyFromFile_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.ExecuteStrategyFromFile(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_BacktesterService_ExecuteStrategyFromConfig_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_BacktesterService_ExecuteStrategyFromConfig_0(ctx context.Context, marshaler runtime.Marshaler, client BacktesterServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ExecuteStrategyFromConfigRequest
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_ExecuteStrategyFromConfig_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.ExecuteStrategyFromConfig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_BacktesterService_ExecuteStrategyFromConfig_0(ctx context.Context, marshaler runtime.Marshaler, server BacktesterServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ExecuteStrategyFromConfigRequest
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_ExecuteStrategyFromConfig_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.ExecuteStrategyFromConfig(ctx, &protoReq)
return msg, metadata, err
}
// RegisterBacktesterServiceHandlerServer registers the http handlers for service BacktesterService to "mux".
// UnaryRPC :call BacktesterServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterBacktesterServiceHandlerFromEndpoint instead.
func RegisterBacktesterServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server BacktesterServiceServer) error {
mux.Handle("GET", pattern_BacktesterService_ExecuteStrategyFromFile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ExecuteStrategyFromFile", runtime.WithHTTPPathPattern("/v1/executestrategyfromfile"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_ExecuteStrategyFromFile_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_ExecuteStrategyFromFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_BacktesterService_ExecuteStrategyFromConfig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/btrpc.BacktesterService/ExecuteStrategyFromConfig", runtime.WithHTTPPathPattern("/v1/executestrategyfromconfig"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_BacktesterService_ExecuteStrategyFromConfig_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_ExecuteStrategyFromConfig_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterBacktesterServiceHandlerFromEndpoint is same as RegisterBacktesterServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterBacktesterServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterBacktesterServiceHandler(ctx, mux, conn)
}
// RegisterBacktesterServiceHandler registers the http handlers for service BacktesterService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterBacktesterServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterBacktesterServiceHandlerClient(ctx, mux, NewBacktesterServiceClient(conn))
}
// RegisterBacktesterServiceHandlerClient registers the http handlers for service BacktesterService
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "BacktesterServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "BacktesterServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "BacktesterServiceClient" to call the correct interceptors.
func RegisterBacktesterServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client BacktesterServiceClient) error {
mux.Handle("GET", pattern_BacktesterService_ExecuteStrategyFromFile_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
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ExecuteStrategyFromFile", runtime.WithHTTPPathPattern("/v1/executestrategyfromfile"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_ExecuteStrategyFromFile_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_ExecuteStrategyFromFile_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_BacktesterService_ExecuteStrategyFromConfig_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
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/btrpc.BacktesterService/ExecuteStrategyFromConfig", runtime.WithHTTPPathPattern("/v1/executestrategyfromconfig"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_BacktesterService_ExecuteStrategyFromConfig_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_BacktesterService_ExecuteStrategyFromConfig_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_BacktesterService_ExecuteStrategyFromFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "executestrategyfromfile"}, ""))
pattern_BacktesterService_ExecuteStrategyFromConfig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "executestrategyfromconfig"}, ""))
)
var (
forward_BacktesterService_ExecuteStrategyFromFile_0 = runtime.ForwardResponseMessage
forward_BacktesterService_ExecuteStrategyFromConfig_0 = runtime.ForwardResponseMessage
)

View File

@@ -0,0 +1,199 @@
syntax = "proto3";
package btrpc;
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
option go_package = "github.com/thrasher-corp/gocryptotrader/backtester/btrpc";
// struct definitions
message StrategySettings {
string name = 1;
bool use_simultaneous_signal_processing = 2;
bool disable_usd_tracking = 3;
repeated CustomSettings custom_settings = 4;
}
message CustomSettings {
string key_field = 1;
string key_value = 2;
}
message ExchangeLevelFunding {
string exchange_name = 1;
string asset = 2;
string currency = 3;
string initial_funds = 4;
string transfer_fee = 5;
}
message FundingSettings {
bool use_exchange_level_funding = 1;
repeated ExchangeLevelFunding exchange_level_funding = 2;
}
message PurchaseSide {
string minimum_size = 1;
string maximum_size = 2;
string maximum_total = 3;
}
message SpotDetails {
string initial_base_funds = 1;
string initial_quote_funds = 2;
}
message FuturesDetails {
Leverage leverage = 1;
}
message CurrencySettings {
string exchange_name = 1;
string asset = 2;
string base = 3;
string quote = 4;
PurchaseSide buy_side = 5;
PurchaseSide sell_side = 6;
string min_slippage_percent = 7;
string max_slippage_percent = 8;
string maker_fee_override = 9;
string taker_fee_override = 10;
string maximum_holdings_ratio = 11;
bool skip_candle_volume_fitting = 12;
bool use_exchange_order_limits = 13;
bool use_exchange_pnl_calculation = 14;
SpotDetails spot_details = 15;
FuturesDetails futures_details = 16;
}
message ApiData {
google.protobuf.Timestamp start_date = 1;
google.protobuf.Timestamp end_date = 2;
bool inclusive_end_date = 3;
}
message DbConfig {
bool enabled = 1;
bool verbose = 2;
string driver = 3;
string host = 4;
uint32 port = 5;
string username = 6;
string password = 7;
string database = 8;
string ssl_mode = 9;
}
message DbData {
google.protobuf.Timestamp start_date = 1;
google.protobuf.Timestamp end_date = 2;
DbConfig config = 3;
string path = 4;
bool inclusive_end_date = 5;
}
message CsvData {
string path = 1;
}
message DatabaseConnectionDetails {
string host = 1;
uint32 port = 2;
string user_name = 3;
string password = 4;
string database = 5;
string ssl_mode = 6;
}
message DatabaseConfig {
bool enabled = 1;
bool verbose = 2;
string driver = 3;
DatabaseConnectionDetails config = 4;
}
message DatabaseData {
google.protobuf.Timestamp start_date = 1;
google.protobuf.Timestamp end_date = 2;
DatabaseConfig config = 3;
string path = 4;
bool inclusive_end_date = 5;
}
message CSVData {
string path = 1;
}
message LiveData {
string api_key_override = 1;
string api_secret_override = 2;
string api_client_id_override = 3;
string api_2fa_override = 4;
string api_sub_account_override = 5;
bool use_real_orders = 6;
}
message DataSettings {
uint64 interval = 1;
string datatype = 2;
ApiData api_data = 3;
DatabaseData database_data = 4;
CSVData csv_data = 5;
LiveData live_data = 6;
}
message Leverage {
bool can_use_leverage = 1;
string maximum_orders_with_leverage_ratio = 2;
string maximum_leverage_rate = 3;
string maximum_collateral_leverage_rate = 4;
}
message PortfolioSettings {
Leverage leverage = 1;
PurchaseSide buy_side = 2;
PurchaseSide sell_side = 3;
}
message StatisticSettings {
string risk_free_rate = 1;
}
message Config {
string nickname = 1;
string goal = 2;
StrategySettings strategy_settings = 3;
FundingSettings funding_settings = 4;
repeated CurrencySettings currency_settings = 5;
DataSettings data_settings = 6;
PortfolioSettings portfolio_settings = 7;
StatisticSettings statistic_settings = 8;
}
// Requests and responses
message ExecuteStrategyFromFileRequest {
string strategy_file_path = 1;
}
message ExecuteStrategyResponse {
bool success = 1;
string message = 2;
}
message ExecuteStrategyFromConfigRequest {
btrpc.Config config = 1;
}
service BacktesterService {
rpc ExecuteStrategyFromFile(ExecuteStrategyFromFileRequest) returns (ExecuteStrategyResponse) {
option (google.api.http) = {
get: "/v1/executestrategyfromfile"
};
}
rpc ExecuteStrategyFromConfig(ExecuteStrategyFromConfigRequest) returns (ExecuteStrategyResponse) {
option (google.api.http) = {
get: "/v1/executestrategyfromconfig"
};
}
}

View File

@@ -0,0 +1,729 @@
{
"swagger": "2.0",
"info": {
"title": "btrpc.proto",
"version": "version not set"
},
"tags": [
{
"name": "BacktesterService"
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/v1/executestrategyfromconfig": {
"get": {
"operationId": "BacktesterService_ExecuteStrategyFromConfig",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcExecuteStrategyResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "config.nickname",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.goal",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.strategySettings.name",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.strategySettings.useSimultaneousSignalProcessing",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.strategySettings.disableUsdTracking",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.fundingSettings.useExchangeLevelFunding",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.dataSettings.interval",
"in": "query",
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "config.dataSettings.datatype",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.apiData.startDate",
"in": "query",
"required": false,
"type": "string",
"format": "date-time"
},
{
"name": "config.dataSettings.apiData.endDate",
"in": "query",
"required": false,
"type": "string",
"format": "date-time"
},
{
"name": "config.dataSettings.apiData.inclusiveEndDate",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.dataSettings.databaseData.startDate",
"in": "query",
"required": false,
"type": "string",
"format": "date-time"
},
{
"name": "config.dataSettings.databaseData.endDate",
"in": "query",
"required": false,
"type": "string",
"format": "date-time"
},
{
"name": "config.dataSettings.databaseData.config.enabled",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.dataSettings.databaseData.config.verbose",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.dataSettings.databaseData.config.driver",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.databaseData.config.config.host",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.databaseData.config.config.port",
"in": "query",
"required": false,
"type": "integer",
"format": "int64"
},
{
"name": "config.dataSettings.databaseData.config.config.userName",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.databaseData.config.config.password",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.databaseData.config.config.database",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.databaseData.config.config.sslMode",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.databaseData.path",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.databaseData.inclusiveEndDate",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.dataSettings.csvData.path",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.liveData.apiKeyOverride",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.dataSettings.liveData.apiSecretOverride",
"in": "query",
"required": false,
"type": "string"
},
{
"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",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.portfolioSettings.leverage.canUseLeverage",
"in": "query",
"required": false,
"type": "boolean"
},
{
"name": "config.portfolioSettings.leverage.maximumOrdersWithLeverageRatio",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.leverage.maximumLeverageRate",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.leverage.maximumCollateralLeverageRate",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.buySide.minimumSize",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.buySide.maximumSize",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.buySide.maximumTotal",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.sellSide.minimumSize",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.sellSide.maximumSize",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.portfolioSettings.sellSide.maximumTotal",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "config.statisticSettings.riskFreeRate",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"BacktesterService"
]
}
},
"/v1/executestrategyfromfile": {
"get": {
"operationId": "BacktesterService_ExecuteStrategyFromFile",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/btrpcExecuteStrategyResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "strategyFilePath",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"BacktesterService"
]
}
}
},
"definitions": {
"btrpcApiData": {
"type": "object",
"properties": {
"startDate": {
"type": "string",
"format": "date-time"
},
"endDate": {
"type": "string",
"format": "date-time"
},
"inclusiveEndDate": {
"type": "boolean"
}
}
},
"btrpcCSVData": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
},
"btrpcConfig": {
"type": "object",
"properties": {
"nickname": {
"type": "string"
},
"goal": {
"type": "string"
},
"strategySettings": {
"$ref": "#/definitions/btrpcStrategySettings"
},
"fundingSettings": {
"$ref": "#/definitions/btrpcFundingSettings"
},
"currencySettings": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcCurrencySettings"
}
},
"dataSettings": {
"$ref": "#/definitions/btrpcDataSettings"
},
"portfolioSettings": {
"$ref": "#/definitions/btrpcPortfolioSettings"
},
"statisticSettings": {
"$ref": "#/definitions/btrpcStatisticSettings"
}
}
},
"btrpcCurrencySettings": {
"type": "object",
"properties": {
"exchangeName": {
"type": "string"
},
"asset": {
"type": "string"
},
"base": {
"type": "string"
},
"quote": {
"type": "string"
},
"buySide": {
"$ref": "#/definitions/btrpcPurchaseSide"
},
"sellSide": {
"$ref": "#/definitions/btrpcPurchaseSide"
},
"minSlippagePercent": {
"type": "string"
},
"maxSlippagePercent": {
"type": "string"
},
"makerFeeOverride": {
"type": "string"
},
"takerFeeOverride": {
"type": "string"
},
"maximumHoldingsRatio": {
"type": "string"
},
"skipCandleVolumeFitting": {
"type": "boolean"
},
"useExchangeOrderLimits": {
"type": "boolean"
},
"useExchangePnlCalculation": {
"type": "boolean"
},
"spotDetails": {
"$ref": "#/definitions/btrpcSpotDetails"
},
"futuresDetails": {
"$ref": "#/definitions/btrpcFuturesDetails"
}
}
},
"btrpcCustomSettings": {
"type": "object",
"properties": {
"keyField": {
"type": "string"
},
"keyValue": {
"type": "string"
}
}
},
"btrpcDataSettings": {
"type": "object",
"properties": {
"interval": {
"type": "string",
"format": "uint64"
},
"datatype": {
"type": "string"
},
"apiData": {
"$ref": "#/definitions/btrpcApiData"
},
"databaseData": {
"$ref": "#/definitions/btrpcDatabaseData"
},
"csvData": {
"$ref": "#/definitions/btrpcCSVData"
},
"liveData": {
"$ref": "#/definitions/btrpcLiveData"
}
}
},
"btrpcDatabaseConfig": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"verbose": {
"type": "boolean"
},
"driver": {
"type": "string"
},
"config": {
"$ref": "#/definitions/btrpcDatabaseConnectionDetails"
}
}
},
"btrpcDatabaseConnectionDetails": {
"type": "object",
"properties": {
"host": {
"type": "string"
},
"port": {
"type": "integer",
"format": "int64"
},
"userName": {
"type": "string"
},
"password": {
"type": "string"
},
"database": {
"type": "string"
},
"sslMode": {
"type": "string"
}
}
},
"btrpcDatabaseData": {
"type": "object",
"properties": {
"startDate": {
"type": "string",
"format": "date-time"
},
"endDate": {
"type": "string",
"format": "date-time"
},
"config": {
"$ref": "#/definitions/btrpcDatabaseConfig"
},
"path": {
"type": "string"
},
"inclusiveEndDate": {
"type": "boolean"
}
}
},
"btrpcExchangeLevelFunding": {
"type": "object",
"properties": {
"exchangeName": {
"type": "string"
},
"asset": {
"type": "string"
},
"currency": {
"type": "string"
},
"initialFunds": {
"type": "string"
},
"transferFee": {
"type": "string"
}
}
},
"btrpcExecuteStrategyResponse": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"message": {
"type": "string"
}
}
},
"btrpcFundingSettings": {
"type": "object",
"properties": {
"useExchangeLevelFunding": {
"type": "boolean"
},
"exchangeLevelFunding": {
"type": "array",
"items": {
"$ref": "#/definitions/btrpcExchangeLevelFunding"
}
}
}
},
"btrpcFuturesDetails": {
"type": "object",
"properties": {
"leverage": {
"$ref": "#/definitions/btrpcLeverage"
}
}
},
"btrpcLeverage": {
"type": "object",
"properties": {
"canUseLeverage": {
"type": "boolean"
},
"maximumOrdersWithLeverageRatio": {
"type": "string"
},
"maximumLeverageRate": {
"type": "string"
},
"maximumCollateralLeverageRate": {
"type": "string"
}
}
},
"btrpcLiveData": {
"type": "object",
"properties": {
"apiKeyOverride": {
"type": "string"
},
"apiSecretOverride": {
"type": "string"
},
"apiClientIdOverride": {
"type": "string"
},
"api2faOverride": {
"type": "string"
},
"apiSubAccountOverride": {
"type": "string"
},
"useRealOrders": {
"type": "boolean"
}
}
},
"btrpcPortfolioSettings": {
"type": "object",
"properties": {
"leverage": {
"$ref": "#/definitions/btrpcLeverage"
},
"buySide": {
"$ref": "#/definitions/btrpcPurchaseSide"
},
"sellSide": {
"$ref": "#/definitions/btrpcPurchaseSide"
}
}
},
"btrpcPurchaseSide": {
"type": "object",
"properties": {
"minimumSize": {
"type": "string"
},
"maximumSize": {
"type": "string"
},
"maximumTotal": {
"type": "string"
}
}
},
"btrpcSpotDetails": {
"type": "object",
"properties": {
"initialBaseFunds": {
"type": "string"
},
"initialQuoteFunds": {
"type": "string"
}
}
},
"btrpcStatisticSettings": {
"type": "object",
"properties": {
"riskFreeRate": {
"type": "string"
}
}
},
"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": {
"@type": {
"type": "string"
}
},
"additionalProperties": {}
},
"rpcStatus": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}

View File

@@ -0,0 +1,141 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc (unknown)
// source: btrpc.proto
package btrpc
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// BacktesterServiceClient is the client API for BacktesterService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
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)
}
type backtesterServiceClient struct {
cc grpc.ClientConnInterface
}
func NewBacktesterServiceClient(cc grpc.ClientConnInterface) BacktesterServiceClient {
return &backtesterServiceClient{cc}
}
func (c *backtesterServiceClient) ExecuteStrategyFromFile(ctx context.Context, in *ExecuteStrategyFromFileRequest, opts ...grpc.CallOption) (*ExecuteStrategyResponse, error) {
out := new(ExecuteStrategyResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ExecuteStrategyFromFile", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *backtesterServiceClient) ExecuteStrategyFromConfig(ctx context.Context, in *ExecuteStrategyFromConfigRequest, opts ...grpc.CallOption) (*ExecuteStrategyResponse, error) {
out := new(ExecuteStrategyResponse)
err := c.cc.Invoke(ctx, "/btrpc.BacktesterService/ExecuteStrategyFromConfig", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// BacktesterServiceServer is the server API for BacktesterService service.
// All implementations must embed UnimplementedBacktesterServiceServer
// for forward compatibility
type BacktesterServiceServer interface {
ExecuteStrategyFromFile(context.Context, *ExecuteStrategyFromFileRequest) (*ExecuteStrategyResponse, error)
ExecuteStrategyFromConfig(context.Context, *ExecuteStrategyFromConfigRequest) (*ExecuteStrategyResponse, error)
mustEmbedUnimplementedBacktesterServiceServer()
}
// UnimplementedBacktesterServiceServer must be embedded to have forward compatible implementations.
type UnimplementedBacktesterServiceServer struct {
}
func (UnimplementedBacktesterServiceServer) ExecuteStrategyFromFile(context.Context, *ExecuteStrategyFromFileRequest) (*ExecuteStrategyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExecuteStrategyFromFile not implemented")
}
func (UnimplementedBacktesterServiceServer) ExecuteStrategyFromConfig(context.Context, *ExecuteStrategyFromConfigRequest) (*ExecuteStrategyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ExecuteStrategyFromConfig not implemented")
}
func (UnimplementedBacktesterServiceServer) mustEmbedUnimplementedBacktesterServiceServer() {}
// UnsafeBacktesterServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to BacktesterServiceServer will
// result in compilation errors.
type UnsafeBacktesterServiceServer interface {
mustEmbedUnimplementedBacktesterServiceServer()
}
func RegisterBacktesterServiceServer(s grpc.ServiceRegistrar, srv BacktesterServiceServer) {
s.RegisterService(&BacktesterService_ServiceDesc, srv)
}
func _BacktesterService_ExecuteStrategyFromFile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExecuteStrategyFromFileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).ExecuteStrategyFromFile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/ExecuteStrategyFromFile",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).ExecuteStrategyFromFile(ctx, req.(*ExecuteStrategyFromFileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _BacktesterService_ExecuteStrategyFromConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ExecuteStrategyFromConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(BacktesterServiceServer).ExecuteStrategyFromConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/btrpc.BacktesterService/ExecuteStrategyFromConfig",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(BacktesterServiceServer).ExecuteStrategyFromConfig(ctx, req.(*ExecuteStrategyFromConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
// BacktesterService_ServiceDesc is the grpc.ServiceDesc for BacktesterService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var BacktesterService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "btrpc.BacktesterService",
HandlerType: (*BacktesterServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ExecuteStrategyFromFile",
Handler: _BacktesterService_ExecuteStrategyFromFile_Handler,
},
{
MethodName: "ExecuteStrategyFromConfig",
Handler: _BacktesterService_ExecuteStrategyFromConfig_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "btrpc.proto",
}

View File

@@ -0,0 +1,17 @@
version: v1
plugins:
- name: go
out: ./
opt:
- paths=source_relative
- name: go-grpc
out: ./
opt:
- paths=source_relative
- name: grpc-gateway
out: ./
opt:
- paths=source_relative
- generate_unbound_methods=true
- name: openapiv2
out: ./

11
backtester/btrpc/buf.lock Normal file
View File

@@ -0,0 +1,11 @@
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: 62f35d8aed1149c291d606d958a7ce32
- remote: buf.build
owner: grpc-ecosystem
repository: grpc-gateway
commit: bc28b723cd774c32b6fbc77621518765

17
backtester/btrpc/buf.yaml Normal file
View File

@@ -0,0 +1,17 @@
version: v1
name: buf.build/gocryptotrader/btrpc
lint:
use:
- DEFAULT
except:
- RPC_REQUEST_RESPONSE_UNIQUE
- PACKAGE_DIRECTORY_MATCH
- PACKAGE_VERSION_SUFFIX
- RPC_RESPONSE_STANDARD_NAME
- RPC_REQUEST_STANDARD_NAME
breaking:
use:
- FILE
deps:
- buf.build/googleapis/googleapis
- buf.build/grpc-ecosystem/grpc-gateway

View File

@@ -134,43 +134,90 @@ func RegisterBacktesterSubLoggers() error {
// PurgeColours removes colour information
func PurgeColours() {
ColourGreen = ""
ColourWhite = ""
ColourGrey = ""
ColourDefault = ""
ColourH1 = ""
ColourH2 = ""
ColourH3 = ""
ColourH4 = ""
ColourSuccess = ""
ColourInfo = ""
ColourDebug = ""
ColourWarn = ""
ColourDarkGrey = ""
ColourError = ""
CMDColours.Green = ""
CMDColours.White = ""
CMDColours.Grey = ""
CMDColours.Default = ""
CMDColours.H1 = ""
CMDColours.H2 = ""
CMDColours.H3 = ""
CMDColours.H4 = ""
CMDColours.Success = ""
CMDColours.Info = ""
CMDColours.Debug = ""
CMDColours.Warn = ""
CMDColours.DarkGrey = ""
CMDColours.Error = ""
}
// SetColours sets cmd output colours at startup. Doing it at any other point
// risks races and this really isn't worth adding a mutex for
func SetColours(colours *Colours) {
if colours.Default != "" && colours.Default != CMDColours.Default {
CMDColours.Default = colours.Default
}
if colours.Green != "" && colours.Green != CMDColours.Green {
CMDColours.Green = colours.Green
}
if colours.Error != "" && colours.Error != CMDColours.Error {
CMDColours.Error = colours.Error
}
if colours.White != "" && colours.White != CMDColours.White {
CMDColours.White = colours.White
}
if colours.Grey != "" && colours.Grey != CMDColours.Grey {
CMDColours.Grey = colours.Grey
}
if colours.H1 != "" && colours.H1 != CMDColours.H1 {
CMDColours.H1 = colours.H1
}
if colours.H2 != "" && colours.H2 != CMDColours.H2 {
CMDColours.H2 = colours.H2
}
if colours.H3 != "" && colours.H3 != CMDColours.H3 {
CMDColours.H3 = colours.H3
}
if colours.H4 != "" && colours.H4 != CMDColours.H4 {
CMDColours.H4 = colours.H4
}
if colours.Success != "" && colours.Error != CMDColours.Success {
CMDColours.Success = colours.Success
}
if colours.Info != "" && colours.Info != CMDColours.Info {
CMDColours.Info = colours.Info
}
if colours.Debug != "" && colours.Debug != CMDColours.Debug {
CMDColours.Debug = colours.Debug
}
if colours.Warn != "" && colours.Warn != CMDColours.Warn {
CMDColours.Warn = colours.Warn
}
if colours.DarkGrey != "" && colours.DarkGrey != CMDColours.DarkGrey {
CMDColours.DarkGrey = colours.DarkGrey
}
}
// Logo returns the logo
func Logo() string {
sb := strings.Builder{}
sb.WriteString(" \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@@ \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@@@@@@@@ " + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@" + ColourGrey + ",,,,, " + ColourWhite + "@@@@@@@@@" + ColourGrey + ",,,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@" + ColourGrey + ",,,,,,, " + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@" + ColourGrey + "(,,,,,,,, " + ColourGrey + ",," + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",," + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,, #,,,,,,,,,,,,,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,,,,,,,,,,,,,,,,,,," + ColourGreen + "%%%%%%%" + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",,,,,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,,,,,,," + ColourGreen + "%%%%%" + ColourGrey + " ,,,,,," + ColourGrey + "%" + ColourGreen + "%%%%%%" + ColourWhite + " \n")
sb.WriteString(" " + ColourGrey + ",,,,,,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,,,," + ColourGreen + "%%%%%%%%%%%%%%%%%%" + ColourGrey + "#" + ColourGreen + "%%" + ColourGrey + " \n")
sb.WriteString(" " + ColourGrey + ",,,,,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,," + ColourGreen + "%%%" + ColourGrey + " ,,,,," + ColourGreen + "%%%%%%%%" + ColourGrey + ",,,,, \n")
sb.WriteString(" " + ColourGrey + ",,,*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,," + ColourGreen + "%%" + ColourGrey + ",, ,,,,,,," + ColourWhite + "@" + ColourGreen + "*%%," + ColourWhite + "@" + ColourGrey + ",,,,,, \n")
sb.WriteString(" " + ColourGrey + "*" + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,, " + ColourGrey + ",,,,," + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@" + ColourGrey + ",,,,,,,,, " + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@" + ColourGrey + ",,,,,,, " + ColourWhite + "@@@@@@@" + ColourGrey + ",,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@" + ColourGrey + ",,,, " + ColourWhite + "@@@@@@@@@" + ColourGrey + "#,,,,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@@@@@@@@ " + ColourGrey + "*,,,," + ColourWhite + " \n")
sb.WriteString(" " + ColourWhite + "@@@@@@@@@@@@@@@@" + ColourDefault + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@@@@@@@@@@ \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@@@@@@@@@@@@@@@@ " + CMDColours.Grey + ",,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@" + CMDColours.Grey + ",,,,, " + CMDColours.White + "@@@@@@@@@" + CMDColours.Grey + ",,,,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@" + CMDColours.Grey + ",,,,,,, " + CMDColours.White + "@@@@@@@" + CMDColours.Grey + ",,,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@" + CMDColours.Grey + "(,,,,,,,, " + CMDColours.Grey + ",," + CMDColours.White + "@@@@@@@" + CMDColours.Grey + ",,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.Grey + ",," + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,,,,, #,,,,,,,,,,,,,,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.Grey + ",,,,*" + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,,,,,,,,,,,,,,,,,,,,,," + CMDColours.Green + "%%%%%%%" + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.Grey + ",,,,,,,*" + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,,,,,,,,,," + CMDColours.Green + "%%%%%" + CMDColours.Grey + " ,,,,,," + CMDColours.Grey + "%" + CMDColours.Green + "%%%%%%" + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.Grey + ",,,,,,,,*" + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,,,,,,," + CMDColours.Green + "%%%%%%%%%%%%%%%%%%" + CMDColours.Grey + "#" + CMDColours.Green + "%%" + CMDColours.Grey + " \n")
sb.WriteString(" " + CMDColours.Grey + ",,,,,,*" + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,,,,," + CMDColours.Green + "%%%" + CMDColours.Grey + " ,,,,," + CMDColours.Green + "%%%%%%%%" + CMDColours.Grey + ",,,,, \n")
sb.WriteString(" " + CMDColours.Grey + ",,,*" + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,," + CMDColours.Green + "%%" + CMDColours.Grey + ",, ,,,,,,," + CMDColours.White + "@" + CMDColours.Green + "*%%," + CMDColours.White + "@" + CMDColours.Grey + ",,,,,, \n")
sb.WriteString(" " + CMDColours.Grey + "*" + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,,,,, " + CMDColours.Grey + ",,,,," + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@" + CMDColours.Grey + ",,,,,,,,, " + CMDColours.White + "@@@@@@@" + CMDColours.Grey + ",,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@" + CMDColours.Grey + ",,,,,,, " + CMDColours.White + "@@@@@@@" + CMDColours.Grey + ",,,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@@" + CMDColours.Grey + ",,,, " + CMDColours.White + "@@@@@@@@@" + CMDColours.Grey + "#,,,,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@@@@@@@@@@@@@@@@ " + CMDColours.Grey + "*,,,," + CMDColours.White + " \n")
sb.WriteString(" " + CMDColours.White + "@@@@@@@@@@@@@@@@" + CMDColours.Default + " \n")
sb.WriteString(ASCIILogo)
return sb.String()
}

View File

@@ -209,7 +209,7 @@ func TestLogo(t *testing.T) {
func TestPurgeColours(t *testing.T) {
PurgeColours()
if ColourSuccess != "" {
if CMDColours.Success != "" {
t.Error("expected purged colour")
}
}

View File

@@ -33,6 +33,8 @@ var (
ErrNilEvent = errors.New("nil event received")
// ErrInvalidDataType occurs when an invalid data type is defined in the config
ErrInvalidDataType = errors.New("invalid datatype received")
// ErrFileNotFound returned when the file is not found
ErrFileNotFound = errors.New("file not found")
errCannotGenerateFileName = errors.New("cannot generate filename")
)
@@ -89,23 +91,41 @@ type Directioner interface {
GetDirection() order.Side
}
// colours to display for the terminal output
var (
ColourDefault = "\u001b[0m"
ColourGreen = "\033[38;5;157m"
ColourWhite = "\033[38;5;255m"
ColourGrey = "\033[38;5;240m"
ColourDarkGrey = "\033[38;5;243m"
ColourH1 = "\033[38;5;33m"
ColourH2 = "\033[38;5;39m"
ColourH3 = "\033[38;5;45m"
ColourH4 = "\033[38;5;51m"
ColourSuccess = "\033[38;5;40m"
ColourInfo = "\u001B[32m"
ColourDebug = "\u001B[34m"
ColourWarn = "\u001B[33m"
ColourError = "\033[38;5;196m"
)
// Colours defines colour types for CMD output
type Colours struct {
Default string
Green string
White string
Grey string
DarkGrey string
H1 string
H2 string
H3 string
H4 string
Success string
Info string
Debug string
Warn string
Error string
}
// CMDColours holds colour information for CMD output
var CMDColours = Colours{
Default: "\u001b[0m",
Green: "\033[38;5;157m",
White: "\033[38;5;255m",
Grey: "\033[38;5;240m",
DarkGrey: "\033[38;5;243m",
H1: "\033[38;5;33m",
H2: "\033[38;5;39m",
H3: "\033[38;5;45m",
H4: "\033[38;5;51m",
Success: "\033[38;5;40m",
Info: "\u001B[32m",
Debug: "\u001B[34m",
Warn: "\u001B[33m",
Error: "\033[38;5;196m",
}
// ASCIILogo is a sweet logo that is optionally printed to the command line window
const ASCIILogo = `

View File

@@ -20,6 +20,65 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
## Config package overview
## 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 |
### 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` |
### 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/` |
### Backtester Config Colours overview
| Key | Description | Example |
|----------|---------------------------------------------------------------------|----------------|
| Default | The colour definition for default text output |`` |
| Green | The colour definition for when green is warranted, such as the logo |`` |
| White | The colour definition for when white is warranted such as the logo |`` |
| Grey | The colour definition for grey | ``|
| DarkGrey | The colour definition for dark grey | ``|
| H1 | The colour definition for main headers | `` |
| H2 | The colour definition for sub headers | `` |
| H3 | The colour definition for sub sub headers | `` |
| H4 | The colour definition for sub sub sub headers | `` |
| Success | The colour definition for successful operations | `` |
| Info | The colour definition for when informing you of something | `` |
| Debug | The colour definition for debug output such as verbose | `` |
| Warn | The colour definition for when a warning occurs | `` |
| Error | The colour definition for when an error occurs | ``|
## Strategy Config overview
### What does the config package do?
The config package contains a set of structs which allow for the customisation of the GoCryptoTrader Backtester when running.
The GoCryptoTrader Backtester runs from reading config files (`.strat` files by default under `/examples`).
@@ -38,172 +97,172 @@ 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 |
| 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 |
| StatisticSettings | 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` |
| 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` |
#### Funding Config Settings
| Key | Description | Example |
| --- | ------- | --- |
| 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 | `[]` |
| ExchangeLevelFunding | 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 |
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|
| 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` |
#### 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 |
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|
| 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 |
##### 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` |
| 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` |
##### FuturesSettings
| Key | Description | Example |
| --- | ------- | ----- |
| Leverage | This struct defines the leverage rules that this specific currency setting must abide by | `1` |
| Key | Description | Example |
|----------|------------------------------------------------------------------------------------------|---------|
| 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 |
| 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` |
| Key | Description | Example |
|--------------|-------------------------------------------------------------------------|---------|
| RiskFreeRate | The risk free rate used in the calculation of sharpe and sortino ratios | `0.03` |
#### 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 |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| 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` |
#### 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 |
|----------|--------------------------------------------------------------------------------------------------------|--------------------------|
| 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` |
#### 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 |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| 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` |
##### database
| Config | Description | Example |
| ------ | ----------- | ------- |
| enabled | Enabled or disables the database connection subsystem | `true` |
| verbose | Displays more information to the logger which can be helpful for debugging | `false` |
| driver | The SQL driver to use. Can be `postgres` or `sqlite` | `sqlite` |
| connectionDetails | See below | |
| Config | Description | Example |
|-------------------|----------------------------------------------------------------------------|----------|
| enabled | Enabled or disables the database connection subsystem | `true` |
| verbose | Displays more information to the logger which can be helpful for debugging | `false` |
| driver | The SQL driver to use. Can be `postgres` or `sqlite` | `sqlite` |
| connectionDetails | See below | |
##### connectionDetails
| Config | Description | Example |
| ------ | ----------- | ------- |
| host | The host address of the database | `localhost` |
| port | The port used to connect to the database | `5432` |
| username | An optional username to connect to the database | `username` |
| password | An optional password to connect to the database | `password` |
| database | The name of the database | `database.db` |
| sslmode | The connection type of the database for Postgres databases only | `disable` |
| Config | Description | Example |
|----------|-----------------------------------------------------------------|---------------|
| host | The host address of the database | `localhost` |
| port | The port used to connect to the database | `5432` |
| username | An optional username to connect to the database | `username` |
| password | An optional password to connect to the database | `password` |
| database | The name of the database | `database.db` |
| sslmode | The connection type of the database for Postgres databases only | `disable` |
#### LiveData
| Key | Description | Example |
| --- | ----------- | ------- |
| 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 |
|-----------------------|--------------------------------------------------------------------------------------------------------|---------------|
| 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` |
##### 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 |
|--------------------------------|------------------------------------------------------------------------------------------|---------|
| 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` |
##### 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 |
|--------------|------------------------------------------------------------------------------------------------------------------|---------|
| 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` |
### Please click GoDocs chevron above to view current GoDoc information for this package

View File

@@ -0,0 +1,71 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
gctconfig "github.com/thrasher-corp/gocryptotrader/config"
)
// ReadBacktesterConfigFromPath will take a config from a path
func ReadBacktesterConfigFromPath(path string) (*BacktesterConfig, error) {
if !file.Exists(path) {
return nil, fmt.Errorf("%w %v", common.ErrFileNotFound, path)
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var resp *BacktesterConfig
err = json.Unmarshal(data, &resp)
return resp, err
}
// GenerateDefaultConfig will return the default backtester config
func GenerateDefaultConfig() (*BacktesterConfig, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
return &BacktesterConfig{
PrintLogo: true,
LogSubheaders: true,
Report: Report{
GenerateReport: true,
TemplatePath: filepath.Join(wd, "report", "tpl.gohtml"),
OutputPath: filepath.Join(wd, "results"),
},
GRPC: GRPC{
Username: "rpcuser",
Password: "helloImTheDefaultPassword",
GRPCConfig: gctconfig.GRPCConfig{
Enabled: true,
ListenAddress: "localhost:9054",
},
TLSDir: DefaultBTDir,
},
UseCMDColours: true,
Colours: common.Colours{
Default: common.CMDColours.Default,
Green: common.CMDColours.Green,
White: common.CMDColours.White,
Grey: common.CMDColours.Grey,
DarkGrey: common.CMDColours.DarkGrey,
H1: common.CMDColours.H1,
H2: common.CMDColours.H2,
H3: common.CMDColours.H3,
H4: common.CMDColours.H4,
Success: common.CMDColours.Success,
Info: common.CMDColours.Info,
Debug: common.CMDColours.Debug,
Warn: common.CMDColours.Warn,
Error: common.CMDColours.Error,
},
}, nil
}

View File

@@ -0,0 +1,45 @@
package config
import (
"path/filepath"
"runtime"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
gctconfig "github.com/thrasher-corp/gocryptotrader/config"
)
var (
// DefaultBTDir is the default backtester config directory
DefaultBTDir = filepath.Join(gctcommon.GetDefaultDataDir(runtime.GOOS), "backtester")
// DefaultBTConfigDir is the default backtester config file
DefaultBTConfigDir = filepath.Join(DefaultBTDir, "config.json")
)
// 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"`
}
// Report contains the report settings
type Report struct {
GenerateReport bool `json:"output-report"`
TemplatePath string `json:"template-path"`
OutputPath string `json:"output-path"`
DarkMode bool `json:"dark-mode"`
}
// GRPC holds the GRPC configuration
type GRPC struct {
Username string `json:"username"`
Password string `json:"password"`
gctconfig.GRPCConfig
TLSDir string `json:"tls-dir"`
}

View File

@@ -0,0 +1,49 @@
package config
import (
"encoding/json"
"errors"
"path/filepath"
"testing"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
)
func TestLoadBacktesterConfig(t *testing.T) {
t.Parallel()
cfg, err := GenerateDefaultConfig()
if err != nil {
t.Error(err)
}
testConfig, err := json.Marshal(cfg)
if err != nil {
t.Error(err)
}
dir := t.TempDir()
f := filepath.Join(dir, "test.config")
err = file.Write(f, testConfig)
if err != nil {
t.Error(err)
}
_, err = ReadBacktesterConfigFromPath(f)
if err != nil {
t.Error(err)
}
_, err = ReadBacktesterConfigFromPath("test")
if !errors.Is(err, common.ErrFileNotFound) {
t.Errorf("received '%v' expected '%v'", err, common.ErrFileNotFound)
}
}
func TestGenerateDefaultConfig(t *testing.T) {
t.Parallel()
cfg, err := GenerateDefaultConfig()
if err != nil {
t.Error(err)
}
if !cfg.PrintLogo {
t.Errorf("received '%v' expected '%v'", cfg.PrintLogo, true)
}
}

View File

@@ -2,7 +2,6 @@ package config
import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"
@@ -17,28 +16,27 @@ import (
"github.com/thrasher-corp/gocryptotrader/log"
)
// ReadConfigFromFile will take a config from a path
func ReadConfigFromFile(path string) (*Config, error) {
// ReadStrategyConfigFromFile will take a config from a path
func ReadStrategyConfigFromFile(path string) (*Config, error) {
if !file.Exists(path) {
return nil, errors.New("file not found")
return nil, fmt.Errorf("%w %v", common.ErrFileNotFound, path)
}
fileData, err := os.ReadFile(path)
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return LoadConfig(fileData)
}
// LoadConfig unmarshalls byte data into a config struct
func LoadConfig(data []byte) (resp *Config, err error) {
var resp *Config
err = json.Unmarshal(data, &resp)
return resp, err
}
// Validate checks all config settings
func (c *Config) Validate() error {
if c == nil {
return fmt.Errorf("%w nil config", common.ErrNilArguments)
}
err := c.validateDate()
if err != nil {
return err
@@ -137,23 +135,13 @@ func (c *Config) validateStrategySettings() error {
// validateDate checks whether someone has set a date poorly in their config
func (c *Config) validateDate() error {
if c.DataSettings.DatabaseData != nil {
if c.DataSettings.DatabaseData.StartDate.IsZero() ||
c.DataSettings.DatabaseData.EndDate.IsZero() {
return errStartEndUnset
}
if c.DataSettings.DatabaseData.StartDate.After(c.DataSettings.DatabaseData.EndDate) ||
c.DataSettings.DatabaseData.StartDate.Equal(c.DataSettings.DatabaseData.EndDate) {
return errBadDate
if err := gctcommon.StartEndTimeCheck(c.DataSettings.DatabaseData.StartDate, c.DataSettings.DatabaseData.EndDate); err != nil {
return err
}
}
if c.DataSettings.APIData != nil {
if c.DataSettings.APIData.StartDate.IsZero() ||
c.DataSettings.APIData.EndDate.IsZero() {
return errStartEndUnset
}
if c.DataSettings.APIData.StartDate.After(c.DataSettings.APIData.EndDate) ||
c.DataSettings.APIData.StartDate.Equal(c.DataSettings.APIData.EndDate) {
return errBadDate
if err := gctcommon.StartEndTimeCheck(c.DataSettings.APIData.StartDate, c.DataSettings.APIData.EndDate); err != nil {
return err
}
}
return nil
@@ -234,8 +222,8 @@ func (c *Config) validateCurrencySettings() error {
// PrintSetting prints relevant settings to the console for easy reading
func (c *Config) PrintSetting() {
log.Info(common.Config, common.ColourH1+"------------------Backtester Settings------------------------"+common.ColourDefault)
log.Info(common.Config, common.ColourH2+"------------------Strategy Settings--------------------------"+common.ColourDefault)
log.Info(common.Config, common.CMDColours.H1+"------------------Backtester Settings------------------------"+common.CMDColours.Default)
log.Info(common.Config, common.CMDColours.H2+"------------------Strategy Settings--------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Strategy: %s", c.StrategySettings.Name)
if len(c.StrategySettings.CustomSettings) > 0 {
log.Info(common.Config, "Custom strategy variables:")
@@ -249,7 +237,7 @@ func (c *Config) PrintSetting() {
log.Infof(common.Config, "USD value tracking: %v", !c.StrategySettings.DisableUSDTracking)
if c.FundingSettings.UseExchangeLevelFunding && c.StrategySettings.SimultaneousSignalProcessing {
log.Info(common.Config, common.ColourH2+"------------------Funding Settings---------------------------"+common.ColourDefault)
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",
@@ -261,7 +249,7 @@ func (c *Config) PrintSetting() {
}
for i := range c.CurrencySettings {
currStr := fmt.Sprintf(common.ColourH2+"------------------%v %v-%v Currency Settings---------------------------------------------------------"+common.ColourDefault,
currStr := fmt.Sprintf(common.CMDColours.H2+"------------------%v %v-%v Currency Settings---------------------------------------------------------"+common.CMDColours.Default,
c.CurrencySettings[i].Asset,
c.CurrencySettings[i].Base,
c.CurrencySettings[i].Quote)
@@ -303,32 +291,32 @@ func (c *Config) PrintSetting() {
log.Infof(common.Config, "Can use exchange defined order execution limits: %+v", c.CurrencySettings[i].CanUseExchangeLimits)
}
log.Info(common.Config, common.ColourH2+"------------------Portfolio Settings-------------------------"+common.ColourDefault)
log.Info(common.Config, common.CMDColours.H2+"------------------Portfolio Settings-------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Buy rules: %+v", c.PortfolioSettings.BuySide)
log.Infof(common.Config, "Sell rules: %+v", c.PortfolioSettings.SellSide)
log.Infof(common.Config, "Leverage rules: %+v", c.PortfolioSettings.Leverage)
if c.DataSettings.LiveData != nil {
log.Info(common.Config, common.ColourH2+"------------------Live Settings------------------------------"+common.ColourDefault)
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 != "")
}
if c.DataSettings.APIData != nil {
log.Info(common.Config, common.ColourH2+"------------------API Settings-------------------------------"+common.ColourDefault)
log.Info(common.Config, common.CMDColours.H2+"------------------API 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, "Start date: %v", c.DataSettings.APIData.StartDate.Format(gctcommon.SimpleTimeFormat))
log.Infof(common.Config, "End date: %v", c.DataSettings.APIData.EndDate.Format(gctcommon.SimpleTimeFormat))
}
if c.DataSettings.CSVData != nil {
log.Info(common.Config, common.ColourH2+"------------------CSV Settings-------------------------------"+common.ColourDefault)
log.Info(common.Config, common.CMDColours.H2+"------------------CSV 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, "CSV file: %v", c.DataSettings.CSVData.FullPath)
}
if c.DataSettings.DatabaseData != nil {
log.Info(common.Config, common.ColourH2+"------------------Database Settings--------------------------"+common.ColourDefault)
log.Info(common.Config, common.CMDColours.H2+"------------------Database 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, "Start date: %v", c.DataSettings.DatabaseData.StartDate.Format(gctcommon.SimpleTimeFormat))

View File

@@ -12,6 +12,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/top2bottom2"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
@@ -53,14 +54,6 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
func TestLoadConfig(t *testing.T) {
t.Parallel()
_, err := LoadConfig([]byte(`{}`))
if err != nil {
t.Error(err)
}
}
func TestValidateDate(t *testing.T) {
t.Parallel()
c := Config{}
@@ -72,14 +65,14 @@ func TestValidateDate(t *testing.T) {
DatabaseData: &DatabaseData{},
}
err = c.validateDate()
if !errors.Is(err, errStartEndUnset) {
t.Errorf("received: %v, expected: %v", err, errStartEndUnset)
if !errors.Is(err, gctcommon.ErrDateUnset) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrDateUnset)
}
c.DataSettings.DatabaseData.StartDate = time.Now()
c.DataSettings.DatabaseData.EndDate = c.DataSettings.DatabaseData.StartDate
err = c.validateDate()
if !errors.Is(err, errBadDate) {
t.Errorf("received: %v, expected: %v", err, errBadDate)
if !errors.Is(err, gctcommon.ErrStartEqualsEnd) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrStartEqualsEnd)
}
c.DataSettings.DatabaseData.EndDate = c.DataSettings.DatabaseData.StartDate.Add(time.Minute)
err = c.validateDate()
@@ -88,14 +81,14 @@ func TestValidateDate(t *testing.T) {
}
c.DataSettings.APIData = &APIData{}
err = c.validateDate()
if !errors.Is(err, errStartEndUnset) {
t.Errorf("received: %v, expected: %v", err, errStartEndUnset)
if !errors.Is(err, gctcommon.ErrDateUnset) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrDateUnset)
}
c.DataSettings.APIData.StartDate = time.Now()
c.DataSettings.APIData.EndDate = c.DataSettings.APIData.StartDate
err = c.validateDate()
if !errors.Is(err, errBadDate) {
t.Errorf("received: %v, expected: %v", err, errBadDate)
if !errors.Is(err, gctcommon.ErrStartEqualsEnd) {
t.Errorf("received: %v, expected: %v", err, gctcommon.ErrStartEqualsEnd)
}
c.DataSettings.APIData.EndDate = c.DataSettings.APIData.StartDate.Add(time.Minute)
err = c.validateDate()
@@ -137,85 +130,6 @@ func TestValidateCurrencySettings(t *testing.T) {
if err != nil {
t.Error(err)
}
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.Futures
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 err != nil {
t.Error(err)
}
c.CurrencySettings = []CurrencySettings{
{
SellSide: MinMax{
@@ -450,12 +364,19 @@ func TestValidate(t *testing.T) {
},
},
}
if err := c.Validate(); !errors.Is(err, nil) {
err := c.Validate()
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
c = nil
err = c.Validate()
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received %v expected %v", err, common.ErrNilArguments)
}
}
func TestReadConfigFromFile(t *testing.T) {
func TestReadStrategyConfigFromFile(t *testing.T) {
tempDir := t.TempDir()
passFile, err := os.CreateTemp(tempDir, "*.start")
if err != nil {
@@ -469,10 +390,15 @@ func TestReadConfigFromFile(t *testing.T) {
if err != nil {
t.Error(err)
}
_, err = ReadConfigFromFile(passFile.Name())
_, err = ReadStrategyConfigFromFile(passFile.Name())
if err != nil {
t.Error(err)
}
_, err = ReadStrategyConfigFromFile("test")
if !errors.Is(err, common.ErrFileNotFound) {
t.Errorf("received '%v' expected '%v'", err, common.ErrFileNotFound)
}
}
func TestGenerateConfigForDCAAPICandles(t *testing.T) {

View File

@@ -11,15 +11,12 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)
// Errors for config validation
var (
errBadDate = errors.New("start date >= end date, please check your config")
errNoCurrencySettings = errors.New("no currency settings set in the config")
errBadInitialFunds = errors.New("initial funds set with invalid data, please check your config")
errUnsetExchange = errors.New("exchange name unset for currency settings, please check your config")
errUnsetCurrency = errors.New("currency unset for currency settings, please check your config")
errBadSlippageRates = errors.New("invalid slippage rates in currency settings, please check your config")
errStartEndUnset = errors.New("data start and end dates are invalid, please check your config")
errSimultaneousProcessingRequired = errors.New("exchange level funding requires simultaneous processing, please check your config and view funding readme for details")
errExchangeLevelFundingRequired = errors.New("invalid config, funding details set while exchange level funding is disabled")
errExchangeLevelFundingDataRequired = errors.New("invalid config, exchange level funding enabled with no funding data set")

View File

@@ -126,7 +126,7 @@ func main() {
}
fn, err = common.GenerateFileName(fn, extension)
if err != nil {
log.Printf("could not write file, please try again. err: %v", err)
log.Printf("Could not write file, please try again. err: %v", err)
continue
}
fmt.Printf("Enter output file. If blank, will default to \"%v\"\n", fn)
@@ -134,14 +134,14 @@ func main() {
if parsedFileName != "" {
fn, err = common.GenerateFileName(parsedFileName, extension)
if err != nil {
log.Printf("could not write file, please try again. err: %v", err)
log.Printf("Could not write file, please try again. err: %v", err)
continue
}
}
fp = filepath.Join(wd, fn)
err = os.WriteFile(fp, resp, file.DefaultPermissionOctal)
if err != nil {
log.Printf("could not write file, please try again. err: %v", err)
log.Printf("Could not write file, please try again. err: %v", err)
continue
}
break

View File

@@ -38,7 +38,7 @@ func LoadData(startDate, endDate time.Time, interval time.Duration, exchangeName
resp.Item = klineItem
for i := range klineItem.Candles {
if klineItem.Candles[i].ValidationIssues != "" {
log.Warnf(common.Data, "candle validation issue for %v %v %v: %v", klineItem.Exchange, klineItem.Asset, klineItem.Pair, klineItem.Candles[i].ValidationIssues)
log.Warnf(common.Data, "Candle validation issue for %v %v %v: %v", klineItem.Exchange, klineItem.Asset, klineItem.Pair, klineItem.Candles[i].ValidationIssues)
}
}
case common.DataTrade:

View File

@@ -95,7 +95,7 @@ func (d *DataFromKline) AppendResults(ki *gctkline.Item) {
d.RangeHolder.Ranges[i].Intervals[j].HasData = true
}
}
log.Debugf(common.Data, "appending %v candle intervals: %v", len(gctCandles), candleTimes)
log.Debugf(common.Data, "Appending %v candle intervals: %v", len(gctCandles), candleTimes)
d.AppendStream(klineData...)
d.SortStream()
}
@@ -110,7 +110,7 @@ func (d *DataFromKline) StreamOpen() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Open
} else {
log.Errorf(common.Data, "incorrect data loaded into stream")
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
}
return ret
@@ -126,7 +126,7 @@ func (d *DataFromKline) StreamHigh() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.High
} else {
log.Errorf(common.Data, "incorrect data loaded into stream")
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
}
return ret
@@ -142,7 +142,7 @@ func (d *DataFromKline) StreamLow() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Low
} else {
log.Errorf(common.Data, "incorrect data loaded into stream")
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
}
return ret
@@ -158,7 +158,7 @@ func (d *DataFromKline) StreamClose() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Close
} else {
log.Errorf(common.Data, "incorrect data loaded into stream")
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
}
return ret
@@ -174,7 +174,7 @@ func (d *DataFromKline) StreamVol() []decimal.Decimal {
if val, ok := s[x].(*kline.Kline); ok {
ret[x] = val.Volume
} else {
log.Errorf(common.Data, "incorrect data loaded into stream")
log.Errorf(common.Data, "Incorrect data loaded into stream")
}
}
return ret

View File

@@ -49,7 +49,7 @@ func (bt *BackTest) Reset() {
// Run will iterate over loaded data events
// save them and then handle the event based on its type
func (bt *BackTest) Run() {
log.Info(common.Backtester, "running backtester against pre-defined data")
log.Info(common.Backtester, "Running backtester against pre-defined data")
dataLoadingIssue:
for ev := bt.EventQueue.NextEvent(); ; ev = bt.EventQueue.NextEvent() {
if ev == nil {

View File

@@ -0,0 +1,50 @@
# GoCryptoTrader Backtester: Backtest 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/engine/backtest)
[![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 backtest 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)
## Backtest package overview
The backtest package is responsible for handling all events. It is the engine which combines all elements.
Data is converted into candles which are then analysed via the strategyhandler. From there, events can be passed through to other handlers such as the portfolio handler to determine whether or not to place an order
A flow of the application is as follows:
![workflow](https://i.imgur.com/Kup6IA9.png)
### 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

@@ -2,13 +2,11 @@ package engine
import (
"errors"
"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/data"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
@@ -18,7 +16,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
"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/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
@@ -27,13 +24,8 @@ import (
"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/database"
"github.com/thrasher-corp/gocryptotrader/database/drivers"
"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/ftx"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
@@ -62,410 +54,6 @@ func (p portfolioOverride) CreateLiquidationOrdersForExchange(ev common.DataEven
}, nil
}
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, nil) {
t.Errorf("received: %v, expected: %v", err, nil)
}
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{},
}
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),
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{},
}
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: "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{},
}
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{},
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 TestLoadLiveData(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{
AuthenticatedSupport: false,
AuthenticatedWebsocketSupport: false,
PEMKeySupport: false,
CredentialsValidator: struct {
RequiresPEM bool
RequiresKey bool
RequiresSecret bool
RequiresClientID bool
RequiresBase64DecodeSecret bool
}{
RequiresPEM: true,
RequiresKey: true,
RequiresSecret: true,
RequiresClientID: true,
RequiresBase64DecodeSecret: true,
},
},
}
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)
}
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)
}
}
func TestReset(t *testing.T) {
t.Parallel()
f, err := funding.SetupFundingManager(&engine.ExchangeManager{}, true, false)

View File

@@ -0,0 +1,505 @@
package engine
import (
"context"
"errors"
"fmt"
"math"
"net"
"net/http"
"path/filepath"
"strings"
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"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"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/asset"
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/gctrpc/auth"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/utils"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)
var (
errBadPort = errors.New("received bad port")
)
// GRPCServer struct
type GRPCServer struct {
btrpc.BacktesterServiceServer
*config.BacktesterConfig
}
// SetupRPCServer sets up the gRPC server
func SetupRPCServer(cfg *config.BacktesterConfig) *GRPCServer {
return &GRPCServer{
BacktesterConfig: cfg,
}
}
// StartRPCServer starts a gRPC server with TLS auth
func StartRPCServer(server *GRPCServer) error {
targetDir := utils.GetTLSDir(server.GRPC.TLSDir)
if err := gctengine.CheckCerts(targetDir); err != nil {
return err
}
log.Debugf(log.GRPCSys, "Backtester GRPC server enabled. Starting GRPC server on https://%v.\n", server.GRPC.ListenAddress)
lis, err := net.Listen("tcp", server.GRPC.ListenAddress)
if err != nil {
return err
}
creds, err := credentials.NewServerTLSFromFile(filepath.Join(targetDir, "cert.pem"), filepath.Join(targetDir, "key.pem"))
if err != nil {
return err
}
opts := []grpc.ServerOption{
grpc.Creds(creds),
grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(server.authenticateClient)),
}
s := grpc.NewServer(opts...)
btrpc.RegisterBacktesterServiceServer(s, server)
go func() {
if err = s.Serve(lis); err != nil {
log.Error(log.GRPCSys, err)
return
}
}()
log.Debugln(log.GRPCSys, "GRPC server started!")
if server.GRPC.GRPCProxyEnabled {
return server.StartRPCRESTProxy()
}
return nil
}
// 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.GRPC.GRPCProxyListenAddress)
targetDir := utils.GetTLSDir(s.GRPC.TLSDir)
creds, err := credentials.NewClientTLSFromFile(filepath.Join(targetDir, "cert.pem"), "")
if err != nil {
return fmt.Errorf("unabled to start gRPC proxy. Err: %w", err)
}
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(creds),
grpc.WithPerRPCCredentials(auth.BasicAuth{
Username: s.GRPC.Username,
Password: s.GRPC.Password,
}),
}
err = btrpc.RegisterBacktesterServiceHandlerFromEndpoint(context.Background(),
mux, s.GRPC.ListenAddress, opts)
if err != nil {
return fmt.Errorf("failed to register gRPC proxy. Err: %w", err)
}
go func() {
if err = http.ListenAndServe(s.GRPC.GRPCProxyListenAddress, mux); err != nil {
log.Errorf(log.GRPCSys, "GRPC proxy failed to server: %s\n", err)
}
}()
log.Debug(log.GRPCSys, "GRPC proxy server started!")
return nil
}
func (s *GRPCServer) authenticateClient(ctx context.Context) (context.Context, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ctx, fmt.Errorf("unable to extract metadata")
}
authStr, ok := md["authorization"]
if !ok {
return ctx, fmt.Errorf("authorization header missing")
}
if !strings.Contains(authStr[0], "Basic") {
return ctx, fmt.Errorf("basic not found in authorization header")
}
decoded, err := crypto.Base64Decode(strings.Split(authStr[0], " ")[1])
if err != nil {
return ctx, fmt.Errorf("unable to base64 decode authorization header")
}
creds := strings.Split(string(decoded), ":")
username := creds[0]
password := creds[1]
if username != s.GRPC.Username ||
password != s.GRPC.Password {
return ctx, fmt.Errorf("username/password mismatch")
}
return ctx, nil
}
// ExecuteStrategyFromFile will backtest a strategy from the filepath provided
func (s *GRPCServer) ExecuteStrategyFromFile(_ context.Context, request *btrpc.ExecuteStrategyFromFileRequest) (*btrpc.ExecuteStrategyResponse, error) {
if request == nil {
return nil, fmt.Errorf("%w nil request", common.ErrNilArguments)
}
dir := request.StrategyFilePath
cfg, err := config.ReadStrategyConfigFromFile(dir)
if err != nil {
return nil, err
}
err = ExecuteStrategy(cfg, s.BacktesterConfig)
if err != nil {
return nil, err
}
return &btrpc.ExecuteStrategyResponse{
Success: true,
}, nil
}
// ExecuteStrategyFromConfig will backtest a strategy config built from a GRPC command
// this should be a preferred method of interacting with backtester, as it allows for very quick
// minor tweaks to strategy to determine the best result - SO LONG AS YOU DONT OVERFIT
func (s *GRPCServer) ExecuteStrategyFromConfig(_ context.Context, request *btrpc.ExecuteStrategyFromConfigRequest) (*btrpc.ExecuteStrategyResponse, error) {
if request == nil || request.Config == nil {
return nil, fmt.Errorf("%w nil request", common.ErrNilArguments)
}
rfr, err := decimal.NewFromString(request.Config.StatisticSettings.RiskFreeRate)
if err != nil {
return nil, err
}
maximumOrdersWithLeverageRatio, err := decimal.NewFromString(request.Config.PortfolioSettings.Leverage.MaximumOrdersWithLeverageRatio)
if err != nil {
return nil, err
}
maximumOrderLeverageRate, err := decimal.NewFromString(request.Config.PortfolioSettings.Leverage.MaximumLeverageRate)
if err != nil {
return nil, err
}
maximumCollateralLeverageRate, err := decimal.NewFromString(request.Config.PortfolioSettings.Leverage.MaximumCollateralLeverageRate)
if err != nil {
return nil, err
}
buySideMinimumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.BuySide.MinimumSize)
if err != nil {
return nil, err
}
buySideMaximumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.BuySide.MaximumSize)
if err != nil {
return nil, err
}
buySideMaximumTotal, err := decimal.NewFromString(request.Config.PortfolioSettings.BuySide.MaximumTotal)
if err != nil {
return nil, err
}
sellSideMinimumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.SellSide.MinimumSize)
if err != nil {
return nil, err
}
sellSideMaximumSize, err := decimal.NewFromString(request.Config.PortfolioSettings.SellSide.MaximumSize)
if err != nil {
return nil, err
}
sellSideMaximumTotal, err := decimal.NewFromString(request.Config.PortfolioSettings.SellSide.MaximumTotal)
if err != nil {
return nil, err
}
fundingSettings := make([]config.ExchangeLevelFunding, len(request.Config.FundingSettings.ExchangeLevelFunding))
for i := range request.Config.FundingSettings.ExchangeLevelFunding {
var initialFunds, transferFee decimal.Decimal
var a asset.Item
initialFunds, err = decimal.NewFromString(request.Config.FundingSettings.ExchangeLevelFunding[i].InitialFunds)
if err != nil {
return nil, err
}
transferFee, err = decimal.NewFromString(request.Config.FundingSettings.ExchangeLevelFunding[i].TransferFee)
if err != nil {
return nil, err
}
a, err = asset.New(request.Config.FundingSettings.ExchangeLevelFunding[i].Asset)
if err != nil {
return nil, err
}
fundingSettings[i] = config.ExchangeLevelFunding{
ExchangeName: request.Config.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
Asset: a,
Currency: currency.NewCode(request.Config.FundingSettings.ExchangeLevelFunding[i].Currency),
InitialFunds: initialFunds,
TransferFee: transferFee,
}
}
customSettings := make(map[string]interface{}, len(request.Config.StrategySettings.CustomSettings))
for i := range request.Config.StrategySettings.CustomSettings {
customSettings[request.Config.StrategySettings.CustomSettings[i].KeyField] = request.Config.StrategySettings.CustomSettings[i].KeyValue
}
configSettings := make([]config.CurrencySettings, len(request.Config.CurrencySettings))
for i := range request.Config.CurrencySettings {
var currencySettingBuySideMinimumSize, currencySettingBuySideMaximumSize,
currencySettingBuySideMaximumTotal, currencySettingSellSideMinimumSize,
currencySettingSellSideMaximumSize, currencySettingSellSideMaximumTotal,
minimumSlippagePercent, maximumSlippagePercent, maximumHoldingsRatio decimal.Decimal
var a asset.Item
currencySettingBuySideMinimumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].BuySide.MinimumSize)
if err != nil {
return nil, err
}
currencySettingBuySideMaximumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].BuySide.MaximumSize)
if err != nil {
return nil, err
}
currencySettingBuySideMaximumTotal, err = decimal.NewFromString(request.Config.CurrencySettings[i].BuySide.MaximumTotal)
if err != nil {
return nil, err
}
currencySettingSellSideMinimumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].SellSide.MinimumSize)
if err != nil {
return nil, err
}
currencySettingSellSideMaximumSize, err = decimal.NewFromString(request.Config.CurrencySettings[i].SellSide.MaximumSize)
if err != nil {
return nil, err
}
currencySettingSellSideMaximumTotal, err = decimal.NewFromString(request.Config.CurrencySettings[i].SellSide.MaximumTotal)
if err != nil {
return nil, err
}
minimumSlippagePercent, err = decimal.NewFromString(request.Config.CurrencySettings[i].MinSlippagePercent)
if err != nil {
return nil, err
}
maximumSlippagePercent, err = decimal.NewFromString(request.Config.CurrencySettings[i].MaxSlippagePercent)
if err != nil {
return nil, err
}
maximumHoldingsRatio, err = decimal.NewFromString(request.Config.CurrencySettings[i].MaximumHoldingsRatio)
if err != nil {
return nil, err
}
a, err = asset.New(request.Config.CurrencySettings[i].Asset)
if err != nil {
return nil, err
}
var maker, taker *decimal.Decimal
if request.Config.CurrencySettings[i].MakerFeeOverride != "" {
// nil is a valid option
var m decimal.Decimal
m, err = decimal.NewFromString(request.Config.CurrencySettings[i].MakerFeeOverride)
if err != nil {
return nil, fmt.Errorf("%v %v %v-%v maker fee %w", request.Config.CurrencySettings[i].ExchangeName, request.Config.CurrencySettings[i].Asset, request.Config.CurrencySettings[i].Base, request.Config.CurrencySettings[i].Quote, err)
}
maker = &m
}
if request.Config.CurrencySettings[i].TakerFeeOverride != "" {
// nil is a valid option
var t decimal.Decimal
t, err = decimal.NewFromString(request.Config.CurrencySettings[i].MakerFeeOverride)
if err != nil {
return nil, fmt.Errorf("%v %v %v-%v taker fee %w", request.Config.CurrencySettings[i].ExchangeName, request.Config.CurrencySettings[i].Asset, request.Config.CurrencySettings[i].Base, request.Config.CurrencySettings[i].Quote, err)
}
taker = &t
}
var spotDetails *config.SpotDetails
if request.Config.CurrencySettings[i].SpotDetails != nil {
spotDetails = &config.SpotDetails{}
if request.Config.CurrencySettings[i].SpotDetails.InitialBaseFunds != "" {
var ibf decimal.Decimal
ibf, err = decimal.NewFromString(request.Config.CurrencySettings[i].SpotDetails.InitialBaseFunds)
if err != nil {
return nil, err
}
spotDetails.InitialBaseFunds = &ibf
}
if request.Config.CurrencySettings[i].SpotDetails.InitialQuoteFunds != "" {
var iqf decimal.Decimal
iqf, err = decimal.NewFromString(request.Config.CurrencySettings[i].SpotDetails.InitialQuoteFunds)
if err != nil {
return nil, err
}
spotDetails.InitialQuoteFunds = &iqf
}
}
var futuresDetails *config.FuturesDetails
if request.Config.CurrencySettings[i].FuturesDetails != nil &&
request.Config.CurrencySettings[i].FuturesDetails.Leverage != nil {
futuresDetails = &config.FuturesDetails{}
var mowlr, mlr, mclr decimal.Decimal
mowlr, err = decimal.NewFromString(request.Config.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio)
if err != nil {
return nil, err
}
mlr, err = decimal.NewFromString(request.Config.CurrencySettings[i].FuturesDetails.Leverage.MaximumLeverageRate)
if err != nil {
return nil, err
}
mclr, err = decimal.NewFromString(request.Config.CurrencySettings[i].FuturesDetails.Leverage.MaximumCollateralLeverageRate)
if err != nil {
return nil, err
}
futuresDetails.Leverage = config.Leverage{
CanUseLeverage: request.Config.CurrencySettings[i].FuturesDetails.Leverage.CanUseLeverage,
MaximumOrdersWithLeverageRatio: mowlr,
MaximumOrderLeverageRate: mlr,
MaximumCollateralLeverageRate: mclr,
}
}
configSettings[i] = config.CurrencySettings{
ExchangeName: request.Config.CurrencySettings[i].ExchangeName,
Asset: a,
Base: currency.NewCode(request.Config.CurrencySettings[i].Base),
Quote: currency.NewCode(request.Config.CurrencySettings[i].Quote),
SpotDetails: spotDetails,
FuturesDetails: futuresDetails,
BuySide: config.MinMax{
MinimumSize: currencySettingBuySideMinimumSize,
MaximumSize: currencySettingBuySideMaximumSize,
MaximumTotal: currencySettingBuySideMaximumTotal,
},
SellSide: config.MinMax{
MinimumSize: currencySettingSellSideMinimumSize,
MaximumSize: currencySettingSellSideMaximumSize,
MaximumTotal: currencySettingSellSideMaximumTotal,
},
MinimumSlippagePercent: minimumSlippagePercent,
MaximumSlippagePercent: maximumSlippagePercent,
MakerFee: maker,
TakerFee: taker,
MaximumHoldingsRatio: maximumHoldingsRatio,
SkipCandleVolumeFitting: request.Config.CurrencySettings[i].SkipCandleVolumeFitting,
CanUseExchangeLimits: request.Config.CurrencySettings[i].UseExchangeOrderLimits,
ShowExchangeOrderLimitWarning: request.Config.CurrencySettings[i].UseExchangeOrderLimits,
UseExchangePNLCalculation: request.Config.CurrencySettings[i].UseExchangePnlCalculation,
}
}
var apiData *config.APIData
if request.Config.DataSettings.ApiData != nil {
apiData = &config.APIData{
StartDate: request.Config.DataSettings.ApiData.StartDate.AsTime(),
EndDate: request.Config.DataSettings.ApiData.EndDate.AsTime(),
InclusiveEndDate: request.Config.DataSettings.ApiData.InclusiveEndDate,
}
}
var dbData *config.DatabaseData
if request.Config.DataSettings.DatabaseData != nil {
if request.Config.DataSettings.DatabaseData.Config.Config.Port > math.MaxUint16 {
return nil, fmt.Errorf("%w '%v' cannot exceed '%v'", errBadPort, request.Config.DataSettings.DatabaseData.Config.Config.Port, math.MaxUint16)
}
cfg := database.Config{
Enabled: request.Config.DataSettings.DatabaseData.Config.Enabled,
Verbose: request.Config.DataSettings.DatabaseData.Config.Verbose,
Driver: request.Config.DataSettings.DatabaseData.Config.Driver,
ConnectionDetails: drivers.ConnectionDetails{
Host: request.Config.DataSettings.DatabaseData.Config.Config.Host,
Port: uint16(request.Config.DataSettings.DatabaseData.Config.Config.Port),
Username: request.Config.DataSettings.DatabaseData.Config.Config.UserName,
Password: request.Config.DataSettings.DatabaseData.Config.Config.Password,
Database: request.Config.DataSettings.DatabaseData.Config.Config.Database,
SSLMode: request.Config.DataSettings.DatabaseData.Config.Config.SslMode,
},
}
dbData = &config.DatabaseData{
StartDate: request.Config.DataSettings.DatabaseData.StartDate.AsTime(),
EndDate: request.Config.DataSettings.DatabaseData.EndDate.AsTime(),
Path: request.Config.DataSettings.DatabaseData.Path,
Config: cfg,
InclusiveEndDate: request.Config.DataSettings.DatabaseData.InclusiveEndDate,
}
}
var liveData *config.LiveData
if request.Config.DataSettings.LiveData != nil {
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,
}
}
var csvData *config.CSVData
if request.Config.DataSettings.CsvData != nil {
csvData = &config.CSVData{
FullPath: request.Config.DataSettings.CsvData.Path,
}
}
cfg := &config.Config{
Nickname: request.Config.Nickname,
Goal: request.Config.Goal,
StrategySettings: config.StrategySettings{
Name: request.Config.StrategySettings.Name,
SimultaneousSignalProcessing: request.Config.StrategySettings.UseSimultaneousSignalProcessing,
DisableUSDTracking: request.Config.StrategySettings.DisableUsdTracking,
CustomSettings: customSettings,
},
FundingSettings: config.FundingSettings{
UseExchangeLevelFunding: request.Config.FundingSettings.UseExchangeLevelFunding,
ExchangeLevelFunding: fundingSettings,
},
CurrencySettings: configSettings,
DataSettings: config.DataSettings{
Interval: gctkline.Interval(request.Config.DataSettings.Interval),
DataType: request.Config.DataSettings.Datatype,
APIData: apiData,
DatabaseData: dbData,
LiveData: liveData,
CSVData: csvData,
},
PortfolioSettings: config.PortfolioSettings{
Leverage: config.Leverage{
CanUseLeverage: request.Config.PortfolioSettings.Leverage.CanUseLeverage,
MaximumOrdersWithLeverageRatio: maximumOrdersWithLeverageRatio,
MaximumOrderLeverageRate: maximumOrderLeverageRate,
MaximumCollateralLeverageRate: maximumCollateralLeverageRate,
},
BuySide: config.MinMax{
MinimumSize: buySideMinimumSize,
MaximumSize: buySideMaximumSize,
MaximumTotal: buySideMaximumTotal,
},
SellSide: config.MinMax{
MinimumSize: sellSideMinimumSize,
MaximumSize: sellSideMaximumSize,
MaximumTotal: sellSideMaximumTotal,
},
},
StatisticSettings: config.StatisticSettings{
RiskFreeRate: rfr,
},
}
err = ExecuteStrategy(cfg, s.BacktesterConfig)
if err != nil {
return nil, err
}
return &btrpc.ExecuteStrategyResponse{
Success: true,
}, nil
}

View File

@@ -0,0 +1,44 @@
# GoCryptoTrader Backtester: Grpcserver 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/engine/grpcserver)
[![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 grpcserver 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)
## Grpcserver package overview
The GRPC server is responsible for handling requests from the client. All GRPC functionality as defined in the proto file is implemented [here](/backtester/btrpc)
### 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,285 @@
package engine
import (
"context"
"errors"
"fmt"
"path/filepath"
"testing"
"time"
"github.com/thrasher-corp/gocryptotrader/backtester/btrpc"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/config"
"google.golang.org/protobuf/types/known/timestamppb"
)
var dcaConfigPath = filepath.Join("..", "config", "strategyexamples", "dca-api-candles.strat")
func TestExecuteStrategyFromFile(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.ExecuteStrategyFromFile(context.Background(), nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received '%v' expecting '%v'", err, common.ErrNilArguments)
}
_, err = s.ExecuteStrategyFromFile(context.Background(), &btrpc.ExecuteStrategyFromFileRequest{})
if !errors.Is(err, common.ErrFileNotFound) {
t.Errorf("received '%v' expecting '%v'", err, common.ErrFileNotFound)
}
_, err = s.ExecuteStrategyFromFile(context.Background(), &btrpc.ExecuteStrategyFromFileRequest{
StrategyFilePath: dcaConfigPath,
})
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received '%v' expecting '%v'", err, common.ErrNilArguments)
}
s.BacktesterConfig = &config.BacktesterConfig{}
_, err = s.ExecuteStrategyFromFile(context.Background(), &btrpc.ExecuteStrategyFromFileRequest{
StrategyFilePath: dcaConfigPath,
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
}
func TestExecuteStrategyFromConfig(t *testing.T) {
t.Parallel()
s := &GRPCServer{}
_, err := s.ExecuteStrategyFromConfig(context.Background(), nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received '%v' expecting '%v'", err, common.ErrNilArguments)
}
s.BacktesterConfig = &config.BacktesterConfig{}
_, err = s.ExecuteStrategyFromConfig(context.Background(), &btrpc.ExecuteStrategyFromConfigRequest{})
if !errors.Is(err, common.ErrNilArguments) {
t.Errorf("received '%v' expecting '%v'", err, common.ErrNilArguments)
}
defaultConfig, err := config.ReadStrategyConfigFromFile(dcaConfigPath)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
customSettings := make([]*btrpc.CustomSettings, len(defaultConfig.StrategySettings.CustomSettings))
x := 0
for k, v := range defaultConfig.StrategySettings.CustomSettings {
customSettings[x] = &btrpc.CustomSettings{
KeyField: k,
KeyValue: fmt.Sprintf("%v", v),
}
x++
}
currencySettings := make([]*btrpc.CurrencySettings, len(defaultConfig.CurrencySettings))
for i := range defaultConfig.CurrencySettings {
var sd *btrpc.SpotDetails
if defaultConfig.CurrencySettings[i].SpotDetails != nil {
if defaultConfig.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
sd = &btrpc.SpotDetails{
InitialBaseFunds: defaultConfig.CurrencySettings[i].SpotDetails.InitialBaseFunds.String(),
}
}
if defaultConfig.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil {
if sd == nil {
sd = &btrpc.SpotDetails{}
}
sd.InitialQuoteFunds = defaultConfig.CurrencySettings[i].SpotDetails.InitialQuoteFunds.String()
}
}
var fd *btrpc.FuturesDetails
if defaultConfig.CurrencySettings[i].FuturesDetails != nil {
fd.Leverage = &btrpc.Leverage{
CanUseLeverage: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.CanUseLeverage,
MaximumOrdersWithLeverageRatio: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio.String(),
MaximumLeverageRate: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate.String(),
MaximumCollateralLeverageRate: defaultConfig.CurrencySettings[i].FuturesDetails.Leverage.MaximumCollateralLeverageRate.String(),
}
}
var makerFee, takerFee string
if defaultConfig.CurrencySettings[i].MakerFee != nil {
makerFee = defaultConfig.CurrencySettings[i].MakerFee.String()
}
if defaultConfig.CurrencySettings[i].TakerFee != nil {
takerFee = defaultConfig.CurrencySettings[i].TakerFee.String()
}
currencySettings[i] = &btrpc.CurrencySettings{
ExchangeName: defaultConfig.CurrencySettings[i].ExchangeName,
Asset: defaultConfig.CurrencySettings[i].Asset.String(),
Base: defaultConfig.CurrencySettings[i].Base.String(),
Quote: defaultConfig.CurrencySettings[i].Quote.String(),
BuySide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.CurrencySettings[i].BuySide.MinimumSize.String(),
MaximumSize: defaultConfig.CurrencySettings[i].BuySide.MaximumSize.String(),
MaximumTotal: defaultConfig.CurrencySettings[i].BuySide.MaximumTotal.String(),
},
SellSide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.CurrencySettings[i].SellSide.MinimumSize.String(),
MaximumSize: defaultConfig.CurrencySettings[i].SellSide.MaximumSize.String(),
MaximumTotal: defaultConfig.CurrencySettings[i].SellSide.MaximumTotal.String(),
},
MinSlippagePercent: defaultConfig.CurrencySettings[i].MinimumSlippagePercent.String(),
MaxSlippagePercent: defaultConfig.CurrencySettings[i].MaximumSlippagePercent.String(),
MakerFeeOverride: makerFee,
TakerFeeOverride: takerFee,
MaximumHoldingsRatio: defaultConfig.CurrencySettings[i].MaximumHoldingsRatio.String(),
SkipCandleVolumeFitting: defaultConfig.CurrencySettings[i].SkipCandleVolumeFitting,
UseExchangeOrderLimits: defaultConfig.CurrencySettings[i].CanUseExchangeLimits,
UseExchangePnlCalculation: defaultConfig.CurrencySettings[i].UseExchangePNLCalculation,
SpotDetails: sd,
FuturesDetails: fd,
}
}
exchangeLevelFunding := make([]*btrpc.ExchangeLevelFunding, len(defaultConfig.FundingSettings.ExchangeLevelFunding))
for i := range defaultConfig.FundingSettings.ExchangeLevelFunding {
exchangeLevelFunding[i] = &btrpc.ExchangeLevelFunding{
ExchangeName: defaultConfig.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
Asset: defaultConfig.FundingSettings.ExchangeLevelFunding[i].Asset.String(),
Currency: defaultConfig.FundingSettings.ExchangeLevelFunding[i].Currency.String(),
InitialFunds: defaultConfig.FundingSettings.ExchangeLevelFunding[i].InitialFunds.String(),
TransferFee: defaultConfig.FundingSettings.ExchangeLevelFunding[i].TransferFee.String(),
}
}
dataSettings := &btrpc.DataSettings{
Interval: uint64(defaultConfig.DataSettings.Interval.Duration().Nanoseconds()),
Datatype: defaultConfig.DataSettings.DataType,
}
if defaultConfig.DataSettings.APIData != nil {
dataSettings.ApiData = &btrpc.ApiData{
StartDate: timestamppb.New(defaultConfig.DataSettings.APIData.StartDate),
EndDate: timestamppb.New(defaultConfig.DataSettings.APIData.EndDate),
InclusiveEndDate: defaultConfig.DataSettings.APIData.InclusiveEndDate,
}
}
if defaultConfig.DataSettings.LiveData != nil {
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,
}
}
if defaultConfig.DataSettings.CSVData != nil {
dataSettings.CsvData = &btrpc.CSVData{
Path: defaultConfig.DataSettings.CSVData.FullPath,
}
}
if defaultConfig.DataSettings.DatabaseData != nil {
dbConnectionDetails := &btrpc.DatabaseConnectionDetails{
Host: defaultConfig.DataSettings.DatabaseData.Config.Host,
Port: uint32(defaultConfig.DataSettings.DatabaseData.Config.Port),
Password: defaultConfig.DataSettings.DatabaseData.Config.Password,
Database: defaultConfig.DataSettings.DatabaseData.Config.Database,
SslMode: defaultConfig.DataSettings.DatabaseData.Config.SSLMode,
UserName: defaultConfig.DataSettings.DatabaseData.Config.Username,
}
dbConfig := &btrpc.DatabaseConfig{
Enabled: false,
Verbose: false,
Driver: "",
Config: dbConnectionDetails,
}
dataSettings.DatabaseData = &btrpc.DatabaseData{
StartDate: timestamppb.New(defaultConfig.DataSettings.DatabaseData.StartDate),
EndDate: timestamppb.New(defaultConfig.DataSettings.DatabaseData.EndDate),
Config: dbConfig,
Path: defaultConfig.DataSettings.DatabaseData.Path,
InclusiveEndDate: defaultConfig.DataSettings.DatabaseData.InclusiveEndDate,
}
}
cfg := &btrpc.Config{
Nickname: defaultConfig.Nickname,
Goal: defaultConfig.Goal,
StrategySettings: &btrpc.StrategySettings{
Name: defaultConfig.StrategySettings.Name,
UseSimultaneousSignalProcessing: defaultConfig.StrategySettings.SimultaneousSignalProcessing,
DisableUsdTracking: defaultConfig.StrategySettings.DisableUSDTracking,
CustomSettings: customSettings,
},
FundingSettings: &btrpc.FundingSettings{
UseExchangeLevelFunding: defaultConfig.FundingSettings.UseExchangeLevelFunding,
ExchangeLevelFunding: exchangeLevelFunding,
},
CurrencySettings: currencySettings,
DataSettings: dataSettings,
PortfolioSettings: &btrpc.PortfolioSettings{
Leverage: &btrpc.Leverage{
CanUseLeverage: defaultConfig.PortfolioSettings.Leverage.CanUseLeverage,
MaximumOrdersWithLeverageRatio: defaultConfig.PortfolioSettings.Leverage.MaximumOrdersWithLeverageRatio.String(),
MaximumLeverageRate: defaultConfig.PortfolioSettings.Leverage.MaximumOrderLeverageRate.String(),
MaximumCollateralLeverageRate: defaultConfig.PortfolioSettings.Leverage.MaximumCollateralLeverageRate.String(),
},
BuySide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.PortfolioSettings.BuySide.MinimumSize.String(),
MaximumSize: defaultConfig.PortfolioSettings.BuySide.MaximumSize.String(),
MaximumTotal: defaultConfig.PortfolioSettings.BuySide.MaximumTotal.String(),
},
SellSide: &btrpc.PurchaseSide{
MinimumSize: defaultConfig.PortfolioSettings.SellSide.MinimumSize.String(),
MaximumSize: defaultConfig.PortfolioSettings.SellSide.MaximumSize.String(),
MaximumTotal: defaultConfig.PortfolioSettings.SellSide.MaximumTotal.String(),
},
},
StatisticSettings: &btrpc.StatisticSettings{
RiskFreeRate: defaultConfig.StatisticSettings.RiskFreeRate.String(),
},
}
_, err = s.ExecuteStrategyFromConfig(context.Background(), &btrpc.ExecuteStrategyFromConfigRequest{
Config: cfg,
})
if !errors.Is(err, nil) {
t.Errorf("received '%v' expecting '%v'", err, nil)
}
// coverage test to ensure the rest of the config can successfully be converted
// this will not have a successful response
cfg.FundingSettings.UseExchangeLevelFunding = true
cfg.StrategySettings.UseSimultaneousSignalProcessing = true
cfg.FundingSettings.ExchangeLevelFunding = append(cfg.FundingSettings.ExchangeLevelFunding, &btrpc.ExchangeLevelFunding{
ExchangeName: defaultConfig.CurrencySettings[0].ExchangeName,
Asset: defaultConfig.CurrencySettings[0].Asset.String(),
Currency: defaultConfig.CurrencySettings[0].Base.String(),
InitialFunds: "1337",
TransferFee: "1337",
})
cfg.CurrencySettings[0].FuturesDetails = &btrpc.FuturesDetails{Leverage: &btrpc.Leverage{
CanUseLeverage: false,
MaximumOrdersWithLeverageRatio: "1337",
MaximumLeverageRate: "1337",
MaximumCollateralLeverageRate: "1337",
}}
cfg.DataSettings.DatabaseData = &btrpc.DatabaseData{
StartDate: timestamppb.New(time.Now()),
EndDate: timestamppb.New(time.Now()),
Config: &btrpc.DatabaseConfig{
Enabled: false,
Verbose: false,
Driver: "",
Config: &btrpc.DatabaseConnectionDetails{},
},
Path: "test",
InclusiveEndDate: false,
}
cfg.DataSettings.LiveData = &btrpc.LiveData{}
cfg.DataSettings.CsvData = &btrpc.CSVData{
Path: "test",
}
for i := range cfg.CurrencySettings {
cfg.CurrencySettings[i].SpotDetails.InitialQuoteFunds = ""
cfg.CurrencySettings[i].SpotDetails.InitialBaseFunds = ""
}
_, err = s.ExecuteStrategyFromConfig(context.Background(), &btrpc.ExecuteStrategyFromConfigRequest{
Config: cfg,
})
if err == nil {
t.Error("expected an error from a bad setup")
}
}

View File

@@ -20,7 +20,7 @@ import (
// 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")
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
@@ -99,7 +99,7 @@ func (bt *BackTest) loadLiveDataLoop(resp *kline.DataFromKline, cfg *config.Conf
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)
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)
if err != nil {
@@ -134,6 +134,6 @@ func (bt *BackTest) loadLiveData(resp *kline.DataFromKline, cfg *config.Config,
}
resp.AppendResults(candles)
bt.Reports.UpdateItem(&resp.Item)
log.Info(common.Backtester, "sleeping for 30 seconds before checking for new candle data")
log.Info(common.Backtester, "Sleeping for 30 seconds before checking for new candle data")
return nil
}

51
backtester/engine/live.md Normal file
View File

@@ -0,0 +1,51 @@
# GoCryptoTrader Backtester: Live 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/engine/live)
[![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 live 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)
## Live package overview
Live trading has specific requirements separate from backtesting. Handling the looping of candle data and managing real orders and orderbooks will be handled here
Live trading is only a proof of concept. Please do not risk your funds by using it with `realOrders` enabled
A flow of the application is as follows:
![workflow](https://i.imgur.com/Kup6IA9.png)
### 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,60 @@
package engine
import (
"errors"
"testing"
"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"
)
func TestLoadLiveData(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,
},
},
}
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)
}
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)
}
}

View File

@@ -20,6 +20,7 @@ import (
"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"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/risk"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
@@ -39,11 +40,12 @@ import (
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/signaler"
)
// 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...")
log.Infoln(common.Setup, "Loading config...")
if cfg == nil {
return nil, errNilConfig
}
@@ -232,7 +234,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
if cfg.CurrencySettings[i].MakerFee != nil &&
cfg.CurrencySettings[i].TakerFee != nil &&
cfg.CurrencySettings[i].MakerFee.GreaterThan(*cfg.CurrencySettings[i].TakerFee) {
log.Warnf(common.Setup, "maker fee '%v' should not exceed taker fee '%v'. Please review config",
log.Warnf(common.Setup, "Maker fee '%v' should not exceed taker fee '%v'. Please review config",
cfg.CurrencySettings[i].MakerFee,
cfg.CurrencySettings[i].TakerFee)
}
@@ -414,13 +416,25 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool
}
bt.Portfolio = p
hasFunding := false
fundingItems := funds.GetAllFunding()
for i := range fundingItems {
if fundingItems[i].InitialFunds.IsPositive() {
hasFunding = true
break
}
}
if !hasFunding {
return nil, holdings.ErrInitialFundsZero
}
cfg.PrintSetting()
return bt, nil
}
func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange, error) {
log.Infoln(common.Setup, "setting exchange settings...")
log.Infoln(common.Setup, "Setting exchange settings...")
resp := exchange.Exchange{}
for i := range cfg.CurrencySettings {
@@ -476,7 +490,7 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
}
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(decimal.Zero) {
log.Warnf(common.Setup, "invalid maximum slippage percent '%v'. Slippage percent is defined as a number, eg '100.00', defaulting to '%v'",
log.Warnf(common.Setup, "Invalid maximum slippage percent '%v'. Slippage percent is defined as a number, eg '100.00', defaulting to '%v'",
cfg.CurrencySettings[i].MaximumSlippagePercent,
slippage.DefaultMaximumSlippagePercent)
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
@@ -485,7 +499,7 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
}
if cfg.CurrencySettings[i].MinimumSlippagePercent.LessThan(decimal.Zero) {
log.Warnf(common.Setup, "invalid minimum slippage percent '%v'. Slippage percent is defined as a number, eg '80.00', defaulting to '%v'",
log.Warnf(common.Setup, "Invalid minimum slippage percent '%v'. Slippage percent is defined as a number, eg '80.00', defaulting to '%v'",
cfg.CurrencySettings[i].MinimumSlippagePercent,
slippage.DefaultMinimumSlippagePercent)
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
@@ -520,7 +534,7 @@ 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",
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)
@@ -568,7 +582,7 @@ func (bt *BackTest) loadExchangePairAssetBase(exch string, base, quote currency.
exchangeBase := e.GetBase()
if exchangeBase.ValidateAPICredentials(exchangeBase.GetDefaultCredentials()) != nil {
log.Warnf(common.Setup, "no credentials set for %v, this is theoretical only", exchangeBase.Name)
log.Warnf(common.Setup, "No credentials set for %v, this is theoretical only", exchangeBase.Name)
}
fPair, err = exchangeBase.FormatExchangeCurrency(cp, ai)
@@ -633,7 +647,7 @@ func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange,
return nil, err
}
log.Infof(common.Setup, "loading data for %v %v %v...\n", exch.GetName(), a, fPair)
log.Infof(common.Setup, "Loading data for %v %v %v...\n", exch.GetName(), a, fPair)
resp := &kline.DataFromKline{}
switch {
case cfg.DataSettings.CSVData != nil:
@@ -860,8 +874,51 @@ func loadLiveData(cfg *config.Config, base *gctexchange.Base) error {
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")
log.Warn(common.Setup, "Invalid API credentials set, real orders set to false")
cfg.DataSettings.LiveData.RealOrders = false
}
return nil
}
// ExecuteStrategy executes the strategy using the provided configs
func ExecuteStrategy(strategyCfg *config.Config, backtesterCfg *config.BacktesterConfig) error {
if err := strategyCfg.Validate(); err != nil {
return err
}
if backtesterCfg == nil {
err := fmt.Errorf("%w backtester config", common.ErrNilArguments)
return err
}
bt, err := NewFromConfig(strategyCfg, backtesterCfg.Report.TemplatePath, backtesterCfg.Report.OutputPath, backtesterCfg.Verbose)
if err != nil {
return err
}
if strategyCfg.DataSettings.LiveData != nil {
go func() {
err = bt.RunLive()
if err != nil {
log.Error(log.Global, err)
return
}
}()
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 {
return err
}
if backtesterCfg.Report.GenerateReport {
bt.Reports.UseDarkMode(backtesterCfg.Report.DarkMode)
err = bt.Reports.GenerateReport()
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,369 @@
package engine
import (
"errors"
"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/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{},
}
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{},
}
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{},
}
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{},
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()
}

View File

@@ -11,7 +11,7 @@ import (
)
// ErrInitialFundsZero is an error when initial funds are zero or less
var ErrInitialFundsZero = errors.New("initial funds < 0")
var ErrInitialFundsZero = errors.New("initial funds <= 0")
// Holding contains pricing statistics for a given time
// for a given exchange asset pair

View File

@@ -31,13 +31,13 @@ func addReason(reason, msg string) string {
// PrintTotalResults outputs all results to the CMD
func (s *Statistic) PrintTotalResults() {
log.Info(common.Statistics, common.ColourH1+"------------------Strategy-----------------------------------"+common.ColourDefault)
log.Info(common.Statistics, common.CMDColours.H1+"------------------Strategy-----------------------------------"+common.CMDColours.Default)
log.Infof(common.Statistics, "Strategy Name: %v", s.StrategyName)
log.Infof(common.Statistics, "Strategy Nickname: %v", s.StrategyNickname)
log.Infof(common.Statistics, "Strategy Goal: %v\n\n", s.StrategyGoal)
log.Info(common.Statistics, common.ColourH2+"------------------Total Results------------------------------"+common.ColourDefault)
log.Info(common.Statistics, common.ColourH3+"------------------Orders-------------------------------------"+common.ColourDefault)
log.Info(common.Statistics, common.CMDColours.H2+"------------------Total Results------------------------------"+common.CMDColours.Default)
log.Info(common.Statistics, common.CMDColours.H3+"------------------Orders-------------------------------------"+common.CMDColours.Default)
log.Infof(common.Statistics, "Total buy orders: %v", convert.IntToHumanFriendlyString(s.TotalBuyOrders, ","))
log.Infof(common.Statistics, "Total sell orders: %v", convert.IntToHumanFriendlyString(s.TotalSellOrders, ","))
log.Infof(common.Statistics, "Total long orders: %v", convert.IntToHumanFriendlyString(s.TotalLongOrders, ","))
@@ -45,7 +45,7 @@ func (s *Statistic) PrintTotalResults() {
log.Infof(common.Statistics, "Total orders: %v\n\n", convert.IntToHumanFriendlyString(s.TotalOrders, ","))
if s.BiggestDrawdown != nil {
log.Info(common.Statistics, common.ColourH3+"------------------Biggest Drawdown-----------------------"+common.ColourDefault)
log.Info(common.Statistics, common.CMDColours.H3+"------------------Biggest Drawdown-----------------------"+common.CMDColours.Default)
log.Infof(common.Statistics, "Exchange: %v Asset: %v Currency: %v", s.BiggestDrawdown.Exchange, s.BiggestDrawdown.Asset, s.BiggestDrawdown.Pair)
log.Infof(common.Statistics, "Highest Price: %s", convert.DecimalToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.Highest.Value, 8, ".", ","))
log.Infof(common.Statistics, "Highest Price Time: %v", s.BiggestDrawdown.MaxDrawdown.Highest.Time)
@@ -56,7 +56,7 @@ func (s *Statistic) PrintTotalResults() {
log.Infof(common.Statistics, "Drawdown length: %v candles\n\n", convert.IntToHumanFriendlyString(s.BiggestDrawdown.MaxDrawdown.IntervalDuration, ","))
}
if s.BestMarketMovement != nil && s.BestStrategyResults != nil {
log.Info(common.Statistics, common.ColourH4+"------------------Orders----------------------------------"+common.ColourDefault)
log.Info(common.Statistics, common.CMDColours.H4+"------------------Orders----------------------------------"+common.CMDColours.Default)
log.Infof(common.Statistics, "Best performing market movement: %v %v %v %v%%", s.BestMarketMovement.Exchange, s.BestMarketMovement.Asset, s.BestMarketMovement.Pair, convert.DecimalToHumanFriendlyString(s.BestMarketMovement.MarketMovement, 2, ".", ","))
log.Infof(common.Statistics, "Best performing strategy movement: %v %v %v %v%%\n\n", s.BestStrategyResults.Exchange, s.BestStrategyResults.Asset, s.BestStrategyResults.Pair, convert.DecimalToHumanFriendlyString(s.BestStrategyResults.StrategyMovement, 2, ".", ","))
}
@@ -67,9 +67,9 @@ func (s *Statistic) PrintTotalResults() {
// grouped by time to allow a clearer picture of events
func (s *Statistic) PrintAllEventsChronologically() {
var results []eventOutputHolder
log.Info(common.Statistics, common.ColourH1+"------------------Events-------------------------------------"+common.ColourDefault)
log.Info(common.Statistics, common.CMDColours.H1+"------------------Events-------------------------------------"+common.CMDColours.Default)
var errs gctcommon.Errors
colour := common.ColourDefault
colour := common.CMDColours.Default
for exch, x := range s.ExchangeAssetPairStatistics {
for a, y := range x {
for pair, currencyStatistic := range y {
@@ -84,7 +84,7 @@ func (s *Statistic) PrintAllEventsChronologically() {
direction == order.TransferredFunds ||
direction == order.UnknownSide {
if direction == order.DoNothing {
colour = common.ColourDarkGrey
colour = common.CMDColours.DarkGrey
}
msg := fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection: %v",
@@ -95,13 +95,13 @@ func (s *Statistic) PrintAllEventsChronologically() {
currencyStatistic.Events[i].FillEvent.GetClosePrice().Round(8),
currencyStatistic.Events[i].FillEvent.GetDirection())
msg = addReason(currencyStatistic.Events[i].FillEvent.GetConcatReasons(), msg)
msg += common.ColourDefault
msg += common.CMDColours.Default
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(), msg)
} else {
// successful order!
colour = common.ColourSuccess
colour = common.CMDColours.Success
if currencyStatistic.Events[i].FillEvent.IsLiquidated() {
colour = common.ColourError
colour = common.CMDColours.Error
}
msg := fmt.Sprintf(colour+
"%v %v%v%v| Price: %v\tDirection %v\tOrder placed: Amount: %v\tFee: %v\tTotal: %v",
@@ -115,7 +115,7 @@ func (s *Statistic) PrintAllEventsChronologically() {
currencyStatistic.Events[i].FillEvent.GetExchangeFee(),
currencyStatistic.Events[i].FillEvent.GetTotal().Round(8))
msg = addReason(currencyStatistic.Events[i].FillEvent.GetConcatReasons(), msg)
msg += common.ColourDefault
msg += common.CMDColours.Default
results = addEventOutputToTime(results, currencyStatistic.Events[i].FillEvent.GetTime(), msg)
}
case currencyStatistic.Events[i].SignalEvent != nil:
@@ -126,7 +126,7 @@ func (s *Statistic) PrintAllEventsChronologically() {
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.ColourDefault
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",
@@ -136,10 +136,10 @@ func (s *Statistic) PrintAllEventsChronologically() {
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.ColourDefault
msg += common.CMDColours.Default
results = addEventOutputToTime(results, currencyStatistic.Events[i].DataEvent.GetTime(), msg)
default:
errs = append(errs, fmt.Errorf(common.ColourError+"%v%v%v unexpected data received %+v"+common.ColourDefault, exch, a, fSIL(pair.String(), limit14), currencyStatistic.Events[i]))
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]))
}
}
}
@@ -157,7 +157,7 @@ func (s *Statistic) PrintAllEventsChronologically() {
}
}
if len(errs) > 0 {
log.Info(common.Statistics, common.ColourError+"------------------Errors-------------------------------------"+common.ColourDefault)
log.Info(common.Statistics, common.CMDColours.Error+"------------------Errors-------------------------------------"+common.CMDColours.Default)
for i := range errs {
log.Error(common.Statistics, errs[i].Error())
}
@@ -179,7 +179,7 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
c.TotalOrders = c.BuyOrders + c.SellOrders + c.ShortOrders + c.LongOrders
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.ColourH1+"------------------Stats for %v %v %v------------------------------------------------------"+common.ColourDefault, e, a, p)
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, ","))
@@ -199,17 +199,17 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
log.Infof(common.CurrencyStatistics, "%s Total orders: %s", sep, convert.IntToHumanFriendlyString(c.TotalOrders, ","))
log.Info(common.CurrencyStatistics, common.ColourH2+"------------------Max Drawdown-------------------------------"+common.ColourDefault)
log.Info(common.CurrencyStatistics, common.CMDColours.H2+"------------------Max Drawdown-------------------------------"+common.CMDColours.Default)
log.Infof(common.CurrencyStatistics, "%s Highest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Highest.Value, 8, ".", ","), c.MaxDrawdown.Highest.Time)
log.Infof(common.CurrencyStatistics, "%s Lowest Price of drawdown: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.MaxDrawdown.Lowest.Value, 8, ".", ","), c.MaxDrawdown.Lowest.Time)
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 {
log.Info(common.CurrencyStatistics, common.ColourH2+"------------------Ratios------------------------------------------------"+common.ColourDefault)
log.Info(common.CurrencyStatistics, common.ColourH3+"------------------Rates-------------------------------------------------"+common.ColourDefault)
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, ".", ","))
log.Info(common.CurrencyStatistics, common.ColourH4+"------------------Arithmetic--------------------------------------------"+common.ColourDefault)
log.Info(common.CurrencyStatistics, common.CMDColours.H4+"------------------Arithmetic--------------------------------------------"+common.CMDColours.Default)
if c.ShowMissingDataWarning {
log.Infoln(common.CurrencyStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.CurrencyStatistics, "Ratio calculations will be skewed")
@@ -219,7 +219,7 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
log.Infof(common.CurrencyStatistics, "%s Information ratio: %v", sep, c.ArithmeticRatios.InformationRatio.Round(4))
log.Infof(common.CurrencyStatistics, "%s Calmar ratio: %v", sep, c.ArithmeticRatios.CalmarRatio.Round(4))
log.Info(common.CurrencyStatistics, common.ColourH4+"------------------Geometric--------------------------------------------"+common.ColourDefault)
log.Info(common.CurrencyStatistics, common.CMDColours.H4+"------------------Geometric--------------------------------------------"+common.CMDColours.Default)
if c.ShowMissingDataWarning {
log.Infoln(common.CurrencyStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.CurrencyStatistics, "Ratio calculations will be skewed")
@@ -230,7 +230,7 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
log.Infof(common.CurrencyStatistics, "%s Calmar ratio: %v", sep, c.GeometricRatios.CalmarRatio.Round(4))
}
log.Info(common.CurrencyStatistics, common.ColourH2+"------------------Results------------------------------------"+common.ColourDefault)
log.Info(common.CurrencyStatistics, common.CMDColours.H2+"------------------Results------------------------------------"+common.CMDColours.Default)
log.Infof(common.CurrencyStatistics, "%s Starting Close Price: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.StartingClosePrice.Value, 8, ".", ","), c.StartingClosePrice.Time)
log.Infof(common.CurrencyStatistics, "%s Finishing Close Price: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.EndingClosePrice.Value, 8, ".", ","), c.EndingClosePrice.Time)
log.Infof(common.CurrencyStatistics, "%s Lowest Close Price: %s at %v", sep, convert.DecimalToHumanFriendlyString(c.LowestClosePrice.Value, 8, ".", ","), c.LowestClosePrice.Time)
@@ -263,7 +263,7 @@ func (c *CurrencyPairStatistic) PrintResults(e string, a asset.Item, p currency.
log.Infof(common.CurrencyStatistics, "%s Final Realised PNL: %s", sep, convert.DecimalToHumanFriendlyString(realised.PNL, 8, ".", ","))
}
if len(errs) > 0 {
log.Info(common.CurrencyStatistics, common.ColourError+"------------------Errors-------------------------------------"+common.ColourDefault)
log.Info(common.CurrencyStatistics, common.CMDColours.Error+"------------------Errors-------------------------------------"+common.CMDColours.Default)
for i := range errs {
log.Error(common.CurrencyStatistics, errs[i].Error())
}
@@ -284,10 +284,10 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
}
}
if len(spotResults) > 0 || len(futuresResults) > 0 {
log.Info(common.FundingStatistics, common.ColourH1+"------------------Funding------------------------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.CMDColours.H1+"------------------Funding------------------------------------"+common.CMDColours.Default)
}
if len(spotResults) > 0 {
log.Info(common.FundingStatistics, common.ColourH2+"------------------Funding Spot Item Results------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.CMDColours.H2+"------------------Funding Spot Item Results------------------"+common.CMDColours.Default)
for i := range spotResults {
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() {
@@ -314,7 +314,7 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
}
}
if len(futuresResults) > 0 {
log.Info(common.FundingStatistics, common.ColourH2+"------------------Funding Futures Item Results---------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.CMDColours.H2+"------------------Funding Futures Item Results---------------"+common.CMDColours.Default)
for i := range futuresResults {
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)
@@ -340,7 +340,7 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
if f.Report.DisableUSDTracking {
return nil
}
log.Info(common.FundingStatistics, common.ColourH2+"------------------USD Tracking Totals------------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.CMDColours.H2+"------------------USD Tracking Totals------------------------"+common.CMDColours.Default)
sep := "USD Tracking Total |\t"
log.Infof(common.FundingStatistics, "%s Initial value: $%s", sep, convert.DecimalToHumanFriendlyString(f.Report.InitialFunds, 8, ".", ","))
@@ -352,14 +352,14 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
log.Infof(common.FundingStatistics, "%s Highest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.HighestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.HighestHoldingValue.Time)
log.Infof(common.FundingStatistics, "%s Lowest funds: $%s at %v", sep, convert.DecimalToHumanFriendlyString(f.TotalUSDStatistics.LowestHoldingValue.Value, 8, ".", ","), f.TotalUSDStatistics.LowestHoldingValue.Time)
log.Info(common.FundingStatistics, common.ColourH3+"------------------Ratios------------------------------------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.ColourH4+"------------------Rates-------------------------------------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.CMDColours.H3+"------------------Ratios------------------------------------------------"+common.CMDColours.Default)
log.Info(common.FundingStatistics, common.CMDColours.H4+"------------------Rates-------------------------------------------------"+common.CMDColours.Default)
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)
}
log.Info(common.FundingStatistics, common.ColourH4+"------------------Arithmetic--------------------------------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.CMDColours.H4+"------------------Arithmetic--------------------------------------------"+common.CMDColours.Default)
if wasAnyDataMissing {
log.Infoln(common.FundingStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.FundingStatistics, "Ratio calculations will be skewed")
@@ -369,7 +369,7 @@ func (f *FundingStatistics) PrintResults(wasAnyDataMissing bool) error {
log.Infof(common.FundingStatistics, "%s Information ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.InformationRatio.Round(4))
log.Infof(common.FundingStatistics, "%s Calmar ratio: %v", sep, f.TotalUSDStatistics.ArithmeticRatios.CalmarRatio.Round(4))
log.Info(common.FundingStatistics, common.ColourH4+"------------------Geometric--------------------------------------------"+common.ColourDefault)
log.Info(common.FundingStatistics, common.CMDColours.H4+"------------------Geometric--------------------------------------------"+common.CMDColours.Default)
if wasAnyDataMissing {
log.Infoln(common.FundingStatistics, "Missing data was detected during this backtesting run")
log.Infoln(common.FundingStatistics, "Ratio calculations will be skewed")

View File

@@ -182,7 +182,7 @@ func (s *Statistic) AddComplianceSnapshotForTime(c compliance.Snapshot, e fill.E
// CalculateAllResults calculates the statistics of all exchange asset pair holdings,
// orders, ratios and drawdowns
func (s *Statistic) CalculateAllResults() error {
log.Info(common.Statistics, "calculating backtesting results")
log.Info(common.Statistics, "Calculating backtesting results")
s.PrintAllEventsChronologically()
currCount := 0
var finalResults []FinalResultsHolder

View File

@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
@@ -10,13 +11,14 @@ import (
"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/common/file"
"github.com/thrasher-corp/gocryptotrader/engine"
"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
var singleRunStrategyPath, templatePath, outputPath, btConfigDir, strategyPluginPath string
var printLogo, generateReport, darkReport, colourOutput, logSubHeader bool
func main() {
wd, err := os.Getwd()
@@ -24,18 +26,96 @@ func main() {
fmt.Printf("Could not get working directory. Error: %v.\n", err)
os.Exit(1)
}
parseFlags(wd)
if !colourOutput {
flags := parseFlags(wd)
var btCfg *config.BacktesterConfig
if btConfigDir == "" {
btConfigDir = config.DefaultBTConfigDir
log.Infof(log.Global, "Blank config received, using default path '%v'", btConfigDir)
}
fe := file.Exists(btConfigDir)
switch {
case fe:
btCfg, err = config.ReadBacktesterConfigFromPath(btConfigDir)
if err != nil {
fmt.Printf("Could not read config. Error: %v.\n", err)
os.Exit(1)
}
case !fe && btConfigDir == config.DefaultBTConfigDir:
btCfg, err = config.GenerateDefaultConfig()
if err != nil {
fmt.Printf("Could not generate config. Error: %v.\n", err)
os.Exit(1)
}
var btCfgJSON []byte
btCfgJSON, err = json.MarshalIndent(btCfg, "", " ")
if err != nil {
fmt.Printf("Could not generate config. Error: %v.\n", err)
os.Exit(1)
}
err = os.MkdirAll(config.DefaultBTDir, file.DefaultPermissionOctal)
if err != nil {
fmt.Printf("Could not generate config. Error: %v.\n", err)
os.Exit(1)
}
err = os.WriteFile(btConfigDir, btCfgJSON, file.DefaultPermissionOctal)
if err != nil {
fmt.Printf("Could not generate config. Error: %v.\n", err)
os.Exit(1)
}
default:
log.Errorf(log.Global, "Non-standard config '%v' does not exist. Exiting...", btConfigDir)
return
}
flagSet := engine.FlagSet(flags)
flagSet.WithBool("printlogo", &printLogo, btCfg.PrintLogo)
flagSet.WithBool("darkreport", &darkReport, btCfg.Report.DarkMode)
flagSet.WithBool("generatereport", &generateReport, btCfg.Report.GenerateReport)
flagSet.WithBool("logsubheaders", &logSubHeader, btCfg.LogSubheaders)
flagSet.WithBool("colouroutput", &colourOutput, btCfg.UseCMDColours)
if singleRunStrategyPath != "" && !file.Exists(singleRunStrategyPath) {
fmt.Printf("Strategy config path not found '%v'", singleRunStrategyPath)
os.Exit(1)
}
defaultTemplate := filepath.Join(
wd,
"report",
"tpl.gohtml")
defaultReportOutput := filepath.Join(
wd,
"results")
if templatePath != defaultTemplate {
btCfg.Report.TemplatePath = templatePath
}
if !file.Exists(btCfg.Report.TemplatePath) {
fmt.Printf("Report template path not found '%v'", btCfg.Report.TemplatePath)
os.Exit(1)
}
if outputPath != defaultReportOutput {
btCfg.Report.OutputPath = outputPath
}
if !file.Exists(btCfg.Report.OutputPath) {
fmt.Printf("Report output path not found '%v'", btCfg.Report.OutputPath)
os.Exit(1)
}
if colourOutput {
common.SetColours(&btCfg.Colours)
} else {
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
log.GlobalLogConfig.AdvancedSettings.ShowLogSystemName = &logSubHeader
log.GlobalLogConfig.AdvancedSettings.Headers.Info = common.CMDColours.Info + "[INFO]" + common.CMDColours.Default
log.GlobalLogConfig.AdvancedSettings.Headers.Warn = common.CMDColours.Warn + "[WARN]" + common.CMDColours.Default
log.GlobalLogConfig.AdvancedSettings.Headers.Debug = common.CMDColours.Debug + "[DEBUG]" + common.CMDColours.Default
log.GlobalLogConfig.AdvancedSettings.Headers.Error = common.CMDColours.Error + "[ERROR]" + common.CMDColours.Default
err = log.SetupGlobalLogger()
if err != nil {
fmt.Printf("Could not setup global logger. Error: %v.\n", err)
@@ -48,81 +128,91 @@ func main() {
os.Exit(1)
}
if printLogo {
fmt.Println(common.Logo())
}
if strategyPluginPath == "" && btCfg.PluginPath != "" {
strategyPluginPath = btCfg.PluginPath
}
if strategyPluginPath != "" {
err = strategies.LoadCustomStrategies(strategyPluginPath)
if err != nil {
fmt.Printf("Could not load custom strategies. Error: %v.\n", err)
os.Exit(1)
}
log.Infof(common.Backtester, "Loaded plugin %v\n", strategyPluginPath)
}
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 singleRunStrategyPath != "" {
dir := singleRunStrategyPath
var cfg *config.Config
cfg, err = config.ReadStrategyConfigFromFile(dir)
if err != nil {
log.Error(log.Global, err)
fmt.Printf("Could not read strategy config. Error: %v.\n", err)
os.Exit(1)
}
err = backtest.ExecuteStrategy(cfg, &config.BacktesterConfig{
Report: config.Report{
GenerateReport: generateReport,
TemplatePath: btCfg.Report.TemplatePath,
OutputPath: btCfg.Report.OutputPath,
DarkMode: darkReport,
},
})
if err != nil {
fmt.Printf("Could not execute strategy. Error: %v.\n", err)
os.Exit(1)
}
return
}
btCfg.Report.DarkMode = darkReport
btCfg.Report.GenerateReport = generateReport
go func(c *config.BacktesterConfig) {
log.Info(log.GRPCSys, "Starting RPC server")
s := backtest.SetupRPCServer(c)
err = backtest.StartRPCServer(s)
if err != nil {
fmt.Printf("Could not start RPC server. Error: %v.\n", err)
os.Exit(1)
}
log.Info(log.GRPCSys, "Ready to receive commands")
}(btCfg)
interrupt := signaler.WaitForInterrupt()
log.Infof(log.Global, "Captured %v, shutdown requested.\n", interrupt)
log.Infoln(log.Global, "Exiting.")
}
func parseFlags(wd string) {
func parseFlags(wd string) map[string]bool {
defaultStrategy := filepath.Join(
wd,
"config",
"strategyexamples",
"dca-api-candles.strat")
defaultTemplate := filepath.Join(
wd,
"report",
"tpl.gohtml")
defaultReportOutput := filepath.Join(
wd,
"results")
flag.StringVar(
&configPath,
"configpath",
filepath.Join(
wd,
"config",
"examples",
"ftx-cash-carry.strat"),
"the config containing strategy params")
&singleRunStrategyPath,
"singlerunstrategypath",
"",
fmt.Sprintf("path to a strategy file. Will execute strategy and exit, instead of creating a GRPC server. Example %v", defaultStrategy))
flag.StringVar(
&btConfigDir,
"backtesterconfigpath",
config.DefaultBTConfigDir,
"the location of the backtester config")
flag.StringVar(
&templatePath,
"templatepath",
filepath.Join(
wd,
"report",
"tpl.gohtml"),
defaultTemplate,
"the report template to use")
flag.BoolVar(
&generateReport,
@@ -130,27 +220,15 @@ func parseFlags(wd string) {
true,
"whether to generate the report file")
flag.StringVar(
&reportOutput,
&outputPath,
"outputpath",
filepath.Join(
wd,
"results"),
defaultReportOutput,
"the path where to output results")
flag.BoolVar(
&printLogo,
"printlogo",
true,
"print out the logo to the command line, projected profits likely won't be affected if disabled")
flag.BoolVar(
&darkReport,
"darkreport",
false,
"sets the output report to use a dark theme by default")
flag.BoolVar(
&verbose,
"verbose",
false,
"if enabled, will set exchange requests to verbose for debugging purposes")
flag.BoolVar(
&colourOutput,
"colouroutput",
@@ -161,10 +239,20 @@ func parseFlags(wd string) {
"logsubheader",
true,
"displays logging subheader to track where activity originates")
flag.BoolVar(
&printLogo,
"printlogo",
true,
"shows the stunning, profit inducing logo on startup")
flag.StringVar(
&strategyPluginPath,
"strategypluginpath",
"",
"example path: "+filepath.Join(wd, "plugins", "strategies", "example", "example.so"))
flag.Parse()
// collect flags
flags := make(map[string]bool)
// Stores the set flags
flag.Visit(func(f *flag.Flag) { flags[f.Name] = true })
return flags
}

View File

@@ -18,7 +18,7 @@ import (
// GenerateReport sends final data from statistics to a template
// to create a lovely final report for someone to view
func (d *Data) GenerateReport() error {
log.Info(common.Report, "generating report")
log.Info(common.Report, "Generating report")
err := d.enhanceCandles()
if err != nil {
return err
@@ -100,7 +100,7 @@ func (d *Data) GenerateReport() error {
if err != nil {
return err
}
log.Infof(common.Report, "successfully saved report to %v", filepath.Join(d.OutputPath, fileName))
log.Infof(common.Report, "Successfully saved report to %v", filepath.Join(d.OutputPath, fileName))
return nil
}

View File

@@ -0,0 +1,17 @@
{{define "backtester btcli" -}}
{{template "backtester-header" .}}
## {{.CapitalName}} overview
This folder contains the GoCryptoTrader Backtester CMD CLI application. It can be used to interact
with the GoCryptoTrader Backtester's GRPC server and send commands to be processed server-side.
For a list of commands, you can run the following
```
go run .
```
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -0,0 +1,61 @@
{{define "backtester btrpc" -}}
{{template "backtester-header" .}}
## {{.CapitalName}} overview
The GoCryptoTrader Backtester utilises gRPC for client/server interaction. Authentication is done
by a self signed TLS cert, which only supports connections from localhost and also
through basic authorisation specified by the users config file.
The GoCryptoTrader Backtester also supports a gRPC JSON proxy service for applications which can
be toggled on or off depending on the users preference. This can be found in your config file
under `grpcProxyEnabled` `grpcProxyListenAddress`. See `btrpc.swagger.json` for endpoint definitions
## Installation
The GoCryptoTrader Backtester requires a local installation of the Google protocol buffers
compiler `protoc` v3.0.0 or above. Please install this via your local package
manager or by downloading one of the releases from the official repository:
[protoc releases](https://github.com/protocolbuffers/protobuf/releases)
Then use `go install` to download the following packages:
```bash
go install \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \
google.golang.org/protobuf/cmd/protoc-gen-go \
google.golang.org/grpc/cmd/protoc-gen-go-grpc
```
This will place the following binaries in your `$GOBIN`;
* `protoc-gen-grpc-gateway`
* `protoc-gen-openapiv2`
* `protoc-gen-go`
* `protoc-gen-go-grpc`
Make sure that your `$GOBIN` is in your `$PATH`.
### Linux / macOS / Windows
The GoCryptoTrader Backtester requires a local installation of the `buf` cli tool that tries to make Protobuf handling more easier and reliable,
after [installation](https://docs.buf.build/installation) you'll need to run:
```shell
buf mod update
```
After previous command, make necessary changes to the `rpc.proto` spec file and run the generation command:
```shell
buf generate
```
If any changes were made, ensure that the `rpc.proto` file is formatted correctly by using `buf format -w`
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -2,6 +2,65 @@
{{template "backtester-header" .}}
## {{.CapitalName}} package overview
## 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 |
### 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` |
### 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/` |
### Backtester Config Colours overview
| Key | Description | Example |
|----------|---------------------------------------------------------------------|----------------|
| Default | The colour definition for default text output |`` |
| Green | The colour definition for when green is warranted, such as the logo |`` |
| White | The colour definition for when white is warranted such as the logo |`` |
| Grey | The colour definition for grey | ``|
| DarkGrey | The colour definition for dark grey | ``|
| H1 | The colour definition for main headers | `` |
| H2 | The colour definition for sub headers | `` |
| H3 | The colour definition for sub sub headers | `` |
| H4 | The colour definition for sub sub sub headers | `` |
| Success | The colour definition for successful operations | `` |
| Info | The colour definition for when informing you of something | `` |
| Debug | The colour definition for debug output such as verbose | `` |
| Warn | The colour definition for when a warning occurs | `` |
| Error | The colour definition for when an error occurs | ``|
## Strategy Config overview
### What does the config package do?
The config package contains a set of structs which allow for the customisation of the GoCryptoTrader Backtester when running.
The GoCryptoTrader Backtester runs from reading config files (`.strat` files by default under `/examples`).
@@ -20,172 +79,172 @@ 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 |
| 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 |
| StatisticSettings | 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` |
| 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` |
#### Funding Config Settings
| Key | Description | Example |
| --- | ------- | --- |
| 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 | `[]` |
| ExchangeLevelFunding | 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 |
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|
| 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` |
#### 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 |
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------|
| 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 |
##### 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` |
| 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` |
##### FuturesSettings
| Key | Description | Example |
| --- | ------- | ----- |
| Leverage | This struct defines the leverage rules that this specific currency setting must abide by | `1` |
| Key | Description | Example |
|----------|------------------------------------------------------------------------------------------|---------|
| 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 |
| 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` |
| Key | Description | Example |
|--------------|-------------------------------------------------------------------------|---------|
| RiskFreeRate | The risk free rate used in the calculation of sharpe and sortino ratios | `0.03` |
#### 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 |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| 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` |
#### 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 |
|----------|--------------------------------------------------------------------------------------------------------|--------------------------|
| 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` |
#### 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 |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
| 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` |
##### database
| Config | Description | Example |
| ------ | ----------- | ------- |
| enabled | Enabled or disables the database connection subsystem | `true` |
| verbose | Displays more information to the logger which can be helpful for debugging | `false` |
| driver | The SQL driver to use. Can be `postgres` or `sqlite` | `sqlite` |
| connectionDetails | See below | |
| Config | Description | Example |
|-------------------|----------------------------------------------------------------------------|----------|
| enabled | Enabled or disables the database connection subsystem | `true` |
| verbose | Displays more information to the logger which can be helpful for debugging | `false` |
| driver | The SQL driver to use. Can be `postgres` or `sqlite` | `sqlite` |
| connectionDetails | See below | |
##### connectionDetails
| Config | Description | Example |
| ------ | ----------- | ------- |
| host | The host address of the database | `localhost` |
| port | The port used to connect to the database | `5432` |
| username | An optional username to connect to the database | `username` |
| password | An optional password to connect to the database | `password` |
| database | The name of the database | `database.db` |
| sslmode | The connection type of the database for Postgres databases only | `disable` |
| Config | Description | Example |
|----------|-----------------------------------------------------------------|---------------|
| host | The host address of the database | `localhost` |
| port | The port used to connect to the database | `5432` |
| username | An optional username to connect to the database | `username` |
| password | An optional password to connect to the database | `password` |
| database | The name of the database | `database.db` |
| sslmode | The connection type of the database for Postgres databases only | `disable` |
#### LiveData
| Key | Description | Example |
| --- | ----------- | ------- |
| 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 |
|-----------------------|--------------------------------------------------------------------------------------------------------|---------------|
| 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` |
##### 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 |
|--------------------------------|------------------------------------------------------------------------------------------|---------|
| 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` |
##### 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 |
|--------------|------------------------------------------------------------------------------------------------------------------|---------|
| 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` |
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}

View File

@@ -0,0 +1,16 @@
{{define "engine backtest" -}}
{{template "backtester-header" .}}
## {{.CapitalName}} package overview
The backtest package is responsible for handling all events. It is the engine which combines all elements.
Data is converted into candles which are then analysed via the strategyhandler. From there, events can be passed through to other handlers such as the portfolio handler to determine whether or not to place an order
A flow of the application is as follows:
![workflow](https://i.imgur.com/Kup6IA9.png)
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -0,0 +1,10 @@
{{define "engine grpcserver" -}}
{{template "backtester-header" .}}
## {{.CapitalName}} package overview
The GRPC server is responsible for handling requests from the client. All GRPC functionality as defined in the proto file is implemented [here](/backtester/btrpc)
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -0,0 +1,17 @@
{{define "engine live" -}}
{{template "backtester-header" .}}
## {{.CapitalName}} package overview
Live trading has specific requirements separate from backtesting. Handling the looping of candle data and managing real orders and orderbooks will be handled here
Live trading is only a proof of concept. Please do not risk your funds by using it with `realOrders` enabled
A flow of the application is as follows:
![workflow](https://i.imgur.com/Kup6IA9.png)
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -1,22 +0,0 @@
{{define "backtester engine" -}}
{{template "backtester-header" .}}
## {{.CapitalName}} package overview
The engine package is the most important package of the GoCryptoTrader backtester. It is the engine which combines all elements.
It is responsible for the following functionality
- Loading settings from a provided config file
- Retrieving data
- Loading the data into assessable chunks
- Analysing the data via the `handleEvent` function
- Looping through all data
- Outputting results into a report
A flow of the application is as follows:
![workflow](https://i.imgur.com/Kup6IA9.png)
### Please click GoDocs chevron above to view current GoDoc information for this package
{{template "contributions"}}
{{template "donations" .}}
{{end}}

View File

@@ -27,13 +27,14 @@ 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
## 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 |
|---------|-------------|
| Long-running application | Transform the Backtester to run a GRPC server, where commands can be sent to run Backtesting operations. Allowing for many strategies to be run, analysed and tweaked in a more efficient manner |
| Leverage support | Leverage is a good way to enhance profit and loss and is important to include in strategies |
| 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 |

View File

@@ -540,7 +540,7 @@ func UpdateDocumentation(details DocumentationDetails) {
}
continue
}
if name == engineFolder {
if strings.Contains(name, engineFolder) {
d, err := os.ReadDir(details.Directories[i])
if err != nil {
fmt.Println("Excluding file:", err)

View File

@@ -854,7 +854,8 @@ func verifyCert(pemData []byte) error {
return nil
}
func checkCerts(certDir string) error {
// CheckCerts checks and verifies RPC server certificates
func CheckCerts(certDir string) error {
certFile := filepath.Join(certDir, "cert.pem")
keyFile := filepath.Join(certDir, "key.pem")

View File

@@ -1318,7 +1318,7 @@ func TestCheckAndGenCerts(t *testing.T) {
}
defer cleanup()
if err := checkCerts(tempDir); err != nil {
if err := CheckCerts(tempDir); err != nil {
t.Fatal(err)
}
@@ -1327,11 +1327,11 @@ func TestCheckAndGenCerts(t *testing.T) {
if err := os.Remove(certFile); err != nil {
t.Fatal(err)
}
if err := checkCerts(tempDir); err != nil {
if err := CheckCerts(tempDir); err != nil {
t.Fatal(err)
}
// Now call checkCerts to test an expired cert
// Now call CheckCerts to test an expired cert
certData, err := mockCert("", time.Now().Add(-time.Hour))
if err != nil {
t.Fatal(err)
@@ -1340,7 +1340,7 @@ func TestCheckAndGenCerts(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err = checkCerts(tempDir); err != nil {
if err = CheckCerts(tempDir); err != nil {
t.Fatal(err)
}
}

View File

@@ -104,10 +104,9 @@ func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, er
return ctx, fmt.Errorf("unable to base64 decode authorization header")
}
credentials := strings.Split(string(decoded), ":")
username := credentials[0]
password := credentials[1]
cred := strings.Split(string(decoded), ":")
username := cred[0]
password := cred[1]
if username != s.Config.RemoteControl.Username ||
password != s.Config.RemoteControl.Password {
@@ -127,8 +126,8 @@ func (s *RPCServer) authenticateClient(ctx context.Context) (context.Context, er
// StartRPCServer starts a gRPC server with TLS auth
func StartRPCServer(engine *Engine) {
targetDir := utils.GetTLSDir(engine.Settings.DataDir)
if err := checkCerts(targetDir); err != nil {
log.Errorf(log.GRPCSys, "gRPC checkCerts failed. err: %s\n", err)
if err := CheckCerts(targetDir); err != nil {
log.Errorf(log.GRPCSys, "gRPC CheckCerts failed. err: %s\n", err)
return
}
log.Debugf(log.GRPCSys, "gRPC server support enabled. Starting gRPC server on https://%v.\n", engine.Config.RemoteControl.GRPC.ListenAddress)

View File

@@ -190,15 +190,17 @@ type API struct {
credentials *account.Credentials
credMu sync.RWMutex
CredentialsValidator struct {
// For Huobi (optional)
RequiresPEM bool
CredentialsValidator CredentialsValidator
}
RequiresKey bool
RequiresSecret bool
RequiresClientID bool
RequiresBase64DecodeSecret bool
}
// CredentialsValidator determines what is required
// to make authenticated requests for an exchange
type CredentialsValidator struct {
RequiresPEM bool
RequiresKey bool
RequiresSecret bool
RequiresClientID bool
RequiresBase64DecodeSecret bool
}
// Base stores the individual exchange information

View File

@@ -4,8 +4,8 @@ deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: 80720a488c9a414bb8d4a9f811084989
commit: 62f35d8aed1149c291d606d958a7ce32
- remote: buf.build
owner: grpc-ecosystem
repository: grpc-gateway
commit: 00116f302b12478b85deb33b734e026c
commit: bc28b723cd774c32b6fbc77621518765

View File

@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc-gen-go v1.28.0
// protoc (unknown)
// source: rpc.proto

File diff suppressed because it is too large Load Diff