mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-03 15:10:49 +00:00
backtester: Futures handling & FTX Cash and Carry example strategy (#930)
* implements futures functions and GRPC functions on new branch * lint and test fixes * Fix uneven split pnl. Adds collateral weight test. docs. New clear func * Test protection if someone has zero collateral * Uses string instead of double for accuracy * Fixes old code panic * context, match, docs * Addresses Shazniterinos, var names, expanded tests * Returns subaccount name, provides USD values when offlinecalc * Fixes oopsie * Fixes cool bug which allowed made up subaccount results * Subaccount override on FTX, subaccount results for collateral * Strenghten collateral account info checks. Improve FTX test * English is my first language * Fixes oopsies * Adds some conceptual futures order details to track PNL * Initial design of future order processing in the backtester * Introduces futures concept for collateral and spot/futures config diffs * Fixes most tests * Simple designs for collateral funding pair concept * Expands interface use so much it hurts * Implements more collateral interfaces * Adds liquidation, adds strategy, struggles with Binance * Attempts at getting FTX to work * Adds calculatePNL as a wrapper function and adds an `IsFutures` asset check * Successfully loads backtester with collateral currency * Fails to really get much going for supporting futures * Merges master changes * Fleshes out how FTX processes collateral * Further FTX collateral workings * hooks up more ftx collateral and pnl calculations * more funcs to flesh out handling * Adds more links, just can't fit the pieces together :( * Greatly expands futures order processing * Fleshes out position tracker to also handle asset and exchange +testing * RM linkedOrderID. rn positioncontroller, unexport * Successfully tracks futures order positions * Fails to calculate PNL * Calculates pnl from orders accurately with exception to flipping orders * Calculates PNL from orders * Adds another controller layer to make it ez from orderstore * Backtester now compiles. Adds test coverage * labels things add scaling collateral test * Calculates pnl in line with fees * Mostly accurate PNL, with exception to appending with diff prices * Adds locks, adds rpc function * grpc implementations * Gracefully handles rpc function * beautiful tests! * rejiggles tests to polish * Finishes FTX testing, adds comments * Exposes collateral calculations to rpc * Adds commands and testing for rpcserver.go functions * Increase testing and fix up backtester code * Returns cool changes to original branch * end of day fixes * Fixing some tests * Fixing tests 🎉 * Fixes all the tests * Splits the backtester setup and running into different files * Merge, minor fixes * Messing with some strategy updates * Failed understanding at collateral usage * Begins the creation of cash and carry strategy * Adds underlying pair, adds filldependentevent for futures * Completes fill prerequsite event implementation. Can't short though * Some bug fixes * investigating funds * CAN NOW CREATE A SHORT ORDER * Minor change in short size * Fixes for unrealised PNL & collateral rendering * Fixes lint and tests * Adds some verbosity * Updates to pnl calc * Tracks pnl for short orders, minor update to strategy * Close and open event based on conditions * Adds pnl data for currency statistics * Working through PNL calculation automatically. Now panics * Adds tracking, is blocked from design * Work to flesh out closing a position * vain attempts at tracking zeroing out bugs * woww, super fun new subloggers 🎉 * Begins attempt at automatically handling contracts and collateral based on direction * Merge master + fixes * Investigating issues with pnl and holdings * Minor pnl fixes * Fixes future position sizing, needs contract sizing * Can render pnl results, focussing on funding statistics * tracking candles for futures, but why not btc * Improves funding statistics * Colours and stats * Fixes collateral and snapshot bugs * Completes test * Fixes totals bug * Fix double buy, expand stats, fixes usd totals, introduce interface * Begins report formatting and calculations * Appends pnl to receiving curr. Fixes map[time]. accurate USD * Improves report output rendering * PNL stats in report. New tests for futures * Fixes existing tests before adding new coverage * Test coverage * Completes portfolio coverage * Increase coverage exchange, portfolio. fix size bug. NEW CHART * WHAT IS GOING ON WITH PNL * Fixes PNL calculation. Adds ability to skip om futures tracking * minor commit before merge * Adds basic liquidation to backtester * Changes liquidation to order based * Liquidationnnnnn * Further fleshes out liquidations * Completes liquidations in a honorable manner. Adds AppendReasonf * Beginnings of spot futures gap chart. Needs to link currencies to render difference * Removes fake liquidation. Adds cool new chart * Fixes somet tests,allows for zero fee value v nil distinction,New tests * Some annoying test fixes that took too long * portfolio coverage * holding coverage, privatisation funding * Testwork * boring tests * engine coverage * More backtesting coverage * Funding, strategy, report test coverage * Completes coverage of report package * Documentation, fixes some assumptions on asset errors * Changes before master merge * Lint and Tests * defaults to non-coloured rendering * Chart rendering * Fixes surprise non-local-lints * Niterinos to the extremeos * Fixes merge problems * The linter splintered across the glinting plinths * Many nits addressed. Now sells spot position on final candle * Adds forgotten coverage * Adds ability to size futures contracts to match spot positions. * fixes order sell sizing * Adds tests to sizing. Fixes charting issue * clint splintered the linters with flint * Improves stats, stat rendering * minifix * Fixes tests and fee bug * Merge fixeroos * Microfixes * Updates orderPNL on first Correctly utilises fees. Adds committed funds * New base funcs. New order summary * Fun test updates * Fix logo colouring * Fixes niteroonies * Fix report * BAD COMMIT * Fixes funding issues.Updates default fee rates.Combines cashcarry case * doc regen * Now returns err * Fixes sizing bug issue introduced in PR * Fixes fun fee/total US value bug * Fix chart bug. Show log charts with disclaimer * sellside fee * fixes fee and slippage view * Fixed slippage price issue * Fixes calculation and removes rendering * Fixes stats and some rendering * Merge fix * Fixes merge issues * go mod tidy, lint updates * New linter attempt * Version bump in appveyor and makefile * Regex filename, config fixes, template h2 fixes * Removes bad stats. * neatens config builder. Moves filename generator * Fixes issue where linter wants to fix my spelling * Fixes pointers and starts
This commit is contained in:
56
backtester/engine/README.md
Normal file
56
backtester/engine/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# GoCryptoTrader Backtester: Engine package
|
||||
|
||||
<img src="/backtester/common/backtester.png?raw=true" width="350px" height="350px" hspace="70">
|
||||
|
||||
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
|
||||
[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
|
||||
[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/backtester/engine)
|
||||
[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
|
||||
|
||||
|
||||
This engine 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)
|
||||
|
||||
## Engine 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:
|
||||

|
||||
|
||||
|
||||
### 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***
|
||||
473
backtester/engine/backtest.go
Normal file
473
backtester/engine/backtest.go
Normal file
@@ -0,0 +1,473 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/compliance"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/holdings"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// New returns a new BackTest instance
|
||||
func New() *BackTest {
|
||||
return &BackTest{
|
||||
shutdown: make(chan struct{}),
|
||||
Datas: &data.HandlerPerCurrency{},
|
||||
EventQueue: &eventholder.Holder{},
|
||||
}
|
||||
}
|
||||
|
||||
// Reset BackTest values to default
|
||||
func (bt *BackTest) Reset() {
|
||||
bt.EventQueue.Reset()
|
||||
bt.Datas.Reset()
|
||||
bt.Portfolio.Reset()
|
||||
bt.Statistic.Reset()
|
||||
bt.Exchange.Reset()
|
||||
bt.Funding.Reset()
|
||||
bt.exchangeManager = nil
|
||||
bt.orderManager = nil
|
||||
bt.databaseManager = nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
dataLoadingIssue:
|
||||
for ev := bt.EventQueue.NextEvent(); ; ev = bt.EventQueue.NextEvent() {
|
||||
if ev == nil {
|
||||
dataHandlerMap := bt.Datas.GetAllData()
|
||||
var hasProcessedData bool
|
||||
for exchangeName, exchangeMap := range dataHandlerMap {
|
||||
for assetItem, assetMap := range exchangeMap {
|
||||
for currencyPair, dataHandler := range assetMap {
|
||||
d := dataHandler.Next()
|
||||
if d == nil {
|
||||
if !bt.hasHandledEvent {
|
||||
log.Errorf(common.Backtester, "Unable to perform `Next` for %v %v %v", exchangeName, assetItem, currencyPair)
|
||||
}
|
||||
break dataLoadingIssue
|
||||
}
|
||||
if bt.Strategy.UsingSimultaneousProcessing() && hasProcessedData {
|
||||
// only append one event, as simultaneous processing
|
||||
// will retrieve all relevant events to process under
|
||||
// processSimultaneousDataEvents()
|
||||
continue
|
||||
}
|
||||
bt.EventQueue.AppendEvent(d)
|
||||
hasProcessedData = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := bt.handleEvent(ev)
|
||||
if err != nil {
|
||||
log.Error(common.Backtester, err)
|
||||
}
|
||||
}
|
||||
if !bt.hasHandledEvent {
|
||||
bt.hasHandledEvent = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleEvent is the main processor of data for the backtester
|
||||
// after data has been loaded and Run has appended a data event to the queue,
|
||||
// handle event will process events and add further events to the queue if they
|
||||
// are required
|
||||
func (bt *BackTest) handleEvent(ev common.EventHandler) error {
|
||||
if ev == nil {
|
||||
return fmt.Errorf("cannot handle event %w", errNilData)
|
||||
}
|
||||
funds, err := bt.Funding.GetFundingForEvent(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bt.Funding.HasFutures() {
|
||||
err = bt.Funding.UpdateCollateral(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
switch eType := ev.(type) {
|
||||
case common.DataEventHandler:
|
||||
if bt.Strategy.UsingSimultaneousProcessing() {
|
||||
err = bt.processSimultaneousDataEvents()
|
||||
} else {
|
||||
err = bt.processSingleDataEvent(eType, funds.FundReleaser())
|
||||
}
|
||||
case signal.Event:
|
||||
err = bt.processSignalEvent(eType, funds.FundReserver())
|
||||
case order.Event:
|
||||
err = bt.processOrderEvent(eType, funds.FundReleaser())
|
||||
case fill.Event:
|
||||
err = bt.processFillEvent(eType, funds.FundReleaser())
|
||||
default:
|
||||
return fmt.Errorf("handleEvent %w %T received, could not process",
|
||||
errUnhandledDatatype,
|
||||
ev)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bt.Funding.CreateSnapshot(ev.GetTime())
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSingleDataEvent will pass the event to the strategy and determine how it should be handled
|
||||
func (bt *BackTest) processSingleDataEvent(ev common.DataEventHandler, funds funding.IFundReleaser) error {
|
||||
err := bt.updateStatsForDataEvent(ev, funds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d, err := bt.Datas.GetDataForCurrency(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := bt.Strategy.OnSignal(d, bt.Funding, bt.Portfolio)
|
||||
if err != nil {
|
||||
if errors.Is(err, base.ErrTooMuchBadData) {
|
||||
// too much bad data is a severe error and backtesting must cease
|
||||
return err
|
||||
}
|
||||
log.Errorf(common.Backtester, "OnSignal %v", err)
|
||||
return nil
|
||||
}
|
||||
err = bt.Statistic.SetEventForOffset(s)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "SetEventForOffset %v", err)
|
||||
}
|
||||
bt.EventQueue.AppendEvent(s)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processSimultaneousDataEvents determines what signal events are generated and appended
|
||||
// to the event queue. It will pass all currency events to the strategy to determine what
|
||||
// currencies to act upon
|
||||
func (bt *BackTest) processSimultaneousDataEvents() error {
|
||||
var dataEvents []data.Handler
|
||||
dataHandlerMap := bt.Datas.GetAllData()
|
||||
for _, exchangeMap := range dataHandlerMap {
|
||||
for _, assetMap := range exchangeMap {
|
||||
for _, dataHandler := range assetMap {
|
||||
latestData := dataHandler.Latest()
|
||||
funds, err := bt.Funding.GetFundingForEvent(latestData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bt.updateStatsForDataEvent(latestData, funds.FundReleaser())
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, statistics.ErrAlreadyProcessed):
|
||||
continue
|
||||
case errors.Is(err, gctorder.ErrPositionLiquidated):
|
||||
return nil
|
||||
default:
|
||||
log.Error(common.Backtester, err)
|
||||
}
|
||||
}
|
||||
dataEvents = append(dataEvents, dataHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
signals, err := bt.Strategy.OnSimultaneousSignals(dataEvents, bt.Funding, bt.Portfolio)
|
||||
if err != nil {
|
||||
if errors.Is(err, base.ErrTooMuchBadData) {
|
||||
// too much bad data is a severe error and backtesting must cease
|
||||
return err
|
||||
}
|
||||
log.Errorf(common.Backtester, "OnSimultaneousSignals %v", err)
|
||||
return nil
|
||||
}
|
||||
for i := range signals {
|
||||
err = bt.Statistic.SetEventForOffset(signals[i])
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", signals[i].GetExchange(), signals[i].GetAssetType(), signals[i].Pair(), err)
|
||||
}
|
||||
bt.EventQueue.AppendEvent(signals[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateStatsForDataEvent makes various systems aware of price movements from
|
||||
// data events
|
||||
func (bt *BackTest) updateStatsForDataEvent(ev common.DataEventHandler, funds funding.IFundReleaser) error {
|
||||
if ev == nil {
|
||||
return common.ErrNilEvent
|
||||
}
|
||||
if funds == nil {
|
||||
return fmt.Errorf("%v %v %v %w missing fund releaser", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), common.ErrNilArguments)
|
||||
}
|
||||
// update statistics with the latest price
|
||||
err := bt.Statistic.SetupEventForTime(ev)
|
||||
if err != nil {
|
||||
if errors.Is(err, statistics.ErrAlreadyProcessed) {
|
||||
return err
|
||||
}
|
||||
log.Errorf(common.Backtester, "SetupEventForTime %v", err)
|
||||
}
|
||||
// update portfolio manager with the latest price
|
||||
err = bt.Portfolio.UpdateHoldings(ev, funds)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "UpdateHoldings %v", err)
|
||||
}
|
||||
|
||||
if ev.GetAssetType().IsFutures() {
|
||||
var cr funding.ICollateralReleaser
|
||||
cr, err = funds.CollateralReleaser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bt.Portfolio.UpdatePNL(ev, ev.GetClosePrice())
|
||||
if err != nil {
|
||||
if errors.Is(err, gctorder.ErrPositionsNotLoadedForPair) {
|
||||
// if there is no position yet, there's nothing to update
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, gctorder.ErrPositionLiquidated) {
|
||||
return fmt.Errorf("UpdatePNL %v", err)
|
||||
}
|
||||
}
|
||||
var pnl *portfolio.PNLSummary
|
||||
pnl, err = bt.Portfolio.GetLatestPNLForEvent(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pnl.Result.IsLiquidated {
|
||||
return nil
|
||||
}
|
||||
err = bt.Portfolio.CheckLiquidationStatus(ev, cr, pnl)
|
||||
if err != nil {
|
||||
if errors.Is(err, gctorder.ErrPositionLiquidated) {
|
||||
liquidErr := bt.triggerLiquidationsForExchange(ev, pnl)
|
||||
if liquidErr != nil {
|
||||
return liquidErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return bt.Statistic.AddPNLForTime(pnl)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bt *BackTest) triggerLiquidationsForExchange(ev common.DataEventHandler, pnl *portfolio.PNLSummary) error {
|
||||
if ev == nil {
|
||||
return common.ErrNilEvent
|
||||
}
|
||||
if pnl == nil {
|
||||
return fmt.Errorf("%w pnl summary", common.ErrNilArguments)
|
||||
}
|
||||
orders, err := bt.Portfolio.CreateLiquidationOrdersForExchange(ev, bt.Funding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range orders {
|
||||
// these orders are raising events for event offsets
|
||||
// which may not have been processed yet
|
||||
// this will create and store stats for each order
|
||||
// then liquidate it at the funding level
|
||||
var datas data.Handler
|
||||
datas, err = bt.Datas.GetDataForCurrency(orders[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
latest := datas.Latest()
|
||||
err = bt.Statistic.SetupEventForTime(latest)
|
||||
if err != nil && !errors.Is(err, statistics.ErrAlreadyProcessed) {
|
||||
return err
|
||||
}
|
||||
bt.EventQueue.AppendEvent(orders[i])
|
||||
err = bt.Statistic.SetEventForOffset(orders[i])
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "SetupEventForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
bt.Funding.Liquidate(orders[i])
|
||||
}
|
||||
pnl.Result.IsLiquidated = true
|
||||
pnl.Result.Status = gctorder.Liquidated
|
||||
return bt.Statistic.AddPNLForTime(pnl)
|
||||
}
|
||||
|
||||
// processSignalEvent receives an event from the strategy for processing under the portfolio
|
||||
func (bt *BackTest) processSignalEvent(ev signal.Event, funds funding.IFundReserver) error {
|
||||
if ev == nil {
|
||||
return common.ErrNilEvent
|
||||
}
|
||||
if funds == nil {
|
||||
return fmt.Errorf("%w funds", common.ErrNilArguments)
|
||||
}
|
||||
cs, err := bt.Exchange.GetCurrencySettings(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "GetCurrencySettings %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
return fmt.Errorf("GetCurrencySettings %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
var o *order.Order
|
||||
o, err = bt.Portfolio.OnSignal(ev, &cs, funds)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "OnSignal %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
return fmt.Errorf("OnSignal %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
err = bt.Statistic.SetEventForOffset(o)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
|
||||
bt.EventQueue.AppendEvent(o)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bt *BackTest) processOrderEvent(ev order.Event, funds funding.IFundReleaser) error {
|
||||
if ev == nil {
|
||||
return common.ErrNilEvent
|
||||
}
|
||||
if funds == nil {
|
||||
return fmt.Errorf("%w funds", common.ErrNilArguments)
|
||||
}
|
||||
d, err := bt.Datas.GetDataForCurrency(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := bt.Exchange.ExecuteOrder(ev, d, bt.orderManager, funds)
|
||||
if err != nil {
|
||||
if f == nil {
|
||||
log.Errorf(common.Backtester, "ExecuteOrder fill event should always be returned, please fix, %v", err)
|
||||
return fmt.Errorf("ExecuteOrder fill event should always be returned, please fix, %v", err)
|
||||
}
|
||||
if !errors.Is(err, exchange.ErrCannotTransact) {
|
||||
log.Errorf(common.Backtester, "ExecuteOrder %v %v %v %v", f.GetExchange(), f.GetAssetType(), f.Pair(), err)
|
||||
}
|
||||
}
|
||||
err = bt.Statistic.SetEventForOffset(f)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
bt.EventQueue.AppendEvent(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bt *BackTest) processFillEvent(ev fill.Event, funds funding.IFundReleaser) error {
|
||||
t, err := bt.Portfolio.OnFill(ev, funds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OnFill %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
err = bt.Statistic.SetEventForOffset(t)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
|
||||
var holding *holdings.Holding
|
||||
holding, err = bt.Portfolio.ViewHoldingAtTimePeriod(ev)
|
||||
if err != nil {
|
||||
log.Error(common.Backtester, err)
|
||||
}
|
||||
if holding == nil {
|
||||
log.Error(common.Backtester, "ViewHoldingAtTimePeriod why is holdings nil?")
|
||||
} else {
|
||||
err = bt.Statistic.AddHoldingsForTime(holding)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
}
|
||||
|
||||
var cp *compliance.Manager
|
||||
cp, err = bt.Portfolio.GetComplianceManager(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "GetComplianceManager %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
|
||||
snap := cp.GetLatestSnapshot()
|
||||
err = bt.Statistic.AddComplianceSnapshotForTime(snap, ev)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "AddComplianceSnapshotForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
|
||||
fde := ev.GetFillDependentEvent()
|
||||
if fde != nil && !fde.IsNil() {
|
||||
// some events can only be triggered on a successful fill event
|
||||
fde.SetOffset(ev.GetOffset())
|
||||
err = bt.Statistic.SetEventForOffset(fde)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", fde.GetExchange(), fde.GetAssetType(), fde.Pair(), err)
|
||||
}
|
||||
od := ev.GetOrder()
|
||||
if fde.MatchOrderAmount() && od != nil {
|
||||
fde.SetAmount(ev.GetAmount())
|
||||
}
|
||||
fde.AppendReasonf("raising event after %v %v %v fill", ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
||||
bt.EventQueue.AppendEvent(fde)
|
||||
}
|
||||
if ev.GetAssetType().IsFutures() {
|
||||
return bt.processFuturesFillEvent(ev, funds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bt *BackTest) processFuturesFillEvent(ev fill.Event, funds funding.IFundReleaser) error {
|
||||
if ev.GetOrder() != nil {
|
||||
pnl, err := bt.Portfolio.TrackFuturesOrder(ev, funds)
|
||||
if err != nil && !errors.Is(err, gctorder.ErrSubmissionIsNil) {
|
||||
return fmt.Errorf("TrackFuturesOrder %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
|
||||
var exch gctexchange.IBotExchange
|
||||
exch, err = bt.exchangeManager.GetExchangeByName(ev.GetExchange())
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetExchangeByName %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
|
||||
rPNL := pnl.GetRealisedPNL()
|
||||
if !rPNL.PNL.IsZero() {
|
||||
var receivingCurrency currency.Code
|
||||
var receivingAsset asset.Item
|
||||
receivingCurrency, receivingAsset, err = exch.GetCurrencyForRealisedPNL(ev.GetAssetType(), ev.Pair())
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetCurrencyForRealisedPNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
err = bt.Funding.RealisePNL(ev.GetExchange(), receivingAsset, receivingCurrency, rPNL.PNL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("RealisePNL %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
}
|
||||
|
||||
err = bt.Statistic.AddPNLForTime(pnl)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
}
|
||||
err := bt.Funding.UpdateCollateral(ev)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateCollateral %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop shuts down the live data loop
|
||||
func (bt *BackTest) Stop() {
|
||||
close(bt.shutdown)
|
||||
}
|
||||
1377
backtester/engine/backtest_test.go
Normal file
1377
backtester/engine/backtest_test.go
Normal file
File diff suppressed because it is too large
Load Diff
45
backtester/engine/backtest_types.go
Normal file
45
backtester/engine/backtest_types.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/eventholder"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/exchange"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/report"
|
||||
"github.com/thrasher-corp/gocryptotrader/engine"
|
||||
)
|
||||
|
||||
var (
|
||||
errNilConfig = errors.New("unable to setup backtester with nil config")
|
||||
errInvalidConfigAsset = errors.New("invalid asset in config")
|
||||
errAmbiguousDataSource = errors.New("ambiguous settings received. Only one data type can be set")
|
||||
errNoDataSource = errors.New("no data settings set in config")
|
||||
errIntervalUnset = errors.New("candle interval unset")
|
||||
errUnhandledDatatype = errors.New("unhandled datatype")
|
||||
errLiveDataTimeout = errors.New("no data returned in 5 minutes, shutting down")
|
||||
errNilData = errors.New("nil data received")
|
||||
errNilExchange = errors.New("nil exchange received")
|
||||
errLiveUSDTrackingNotSupported = errors.New("USD tracking not supported for live data")
|
||||
)
|
||||
|
||||
// BackTest is the main holder of all backtesting functionality
|
||||
type BackTest struct {
|
||||
hasHandledEvent bool
|
||||
shutdown chan struct{}
|
||||
Datas data.Holder
|
||||
Strategy strategies.Handler
|
||||
Portfolio portfolio.Handler
|
||||
Exchange exchange.ExecutionHandler
|
||||
Statistic statistics.Handler
|
||||
EventQueue eventholder.EventHolder
|
||||
Reports report.Handler
|
||||
Funding funding.IFundingManager
|
||||
exchangeManager *engine.ExchangeManager
|
||||
orderManager *engine.OrderManager
|
||||
databaseManager *engine.DatabaseConnectionManager
|
||||
}
|
||||
139
backtester/engine/live.go
Normal file
139
backtester/engine/live.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/live"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// RunLive is a proof of concept function that does not yet support multi currency usage
|
||||
// It runs by constantly checking for new live datas and running through the list of events
|
||||
// once new data is processed. It will run until application close event has been received
|
||||
func (bt *BackTest) RunLive() error {
|
||||
log.Info(common.Backtester, "running backtester against live data")
|
||||
timeoutTimer := time.NewTimer(time.Minute * 5)
|
||||
// a frequent timer so that when a new candle is released by an exchange
|
||||
// that it can be processed quickly
|
||||
processEventTicker := time.NewTicker(time.Second)
|
||||
doneARun := false
|
||||
for {
|
||||
select {
|
||||
case <-bt.shutdown:
|
||||
return nil
|
||||
case <-timeoutTimer.C:
|
||||
return errLiveDataTimeout
|
||||
case <-processEventTicker.C:
|
||||
for e := bt.EventQueue.NextEvent(); ; e = bt.EventQueue.NextEvent() {
|
||||
if e == nil {
|
||||
// as live only supports singular currency, just get the proper reference manually
|
||||
var d data.Handler
|
||||
dd := bt.Datas.GetAllData()
|
||||
for k1, v1 := range dd {
|
||||
for k2, v2 := range v1 {
|
||||
for k3 := range v2 {
|
||||
d = dd[k1][k2][k3]
|
||||
}
|
||||
}
|
||||
}
|
||||
de := d.Next()
|
||||
if de == nil {
|
||||
break
|
||||
}
|
||||
|
||||
bt.EventQueue.AppendEvent(de)
|
||||
doneARun = true
|
||||
continue
|
||||
}
|
||||
err := bt.handleEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if doneARun {
|
||||
timeoutTimer = time.NewTimer(time.Minute * 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadLiveDataLoop is an incomplete function to continuously retrieve exchange data on a loop
|
||||
// from live. Its purpose is to be able to perform strategy analysis against current data
|
||||
func (bt *BackTest) loadLiveDataLoop(resp *kline.DataFromKline, cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, dataType int64) {
|
||||
startDate := time.Now().Add(-cfg.DataSettings.Interval.Duration() * 2)
|
||||
dates, err := gctkline.CalculateCandleDateRanges(
|
||||
startDate,
|
||||
startDate.AddDate(1, 0, 0),
|
||||
cfg.DataSettings.Interval,
|
||||
0)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "%v. Please check your GoCryptoTrader configuration", err)
|
||||
return
|
||||
}
|
||||
candles, err := live.LoadData(context.TODO(),
|
||||
exch,
|
||||
dataType,
|
||||
cfg.DataSettings.Interval.Duration(),
|
||||
fPair,
|
||||
a)
|
||||
if err != nil {
|
||||
log.Errorf(common.Backtester, "%v. Please check your GoCryptoTrader configuration", err)
|
||||
return
|
||||
}
|
||||
dates.SetHasDataFromCandles(candles.Candles)
|
||||
resp.RangeHolder = dates
|
||||
resp.Item = *candles
|
||||
|
||||
loadNewDataTimer := time.NewTimer(time.Second * 5)
|
||||
for {
|
||||
select {
|
||||
case <-bt.shutdown:
|
||||
return
|
||||
case <-loadNewDataTimer.C:
|
||||
log.Infof(common.Backtester, "fetching data for %v %v %v %v", exch.GetName(), a, fPair, cfg.DataSettings.Interval)
|
||||
loadNewDataTimer.Reset(time.Second * 15)
|
||||
err = bt.loadLiveData(resp, cfg, exch, fPair, a, dataType)
|
||||
if err != nil {
|
||||
log.Error(common.Backtester, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (bt *BackTest) loadLiveData(resp *kline.DataFromKline, cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, dataType int64) error {
|
||||
if resp == nil {
|
||||
return errNilData
|
||||
}
|
||||
if cfg == nil {
|
||||
return errNilConfig
|
||||
}
|
||||
if exch == nil {
|
||||
return errNilExchange
|
||||
}
|
||||
candles, err := live.LoadData(context.TODO(),
|
||||
exch,
|
||||
dataType,
|
||||
cfg.DataSettings.Interval.Duration(),
|
||||
fPair,
|
||||
a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(candles.Candles) == 0 {
|
||||
return nil
|
||||
}
|
||||
resp.AppendResults(candles)
|
||||
bt.Reports.UpdateItem(&resp.Item)
|
||||
log.Info(common.Backtester, "sleeping for 30 seconds before checking for new candle data")
|
||||
return nil
|
||||
}
|
||||
867
backtester/engine/setup.go
Normal file
867
backtester/engine/setup.go
Normal file
@@ -0,0 +1,867 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/api"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/csv"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline/database"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/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/risk"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/portfolio/size"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/statistics"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/funding/trackingcurrencies"
|
||||
"github.com/thrasher-corp/gocryptotrader/backtester/report"
|
||||
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/convert"
|
||||
gctconfig "github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
gctdatabase "github.com/thrasher-corp/gocryptotrader/database"
|
||||
"github.com/thrasher-corp/gocryptotrader/engine"
|
||||
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// NewFromConfig takes a strategy config and configures a backtester variable to run
|
||||
func NewFromConfig(cfg *config.Config, templatePath, output string, verbose bool) (*BackTest, error) {
|
||||
log.Infoln(common.Setup, "loading config...")
|
||||
if cfg == nil {
|
||||
return nil, errNilConfig
|
||||
}
|
||||
var err error
|
||||
bt := New()
|
||||
bt.exchangeManager = engine.SetupExchangeManager()
|
||||
bt.orderManager, err = engine.SetupOrderManager(bt.exchangeManager, &engine.CommunicationManager{}, &sync.WaitGroup{}, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = bt.orderManager.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.DataSettings.DatabaseData != nil {
|
||||
bt.databaseManager, err = engine.SetupDatabaseConnectionManager(&cfg.DataSettings.DatabaseData.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reports := &report.Data{
|
||||
Config: cfg,
|
||||
TemplatePath: templatePath,
|
||||
OutputPath: output,
|
||||
}
|
||||
bt.Reports = reports
|
||||
|
||||
buyRule := exchange.MinMax{
|
||||
MinimumSize: cfg.PortfolioSettings.BuySide.MinimumSize,
|
||||
MaximumSize: cfg.PortfolioSettings.BuySide.MaximumSize,
|
||||
MaximumTotal: cfg.PortfolioSettings.BuySide.MaximumTotal,
|
||||
}
|
||||
sellRule := exchange.MinMax{
|
||||
MinimumSize: cfg.PortfolioSettings.SellSide.MinimumSize,
|
||||
MaximumSize: cfg.PortfolioSettings.SellSide.MaximumSize,
|
||||
MaximumTotal: cfg.PortfolioSettings.SellSide.MaximumTotal,
|
||||
}
|
||||
sizeManager := &size.Size{
|
||||
BuySide: buyRule,
|
||||
SellSide: sellRule,
|
||||
}
|
||||
|
||||
funds, err := funding.SetupFundingManager(
|
||||
bt.exchangeManager,
|
||||
cfg.FundingSettings.UseExchangeLevelFunding,
|
||||
cfg.StrategySettings.DisableUSDTracking,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.FundingSettings.UseExchangeLevelFunding {
|
||||
for i := range cfg.FundingSettings.ExchangeLevelFunding {
|
||||
a := cfg.FundingSettings.ExchangeLevelFunding[i].Asset
|
||||
cq := cfg.FundingSettings.ExchangeLevelFunding[i].Currency
|
||||
var item *funding.Item
|
||||
item, err = funding.CreateItem(cfg.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
|
||||
a,
|
||||
cq,
|
||||
cfg.FundingSettings.ExchangeLevelFunding[i].InitialFunds,
|
||||
cfg.FundingSettings.ExchangeLevelFunding[i].TransferFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = funds.AddItem(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var emm = make(map[string]gctexchange.IBotExchange)
|
||||
for i := range cfg.CurrencySettings {
|
||||
_, ok := emm[cfg.CurrencySettings[i].ExchangeName]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
var exch gctexchange.IBotExchange
|
||||
exch, err = bt.exchangeManager.NewExchangeByName(cfg.CurrencySettings[i].ExchangeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var conf *gctconfig.Exchange
|
||||
conf, err = exch.GetDefaultConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.Enabled = true
|
||||
conf.WebsocketTrafficTimeout = time.Second
|
||||
conf.Websocket = convert.BoolPtr(false)
|
||||
conf.WebsocketResponseCheckTimeout = time.Second
|
||||
conf.WebsocketResponseMaxLimit = time.Second
|
||||
conf.Verbose = verbose
|
||||
err = exch.Setup(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exchBase := exch.GetBase()
|
||||
err = exch.UpdateTradablePairs(context.Background(), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assets := exchBase.CurrencyPairs.GetAssetTypes(false)
|
||||
for i := range assets {
|
||||
exchBase.CurrencyPairs.Pairs[assets[i]].AssetEnabled = convert.BoolPtr(true)
|
||||
err = exch.SetPairs(exchBase.CurrencyPairs.Pairs[assets[i]].Available, assets[i], true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
bt.exchangeManager.Add(exch)
|
||||
emm[cfg.CurrencySettings[i].ExchangeName] = exch
|
||||
}
|
||||
|
||||
portfolioRisk := &risk.Risk{
|
||||
CurrencySettings: make(map[string]map[asset.Item]map[currency.Pair]*risk.CurrencySettings),
|
||||
}
|
||||
|
||||
for i := range cfg.CurrencySettings {
|
||||
if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName] == nil {
|
||||
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName] = make(map[asset.Item]map[currency.Pair]*risk.CurrencySettings)
|
||||
}
|
||||
a := cfg.CurrencySettings[i].Asset
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"%w for %v %v %v-%v. Err %v",
|
||||
errInvalidConfigAsset,
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
cfg.CurrencySettings[i].Asset,
|
||||
cfg.CurrencySettings[i].Base,
|
||||
cfg.CurrencySettings[i].Quote,
|
||||
err)
|
||||
}
|
||||
if portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] == nil {
|
||||
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a] = make(map[currency.Pair]*risk.CurrencySettings)
|
||||
}
|
||||
var curr currency.Pair
|
||||
var b, q currency.Code
|
||||
b = cfg.CurrencySettings[i].Base
|
||||
q = cfg.CurrencySettings[i].Quote
|
||||
curr = currency.NewPair(b, q)
|
||||
var exch gctexchange.IBotExchange
|
||||
exch, err = bt.exchangeManager.GetExchangeByName(cfg.CurrencySettings[i].ExchangeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exchBase := exch.GetBase()
|
||||
var requestFormat currency.PairFormat
|
||||
requestFormat, err = exchBase.GetPairFormat(a, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get pair format %v, %w", curr, err)
|
||||
}
|
||||
curr = curr.Format(requestFormat.Delimiter, requestFormat.Uppercase)
|
||||
var avail, enabled currency.Pairs
|
||||
avail, err = exch.GetAvailablePairs(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
|
||||
}
|
||||
enabled, err = exch.GetEnabledPairs(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
|
||||
}
|
||||
|
||||
avail = avail.Add(curr)
|
||||
enabled = enabled.Add(curr)
|
||||
err = exch.SetPairs(enabled, a, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
|
||||
}
|
||||
err = exch.SetPairs(avail, a, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not format currency %v, %w", curr, err)
|
||||
}
|
||||
|
||||
portSet := &risk.CurrencySettings{
|
||||
MaximumHoldingRatio: cfg.CurrencySettings[i].MaximumHoldingsRatio,
|
||||
}
|
||||
if cfg.CurrencySettings[i].FuturesDetails != nil {
|
||||
portSet.MaximumOrdersWithLeverageRatio = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio
|
||||
portSet.MaxLeverageRate = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate
|
||||
}
|
||||
portfolioRisk.CurrencySettings[cfg.CurrencySettings[i].ExchangeName][a][curr] = portSet
|
||||
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",
|
||||
cfg.CurrencySettings[i].MakerFee,
|
||||
cfg.CurrencySettings[i].TakerFee)
|
||||
}
|
||||
|
||||
var baseItem, quoteItem, futureItem *funding.Item
|
||||
if cfg.FundingSettings.UseExchangeLevelFunding {
|
||||
switch {
|
||||
case a == asset.Spot:
|
||||
// add any remaining currency items that have no funding data in the strategy config
|
||||
baseItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
|
||||
a,
|
||||
b,
|
||||
decimal.Zero,
|
||||
decimal.Zero)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quoteItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
|
||||
a,
|
||||
q,
|
||||
decimal.Zero,
|
||||
decimal.Zero)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = funds.AddItem(baseItem)
|
||||
if err != nil && !errors.Is(err, funding.ErrAlreadyExists) {
|
||||
return nil, err
|
||||
}
|
||||
err = funds.AddItem(quoteItem)
|
||||
if err != nil && !errors.Is(err, funding.ErrAlreadyExists) {
|
||||
return nil, err
|
||||
}
|
||||
case a.IsFutures():
|
||||
// setup contract items
|
||||
c := funding.CreateFuturesCurrencyCode(b, q)
|
||||
futureItem, err = funding.CreateItem(cfg.CurrencySettings[i].ExchangeName,
|
||||
a,
|
||||
c,
|
||||
decimal.Zero,
|
||||
decimal.Zero)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var collateralCurrency currency.Code
|
||||
collateralCurrency, _, err = exch.GetCollateralCurrencyForContract(a, currency.NewPair(b, q))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = funds.LinkCollateralCurrency(futureItem, collateralCurrency)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = funds.AddItem(futureItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %v unsupported", errInvalidConfigAsset, a)
|
||||
}
|
||||
} else {
|
||||
var bFunds, qFunds decimal.Decimal
|
||||
if cfg.CurrencySettings[i].SpotDetails != nil {
|
||||
if cfg.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
|
||||
bFunds = *cfg.CurrencySettings[i].SpotDetails.InitialBaseFunds
|
||||
}
|
||||
if cfg.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil {
|
||||
qFunds = *cfg.CurrencySettings[i].SpotDetails.InitialQuoteFunds
|
||||
}
|
||||
}
|
||||
baseItem, err = funding.CreateItem(
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
a,
|
||||
curr.Base,
|
||||
bFunds,
|
||||
decimal.Zero)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quoteItem, err = funding.CreateItem(
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
a,
|
||||
curr.Quote,
|
||||
qFunds,
|
||||
decimal.Zero)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pair *funding.SpotPair
|
||||
pair, err = funding.CreatePair(baseItem, quoteItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = funds.AddPair(pair)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bt.Funding = funds
|
||||
var p *portfolio.Portfolio
|
||||
p, err = portfolio.Setup(sizeManager, portfolioRisk, cfg.StatisticSettings.RiskFreeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bt.Strategy, err = strategies.LoadStrategyByName(cfg.StrategySettings.Name, cfg.StrategySettings.SimultaneousSignalProcessing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bt.Strategy.SetDefaults()
|
||||
if cfg.StrategySettings.CustomSettings != nil {
|
||||
err = bt.Strategy.SetCustomSettings(cfg.StrategySettings.CustomSettings)
|
||||
if err != nil && !errors.Is(err, base.ErrCustomSettingsUnsupported) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
stats := &statistics.Statistic{
|
||||
StrategyName: bt.Strategy.Name(),
|
||||
StrategyNickname: cfg.Nickname,
|
||||
StrategyDescription: bt.Strategy.Description(),
|
||||
StrategyGoal: cfg.Goal,
|
||||
ExchangeAssetPairStatistics: make(map[string]map[asset.Item]map[currency.Pair]*statistics.CurrencyPairStatistic),
|
||||
RiskFreeRate: cfg.StatisticSettings.RiskFreeRate,
|
||||
CandleInterval: cfg.DataSettings.Interval,
|
||||
FundManager: bt.Funding,
|
||||
}
|
||||
bt.Statistic = stats
|
||||
reports.Statistics = stats
|
||||
|
||||
if !cfg.StrategySettings.DisableUSDTracking {
|
||||
var trackingPairs []trackingcurrencies.TrackingPair
|
||||
for i := range cfg.CurrencySettings {
|
||||
trackingPairs = append(trackingPairs, trackingcurrencies.TrackingPair{
|
||||
Exchange: cfg.CurrencySettings[i].ExchangeName,
|
||||
Asset: cfg.CurrencySettings[i].Asset,
|
||||
Base: cfg.CurrencySettings[i].Base,
|
||||
Quote: cfg.CurrencySettings[i].Quote,
|
||||
})
|
||||
}
|
||||
trackingPairs, err = trackingcurrencies.CreateUSDTrackingPairs(trackingPairs, bt.exchangeManager)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trackingPairCheck:
|
||||
for i := range trackingPairs {
|
||||
for j := range cfg.CurrencySettings {
|
||||
if cfg.CurrencySettings[j].ExchangeName == trackingPairs[i].Exchange &&
|
||||
cfg.CurrencySettings[j].Asset == trackingPairs[i].Asset &&
|
||||
cfg.CurrencySettings[j].Base.Equal(trackingPairs[i].Base) &&
|
||||
cfg.CurrencySettings[j].Quote.Equal(trackingPairs[i].Quote) {
|
||||
continue trackingPairCheck
|
||||
}
|
||||
}
|
||||
cfg.CurrencySettings = append(cfg.CurrencySettings, config.CurrencySettings{
|
||||
ExchangeName: trackingPairs[i].Exchange,
|
||||
Asset: trackingPairs[i].Asset,
|
||||
Base: trackingPairs[i].Base,
|
||||
Quote: trackingPairs[i].Quote,
|
||||
USDTrackingPair: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
e, err := bt.setupExchangeSettings(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bt.Exchange = &e
|
||||
for i := range e.CurrencySettings {
|
||||
err = p.SetupCurrencySettingsMap(&e.CurrencySettings[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
bt.Portfolio = p
|
||||
|
||||
cfg.PrintSetting()
|
||||
|
||||
return bt, nil
|
||||
}
|
||||
|
||||
func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange, error) {
|
||||
log.Infoln(common.Setup, "setting exchange settings...")
|
||||
resp := exchange.Exchange{}
|
||||
|
||||
for i := range cfg.CurrencySettings {
|
||||
exch, pair, a, err := bt.loadExchangePairAssetBase(
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
cfg.CurrencySettings[i].Base,
|
||||
cfg.CurrencySettings[i].Quote,
|
||||
cfg.CurrencySettings[i].Asset)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
exchangeName := strings.ToLower(exch.GetName())
|
||||
bt.Datas.Setup()
|
||||
klineData, err := bt.loadData(cfg, exch, pair, a, cfg.CurrencySettings[i].USDTrackingPair)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = bt.Funding.AddUSDTrackingData(klineData)
|
||||
if err != nil &&
|
||||
!errors.Is(err, trackingcurrencies.ErrCurrencyDoesNotContainsUSD) &&
|
||||
!errors.Is(err, funding.ErrUSDTrackingDisabled) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if cfg.CurrencySettings[i].USDTrackingPair {
|
||||
continue
|
||||
}
|
||||
|
||||
bt.Datas.SetDataForCurrency(exchangeName, a, pair, klineData)
|
||||
|
||||
var makerFee, takerFee decimal.Decimal
|
||||
if cfg.CurrencySettings[i].MakerFee != nil && cfg.CurrencySettings[i].MakerFee.GreaterThan(decimal.Zero) {
|
||||
makerFee = *cfg.CurrencySettings[i].MakerFee
|
||||
}
|
||||
if cfg.CurrencySettings[i].TakerFee != nil && cfg.CurrencySettings[i].TakerFee.GreaterThan(decimal.Zero) {
|
||||
takerFee = *cfg.CurrencySettings[i].TakerFee
|
||||
}
|
||||
if cfg.CurrencySettings[i].TakerFee == nil || cfg.CurrencySettings[i].MakerFee == nil {
|
||||
var apiMakerFee, apiTakerFee decimal.Decimal
|
||||
apiMakerFee, apiTakerFee = getFees(context.TODO(), exch, pair)
|
||||
if cfg.CurrencySettings[i].MakerFee == nil {
|
||||
makerFee = apiMakerFee
|
||||
cfg.CurrencySettings[i].MakerFee = &makerFee
|
||||
cfg.CurrencySettings[i].UsingExchangeMakerFee = true
|
||||
}
|
||||
if cfg.CurrencySettings[i].TakerFee == nil {
|
||||
takerFee = apiTakerFee
|
||||
cfg.CurrencySettings[i].TakerFee = &takerFee
|
||||
cfg.CurrencySettings[i].UsingExchangeTakerFee = true
|
||||
}
|
||||
}
|
||||
|
||||
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'",
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent,
|
||||
slippage.DefaultMaximumSlippagePercent)
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.IsZero() {
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MinimumSlippagePercent.LessThan(decimal.Zero) {
|
||||
log.Warnf(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
|
||||
}
|
||||
if cfg.CurrencySettings[i].MinimumSlippagePercent.IsZero() {
|
||||
cfg.CurrencySettings[i].MinimumSlippagePercent = slippage.DefaultMinimumSlippagePercent
|
||||
}
|
||||
if cfg.CurrencySettings[i].MaximumSlippagePercent.LessThan(cfg.CurrencySettings[i].MinimumSlippagePercent) {
|
||||
cfg.CurrencySettings[i].MaximumSlippagePercent = slippage.DefaultMaximumSlippagePercent
|
||||
}
|
||||
|
||||
realOrders := false
|
||||
if cfg.DataSettings.LiveData != nil {
|
||||
realOrders = cfg.DataSettings.LiveData.RealOrders
|
||||
}
|
||||
|
||||
buyRule := exchange.MinMax{
|
||||
MinimumSize: cfg.CurrencySettings[i].BuySide.MinimumSize,
|
||||
MaximumSize: cfg.CurrencySettings[i].BuySide.MaximumSize,
|
||||
MaximumTotal: cfg.CurrencySettings[i].BuySide.MaximumTotal,
|
||||
}
|
||||
sellRule := exchange.MinMax{
|
||||
MinimumSize: cfg.CurrencySettings[i].SellSide.MinimumSize,
|
||||
MaximumSize: cfg.CurrencySettings[i].SellSide.MaximumSize,
|
||||
MaximumTotal: cfg.CurrencySettings[i].SellSide.MaximumTotal,
|
||||
}
|
||||
|
||||
limits, err := exch.GetOrderExecutionLimits(a, pair)
|
||||
if err != nil && !errors.Is(err, gctorder.ErrExchangeLimitNotLoaded) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if limits != (gctorder.MinMaxLevel{}) {
|
||||
if !cfg.CurrencySettings[i].CanUseExchangeLimits {
|
||||
log.Warnf(common.Setup, "exchange %s order execution limits supported but disabled for %s %s, live results may differ",
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
pair,
|
||||
a)
|
||||
cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true
|
||||
}
|
||||
}
|
||||
var lev exchange.Leverage
|
||||
if cfg.CurrencySettings[i].FuturesDetails != nil {
|
||||
lev = exchange.Leverage{
|
||||
CanUseLeverage: cfg.CurrencySettings[i].FuturesDetails.Leverage.CanUseLeverage,
|
||||
MaximumLeverageRate: cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate,
|
||||
MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio,
|
||||
}
|
||||
}
|
||||
resp.CurrencySettings = append(resp.CurrencySettings, exchange.Settings{
|
||||
Exchange: exch,
|
||||
MinimumSlippageRate: cfg.CurrencySettings[i].MinimumSlippagePercent,
|
||||
MaximumSlippageRate: cfg.CurrencySettings[i].MaximumSlippagePercent,
|
||||
Pair: pair,
|
||||
Asset: a,
|
||||
MakerFee: makerFee,
|
||||
TakerFee: takerFee,
|
||||
UseRealOrders: realOrders,
|
||||
BuySide: buyRule,
|
||||
SellSide: sellRule,
|
||||
Leverage: lev,
|
||||
Limits: limits,
|
||||
SkipCandleVolumeFitting: cfg.CurrencySettings[i].SkipCandleVolumeFitting,
|
||||
CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits,
|
||||
UseExchangePNLCalculation: cfg.CurrencySettings[i].UseExchangePNLCalculation,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (bt *BackTest) loadExchangePairAssetBase(exch string, base, quote currency.Code, ai asset.Item) (gctexchange.IBotExchange, currency.Pair, asset.Item, error) {
|
||||
e, err := bt.exchangeManager.GetExchangeByName(exch)
|
||||
if err != nil {
|
||||
return nil, currency.EMPTYPAIR, asset.Empty, err
|
||||
}
|
||||
|
||||
var cp, fPair currency.Pair
|
||||
cp = currency.NewPair(base, quote)
|
||||
|
||||
exchangeBase := e.GetBase()
|
||||
if exchangeBase.ValidateAPICredentials(exchangeBase.GetDefaultCredentials()) != nil {
|
||||
log.Warnf(common.Setup, "no credentials set for %v, this is theoretical only", exchangeBase.Name)
|
||||
}
|
||||
|
||||
fPair, err = exchangeBase.FormatExchangeCurrency(cp, ai)
|
||||
if err != nil {
|
||||
return nil, currency.EMPTYPAIR, asset.Empty, err
|
||||
}
|
||||
return e, fPair, ai, nil
|
||||
}
|
||||
|
||||
// getFees will return an exchange's fee rate from GCT's wrapper function
|
||||
func getFees(ctx context.Context, exch gctexchange.IBotExchange, fPair currency.Pair) (makerFee, takerFee decimal.Decimal) {
|
||||
fTakerFee, err := exch.GetFeeByType(ctx,
|
||||
&gctexchange.FeeBuilder{FeeType: gctexchange.OfflineTradeFee,
|
||||
Pair: fPair,
|
||||
IsMaker: false,
|
||||
PurchasePrice: 1,
|
||||
Amount: 1,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf(common.Setup, "Could not retrieve taker fee for %v. %v", exch.GetName(), err)
|
||||
}
|
||||
|
||||
fMakerFee, err := exch.GetFeeByType(ctx,
|
||||
&gctexchange.FeeBuilder{
|
||||
FeeType: gctexchange.OfflineTradeFee,
|
||||
Pair: fPair,
|
||||
IsMaker: true,
|
||||
PurchasePrice: 1,
|
||||
Amount: 1,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf(common.Setup, "Could not retrieve maker fee for %v. %v", exch.GetName(), err)
|
||||
}
|
||||
|
||||
return decimal.NewFromFloat(fMakerFee), decimal.NewFromFloat(fTakerFee)
|
||||
}
|
||||
|
||||
// loadData will create kline data from the sources defined in start config files. It can exist from databases, csv or API endpoints
|
||||
// it can also be generated from trade data which will be converted into kline data
|
||||
func (bt *BackTest) loadData(cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
|
||||
if exch == nil {
|
||||
return nil, engine.ErrExchangeNotFound
|
||||
}
|
||||
b := exch.GetBase()
|
||||
if cfg.DataSettings.DatabaseData == nil &&
|
||||
cfg.DataSettings.LiveData == nil &&
|
||||
cfg.DataSettings.APIData == nil &&
|
||||
cfg.DataSettings.CSVData == nil {
|
||||
return nil, errNoDataSource
|
||||
}
|
||||
if (cfg.DataSettings.APIData != nil && cfg.DataSettings.DatabaseData != nil) ||
|
||||
(cfg.DataSettings.APIData != nil && cfg.DataSettings.LiveData != nil) ||
|
||||
(cfg.DataSettings.APIData != nil && cfg.DataSettings.CSVData != nil) ||
|
||||
(cfg.DataSettings.DatabaseData != nil && cfg.DataSettings.LiveData != nil) ||
|
||||
(cfg.DataSettings.CSVData != nil && cfg.DataSettings.LiveData != nil) ||
|
||||
(cfg.DataSettings.CSVData != nil && cfg.DataSettings.DatabaseData != nil) {
|
||||
return nil, errAmbiguousDataSource
|
||||
}
|
||||
|
||||
dataType, err := common.DataTypeToInt(cfg.DataSettings.DataType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof(common.Setup, "loading data for %v %v %v...\n", exch.GetName(), a, fPair)
|
||||
resp := &kline.DataFromKline{}
|
||||
switch {
|
||||
case cfg.DataSettings.CSVData != nil:
|
||||
if cfg.DataSettings.Interval <= 0 {
|
||||
return nil, errIntervalUnset
|
||||
}
|
||||
resp, err = csv.LoadData(
|
||||
dataType,
|
||||
cfg.DataSettings.CSVData.FullPath,
|
||||
strings.ToLower(exch.GetName()),
|
||||
cfg.DataSettings.Interval.Duration(),
|
||||
fPair,
|
||||
a,
|
||||
isUSDTrackingPair)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v. Please check your GoCryptoTrader configuration", err)
|
||||
}
|
||||
resp.Item.RemoveDuplicates()
|
||||
resp.Item.SortCandlesByTimestamp(false)
|
||||
resp.RangeHolder, err = gctkline.CalculateCandleDateRanges(
|
||||
resp.Item.Candles[0].Time,
|
||||
resp.Item.Candles[len(resp.Item.Candles)-1].Time.Add(cfg.DataSettings.Interval.Duration()),
|
||||
cfg.DataSettings.Interval,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.RangeHolder.SetHasDataFromCandles(resp.Item.Candles)
|
||||
summary := resp.RangeHolder.DataSummary(false)
|
||||
if len(summary) > 0 {
|
||||
log.Warnf(common.Setup, "%v", summary)
|
||||
}
|
||||
case cfg.DataSettings.DatabaseData != nil:
|
||||
if cfg.DataSettings.DatabaseData.InclusiveEndDate {
|
||||
cfg.DataSettings.DatabaseData.EndDate = cfg.DataSettings.DatabaseData.EndDate.Add(cfg.DataSettings.Interval.Duration())
|
||||
}
|
||||
if cfg.DataSettings.DatabaseData.Path == "" {
|
||||
cfg.DataSettings.DatabaseData.Path = filepath.Join(gctcommon.GetDefaultDataDir(runtime.GOOS), "database")
|
||||
}
|
||||
gctdatabase.DB.DataPath = cfg.DataSettings.DatabaseData.Path
|
||||
err = gctdatabase.DB.SetConfig(&cfg.DataSettings.DatabaseData.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = bt.databaseManager.Start(&sync.WaitGroup{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
stopErr := bt.databaseManager.Stop()
|
||||
if stopErr != nil {
|
||||
log.Error(common.Setup, stopErr)
|
||||
}
|
||||
}()
|
||||
resp, err = loadDatabaseData(cfg, exch.GetName(), fPair, a, dataType, isUSDTrackingPair)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to retrieve data from GoCryptoTrader database. Error: %v. Please ensure the database is setup correctly and has data before use", err)
|
||||
}
|
||||
|
||||
resp.Item.RemoveDuplicates()
|
||||
resp.Item.SortCandlesByTimestamp(false)
|
||||
resp.RangeHolder, err = gctkline.CalculateCandleDateRanges(
|
||||
cfg.DataSettings.DatabaseData.StartDate,
|
||||
cfg.DataSettings.DatabaseData.EndDate,
|
||||
cfg.DataSettings.Interval,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.RangeHolder.SetHasDataFromCandles(resp.Item.Candles)
|
||||
summary := resp.RangeHolder.DataSummary(false)
|
||||
if len(summary) > 0 {
|
||||
log.Warnf(common.Setup, "%v", summary)
|
||||
}
|
||||
case cfg.DataSettings.APIData != nil:
|
||||
if cfg.DataSettings.APIData.InclusiveEndDate {
|
||||
cfg.DataSettings.APIData.EndDate = cfg.DataSettings.APIData.EndDate.Add(cfg.DataSettings.Interval.Duration())
|
||||
}
|
||||
resp, err = loadAPIData(
|
||||
cfg,
|
||||
exch,
|
||||
fPair,
|
||||
a,
|
||||
b.Features.Enabled.Kline.ResultLimit,
|
||||
dataType)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
case cfg.DataSettings.LiveData != nil:
|
||||
if isUSDTrackingPair {
|
||||
return nil, errLiveUSDTrackingNotSupported
|
||||
}
|
||||
if len(cfg.CurrencySettings) > 1 {
|
||||
return nil, errors.New("live data simulation only supports one currency")
|
||||
}
|
||||
err = loadLiveData(cfg, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go bt.loadLiveDataLoop(
|
||||
resp,
|
||||
cfg,
|
||||
exch,
|
||||
fPair,
|
||||
a,
|
||||
dataType)
|
||||
return resp, nil
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("processing error, response returned nil")
|
||||
}
|
||||
|
||||
if a.IsFutures() {
|
||||
// returning the collateral currency along with using the
|
||||
// fPair base creates a pair that links the futures contract to
|
||||
// is underlying pair
|
||||
// eg BTC-PERP on FTX has a collateral currency of USD
|
||||
// taking the BTC base and USD as quote, allows linking
|
||||
// BTC-USD and BTC-PERP
|
||||
var curr currency.Code
|
||||
curr, _, err = exch.GetCollateralCurrencyForContract(a, fPair)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
resp.Item.UnderlyingPair = currency.NewPair(fPair.Base, curr)
|
||||
}
|
||||
|
||||
err = b.ValidateKline(fPair, a, resp.Item.Interval)
|
||||
if err != nil {
|
||||
if dataType != common.DataTrade || !strings.EqualFold(err.Error(), "interval not supported") {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = resp.Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bt.Reports.AddKlineItem(&resp.Item)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func loadDatabaseData(cfg *config.Config, name string, fPair currency.Pair, a asset.Item, dataType int64, isUSDTrackingPair bool) (*kline.DataFromKline, error) {
|
||||
if cfg == nil || cfg.DataSettings.DatabaseData == nil {
|
||||
return nil, errors.New("nil config data received")
|
||||
}
|
||||
if cfg.DataSettings.Interval <= 0 {
|
||||
return nil, errIntervalUnset
|
||||
}
|
||||
|
||||
return database.LoadData(
|
||||
cfg.DataSettings.DatabaseData.StartDate,
|
||||
cfg.DataSettings.DatabaseData.EndDate,
|
||||
cfg.DataSettings.Interval.Duration(),
|
||||
strings.ToLower(name),
|
||||
dataType,
|
||||
fPair,
|
||||
a,
|
||||
isUSDTrackingPair)
|
||||
}
|
||||
|
||||
func loadAPIData(cfg *config.Config, exch gctexchange.IBotExchange, fPair currency.Pair, a asset.Item, resultLimit uint32, dataType int64) (*kline.DataFromKline, error) {
|
||||
if cfg.DataSettings.Interval <= 0 {
|
||||
return nil, errIntervalUnset
|
||||
}
|
||||
dates, err := gctkline.CalculateCandleDateRanges(
|
||||
cfg.DataSettings.APIData.StartDate,
|
||||
cfg.DataSettings.APIData.EndDate,
|
||||
cfg.DataSettings.Interval,
|
||||
resultLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candles, err := api.LoadData(context.TODO(),
|
||||
dataType,
|
||||
cfg.DataSettings.APIData.StartDate,
|
||||
cfg.DataSettings.APIData.EndDate,
|
||||
cfg.DataSettings.Interval.Duration(),
|
||||
exch,
|
||||
fPair,
|
||||
a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v. Please check your GoCryptoTrader configuration", err)
|
||||
}
|
||||
dates.SetHasDataFromCandles(candles.Candles)
|
||||
summary := dates.DataSummary(false)
|
||||
if len(summary) > 0 {
|
||||
log.Warnf(common.Setup, "%v", summary)
|
||||
}
|
||||
candles.FillMissingDataWithEmptyEntries(dates)
|
||||
candles.RemoveOutsideRange(cfg.DataSettings.APIData.StartDate, cfg.DataSettings.APIData.EndDate)
|
||||
return &kline.DataFromKline{
|
||||
Item: *candles,
|
||||
RangeHolder: dates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadLiveData(cfg *config.Config, base *gctexchange.Base) error {
|
||||
if cfg == nil || base == nil || cfg.DataSettings.LiveData == nil {
|
||||
return common.ErrNilArguments
|
||||
}
|
||||
if cfg.DataSettings.Interval <= 0 {
|
||||
return errIntervalUnset
|
||||
}
|
||||
|
||||
if cfg.DataSettings.LiveData.APIKeyOverride != "" {
|
||||
base.API.SetKey(cfg.DataSettings.LiveData.APIKeyOverride)
|
||||
}
|
||||
if cfg.DataSettings.LiveData.APISecretOverride != "" {
|
||||
base.API.SetSecret(cfg.DataSettings.LiveData.APISecretOverride)
|
||||
}
|
||||
if cfg.DataSettings.LiveData.APIClientIDOverride != "" {
|
||||
base.API.SetClientID(cfg.DataSettings.LiveData.APIClientIDOverride)
|
||||
}
|
||||
if cfg.DataSettings.LiveData.API2FAOverride != "" {
|
||||
base.API.SetPEMKey(cfg.DataSettings.LiveData.API2FAOverride)
|
||||
}
|
||||
if cfg.DataSettings.LiveData.APISubAccountOverride != "" {
|
||||
base.API.SetSubAccount(cfg.DataSettings.LiveData.APISubAccountOverride)
|
||||
}
|
||||
|
||||
validated := base.AreCredentialsValid(context.TODO())
|
||||
base.API.AuthenticatedSupport = validated
|
||||
if !validated && cfg.DataSettings.LiveData.RealOrders {
|
||||
log.Warn(common.Setup, "invalid API credentials set, real orders set to false")
|
||||
cfg.DataSettings.LiveData.RealOrders = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user