mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* moves everything to use single map keys, also breaks * full rollout * tests * fix a little bug * minor test fixups * Fix Key use * rm 🔑 from 🔑 struct name
849 lines
23 KiB
Go
849 lines
23 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/common"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/data"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/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/base"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/kline"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/signal"
|
|
"github.com/thrasher-corp/gocryptotrader/backtester/funding"
|
|
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
|
|
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
// Reset BackTest values to default
|
|
func (bt *BackTest) Reset() error {
|
|
if bt == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
var err error
|
|
if bt.orderManager != nil {
|
|
err = bt.orderManager.Stop()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if bt.databaseManager != nil {
|
|
err = bt.databaseManager.Stop()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err = bt.EventQueue.Reset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.DataHolder.Reset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.Portfolio.Reset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.Statistic.Reset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.Exchange.Reset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.Funding.Reset()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bt.exchangeManager = nil
|
|
bt.orderManager = nil
|
|
bt.databaseManager = nil
|
|
return nil
|
|
}
|
|
|
|
// RunLive is a proof of concept function that does not yet support multi currency usage
|
|
// It tasks by constantly checking for new live datas and running through the list of events
|
|
// once new data is processed. It will run until application close event has been received
|
|
func (bt *BackTest) RunLive() error {
|
|
if bt.LiveDataHandler == nil {
|
|
return errLiveOnly
|
|
}
|
|
var err error
|
|
if bt.LiveDataHandler.IsRealOrders() {
|
|
err = bt.LiveDataHandler.UpdateFunding(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err = bt.LiveDataHandler.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bt.wg.Add(1)
|
|
go func() {
|
|
err = bt.liveCheck()
|
|
if err != nil {
|
|
log.Errorln(common.LiveStrategy, err)
|
|
}
|
|
bt.wg.Done()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (bt *BackTest) liveCheck() error {
|
|
for {
|
|
select {
|
|
case <-bt.shutdown:
|
|
return bt.LiveDataHandler.Stop()
|
|
case <-bt.LiveDataHandler.HasShutdownFromError():
|
|
return bt.Stop()
|
|
case <-bt.LiveDataHandler.HasShutdown():
|
|
return nil
|
|
case <-bt.LiveDataHandler.Updated():
|
|
err := bt.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExecuteStrategy executes the strategy using the provided configs
|
|
func (bt *BackTest) ExecuteStrategy(waitForOfflineCompletion bool) error {
|
|
if bt == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
bt.m.Lock()
|
|
if bt.MetaData.DateLoaded.IsZero() {
|
|
bt.m.Unlock()
|
|
return errNotSetup
|
|
}
|
|
if !bt.MetaData.Closed && !bt.MetaData.DateStarted.IsZero() {
|
|
bt.m.Unlock()
|
|
return fmt.Errorf("%w %v %v", errTaskIsRunning, bt.MetaData.ID, bt.MetaData.Strategy)
|
|
}
|
|
if bt.MetaData.Closed {
|
|
bt.m.Unlock()
|
|
return fmt.Errorf("%w %v %v", errAlreadyRan, bt.MetaData.ID, bt.MetaData.Strategy)
|
|
}
|
|
if waitForOfflineCompletion && bt.MetaData.LiveTesting {
|
|
bt.m.Unlock()
|
|
return fmt.Errorf("%w cannot wait for a live task to finish", errCannotHandleRequest)
|
|
}
|
|
|
|
bt.MetaData.DateStarted = time.Now()
|
|
liveTesting := bt.MetaData.LiveTesting
|
|
bt.m.Unlock()
|
|
|
|
var err error
|
|
switch {
|
|
case waitForOfflineCompletion && !liveTesting:
|
|
err = bt.Run()
|
|
if err != nil {
|
|
log.Errorln(common.Backtester, err)
|
|
}
|
|
return bt.Stop()
|
|
case !waitForOfflineCompletion && liveTesting:
|
|
return bt.RunLive()
|
|
case !waitForOfflineCompletion && !liveTesting:
|
|
go func() {
|
|
err = bt.Run()
|
|
if err != nil {
|
|
log.Errorln(common.Backtester, err)
|
|
}
|
|
err = bt.Stop()
|
|
if err != nil {
|
|
log.Errorln(common.Backtester, err)
|
|
}
|
|
}()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run will iterate over loaded data events
|
|
// save them and then handle the event based on its type
|
|
func (bt *BackTest) Run() error {
|
|
// doubleNil allows the run function to exit if no new data is detected on a live run
|
|
var doubleNil bool
|
|
if bt.MetaData.DateLoaded.IsZero() {
|
|
return errNotSetup
|
|
}
|
|
for ev := bt.EventQueue.NextEvent(); ; ev = bt.EventQueue.NextEvent() {
|
|
if ev == nil {
|
|
if bt.hasShutdown {
|
|
return nil
|
|
}
|
|
if doubleNil {
|
|
if bt.verbose {
|
|
log.Infoln(common.Backtester, "No new data on second check")
|
|
}
|
|
return nil
|
|
}
|
|
doubleNil = true
|
|
dataHandlers, err := bt.DataHolder.GetAllData()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range dataHandlers {
|
|
var e data.Event
|
|
e, err = dataHandlers[i].Next()
|
|
if err != nil {
|
|
if errors.Is(err, data.ErrEndOfData) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
if e == nil {
|
|
if !bt.hasProcessedAnEvent && bt.LiveDataHandler == nil {
|
|
var (
|
|
exch string
|
|
assetItem asset.Item
|
|
cp currency.Pair
|
|
)
|
|
exch, assetItem, cp, err = dataHandlers[i].GetDetails()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Errorf(common.Backtester, "Unable to perform `Next` for %v %v %v", exch, assetItem, cp)
|
|
}
|
|
return nil
|
|
}
|
|
o := e.GetOffset()
|
|
if bt.Strategy.UsingSimultaneousProcessing() && bt.hasProcessedDataAtOffset[o] {
|
|
// only append one event, as simultaneous processing
|
|
// will retrieve all relevant events to process under
|
|
// processSimultaneousDataEvents()
|
|
continue
|
|
}
|
|
bt.EventQueue.AppendEvent(e)
|
|
if !bt.hasProcessedDataAtOffset[o] {
|
|
bt.hasProcessedDataAtOffset[o] = true
|
|
}
|
|
}
|
|
} else {
|
|
doubleNil = false
|
|
err := bt.handleEvent(ev)
|
|
if err != nil {
|
|
log.Errorln(common.Backtester, err)
|
|
}
|
|
if !bt.hasProcessedAnEvent {
|
|
bt.hasProcessedAnEvent = 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.Event) error {
|
|
if ev == nil {
|
|
return fmt.Errorf("cannot handle event %w", errNilData)
|
|
}
|
|
|
|
funds, err := bt.Funding.GetFundingForEvent(ev)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch eType := ev.(type) {
|
|
case kline.Event:
|
|
// using kline.Event as signal.Event also matches data.Event
|
|
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())
|
|
if bt.LiveDataHandler != nil {
|
|
// output log data per interval instead of at the end
|
|
result, logErr := bt.Statistic.CreateLog(eType)
|
|
if logErr != nil {
|
|
return logErr
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Infoln(common.LiveStrategy, result)
|
|
}
|
|
default:
|
|
err = fmt.Errorf("handleEvent %w %T received, could not process",
|
|
errUnhandledDatatype,
|
|
ev)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return bt.Funding.CreateSnapshot(ev.GetTime())
|
|
}
|
|
|
|
// processSingleDataEvent will pass the event to the strategy and determine how it should be handled
|
|
func (bt *BackTest) processSingleDataEvent(ev data.Event, funds funding.IFundReleaser) error {
|
|
err := bt.updateStatsForDataEvent(ev, funds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d, err := bt.DataHolder.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 {
|
|
dataHolders, err := bt.DataHolder.GetAllData()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dataEvents := make([]data.Handler, 0, len(dataHolders))
|
|
for i := range dataHolders {
|
|
var latestData data.Event
|
|
latestData, err = dataHolders[i].Latest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var funds funding.IFundingPair
|
|
funds, err = bt.Funding.GetFundingForEvent(latestData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.updateStatsForDataEvent(latestData, funds.FundReleaser())
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, statistics.ErrAlreadyProcessed):
|
|
if !bt.MetaData.Closed || !bt.MetaData.ClosePositionsOnStop {
|
|
// Closing positions on close reuses existing events and doesn't need to be logged
|
|
// any other scenario, this should be logged
|
|
log.Warnf(common.LiveStrategy, "%v %v", latestData.GetOffset(), err)
|
|
}
|
|
continue
|
|
case errors.Is(err, futures.ErrPositionLiquidated):
|
|
return nil
|
|
default:
|
|
log.Errorln(common.Backtester, err)
|
|
}
|
|
}
|
|
dataEvents = append(dataEvents, dataHolders[i])
|
|
}
|
|
signals, err := bt.Strategy.OnSimultaneousSignals(dataEvents, bt.Funding, bt.Portfolio)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, base.ErrTooMuchBadData):
|
|
// too much bad data is a severe error and backtesting must cease
|
|
return err
|
|
case errors.Is(err, base.ErrNoDataToProcess) && bt.MetaData.Closed && bt.MetaData.ClosePositionsOnStop:
|
|
// event queue is being cleared with no data events to process
|
|
return nil
|
|
default:
|
|
log.Errorf(common.Backtester, "OnSimultaneousSignals %v", err)
|
|
return nil
|
|
}
|
|
}
|
|
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 data.Event, funds funding.IFundReleaser) error {
|
|
if ev == nil {
|
|
return common.ErrNilEvent
|
|
}
|
|
if funds == nil {
|
|
return fmt.Errorf("%v %v %v %w missing fund releaser", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), gctcommon.ErrNilPointer)
|
|
}
|
|
// update statistics with the latest price
|
|
err := bt.Statistic.SetEventForOffset(ev)
|
|
if err != nil {
|
|
if errors.Is(err, statistics.ErrAlreadyProcessed) {
|
|
return err
|
|
}
|
|
log.Errorf(common.Backtester, "SetEventForOffset %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, futures.ErrPositionNotFound) {
|
|
// if there is no position yet, there's nothing to update
|
|
return nil
|
|
}
|
|
if !errors.Is(err, futures.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
|
|
}
|
|
if bt.LiveDataHandler == nil || (bt.LiveDataHandler != nil && !bt.LiveDataHandler.IsRealOrders()) {
|
|
err = bt.Portfolio.CheckLiquidationStatus(ev, cr, pnl)
|
|
if err != nil {
|
|
if errors.Is(err, futures.ErrPositionLiquidated) {
|
|
liquidErr := bt.triggerLiquidationsForExchange(ev, pnl)
|
|
if liquidErr != nil {
|
|
return liquidErr
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
return bt.Statistic.AddPNLForTime(pnl)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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", gctcommon.ErrNilPointer)
|
|
}
|
|
cs, err := bt.Exchange.GetCurrencySettings(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
|
if err != nil {
|
|
log.Errorf(common.Backtester, "GetCurrencySettings %v", err)
|
|
return fmt.Errorf("GetCurrencySettings %v", err)
|
|
}
|
|
var o *order.Order
|
|
o, err = bt.Portfolio.OnSignal(ev, &cs, funds)
|
|
if err != nil {
|
|
log.Errorf(common.Backtester, "OnSignal %v", 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", 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", gctcommon.ErrNilPointer)
|
|
}
|
|
d, err := bt.DataHolder.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 {
|
|
_, 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.Funding.UpdateCollateralForEvent(ev, false)
|
|
if err != nil {
|
|
return fmt.Errorf("UpdateCollateralForEvent %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
holding, err := bt.Portfolio.ViewHoldingAtTimePeriod(ev)
|
|
if err != nil {
|
|
log.Errorln(common.Backtester, err)
|
|
}
|
|
err = bt.Statistic.AddHoldingsForTime(holding)
|
|
if err != nil {
|
|
log.Errorf(common.Backtester, "AddHoldingsForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
|
|
snap, err := bt.Portfolio.GetLatestComplianceSnapshot(ev.GetExchange(), ev.GetAssetType(), ev.Pair())
|
|
if err != nil {
|
|
log.Errorf(common.Backtester, "GetLatestComplianceSnapshot %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
err = bt.Statistic.AddComplianceSnapshotForTime(snap, ev)
|
|
if err != nil {
|
|
log.Errorf(common.Backtester, "AddComplianceSnapshotForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
|
|
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 {
|
|
return nil
|
|
}
|
|
pnl, err := bt.Portfolio.TrackFuturesOrder(ev, funds)
|
|
if err != nil && !errors.Is(err, gctorder.ErrSubmissionIsNil) {
|
|
return fmt.Errorf("TrackFuturesOrder %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
|
|
var exch gctexchange.IBotExchange
|
|
exch, err = bt.exchangeManager.GetExchangeByName(ev.GetExchange())
|
|
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 {
|
|
return fmt.Errorf("AddPNLForTime %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
err = bt.Funding.UpdateCollateralForEvent(ev, false)
|
|
if err != nil {
|
|
return fmt.Errorf("UpdateCollateralForEvent %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Stop shuts down the live data loop
|
|
func (bt *BackTest) Stop() error {
|
|
if bt == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
bt.m.Lock()
|
|
defer bt.m.Unlock()
|
|
if bt.MetaData.Closed {
|
|
return errAlreadyRan
|
|
}
|
|
close(bt.shutdown)
|
|
bt.MetaData.Closed = true
|
|
bt.MetaData.DateEnded = time.Now()
|
|
if bt.MetaData.ClosePositionsOnStop {
|
|
err := bt.CloseAllPositions()
|
|
if err != nil {
|
|
log.Errorf(common.Backtester, "Could not close all positions on stop: %s", err)
|
|
}
|
|
}
|
|
if !bt.hasProcessedAnEvent {
|
|
return nil
|
|
}
|
|
err := bt.Statistic.CalculateAllResults()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.Reports.GenerateReport()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bt *BackTest) triggerLiquidationsForExchange(ev data.Event, pnl *portfolio.PNLSummary) error {
|
|
if ev == nil {
|
|
return common.ErrNilEvent
|
|
}
|
|
if pnl == nil {
|
|
return fmt.Errorf("%w pnl summary", gctcommon.ErrNilPointer)
|
|
}
|
|
orders, err := bt.Portfolio.CreateLiquidationOrdersForExchange(ev, bt.Funding)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range orders {
|
|
// these orders are raising events for event offsets
|
|
// which may not have been processed yet
|
|
// this will create and store stats for each order
|
|
// then liquidate it at the funding level
|
|
var datas data.Handler
|
|
datas, err = bt.DataHolder.GetDataForCurrency(orders[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var latest data.Event
|
|
latest, err = datas.Latest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.Statistic.SetEventForOffset(latest)
|
|
if err != nil && !errors.Is(err, statistics.ErrAlreadyProcessed) {
|
|
return err
|
|
}
|
|
bt.EventQueue.AppendEvent(orders[i])
|
|
err = bt.Statistic.SetEventForOffset(orders[i])
|
|
if err != nil {
|
|
log.Errorf(common.Backtester, "SetEventForOffset %v %v %v %v", ev.GetExchange(), ev.GetAssetType(), ev.Pair(), err)
|
|
}
|
|
err = bt.Funding.Liquidate(orders[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
pnl.Result.IsLiquidated = true
|
|
pnl.Result.Status = gctorder.Liquidated
|
|
return bt.Statistic.AddPNLForTime(pnl)
|
|
}
|
|
|
|
// CloseAllPositions will close sell any positions held on closure
|
|
// can only be with live testing and where a strategy supports it
|
|
func (bt *BackTest) CloseAllPositions() error {
|
|
if bt.LiveDataHandler == nil {
|
|
return errLiveOnly
|
|
}
|
|
err := bt.LiveDataHandler.UpdateFunding(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dataHolders, err := bt.DataHolder.GetAllData()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
latestPrices := make([]data.Event, len(dataHolders))
|
|
for i := range dataHolders {
|
|
var latest data.Event
|
|
latest, err = dataHolders[i].Latest()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
latestPrices[i] = latest
|
|
}
|
|
events, err := bt.Strategy.CloseAllPositions(bt.Portfolio.GetLatestHoldingsForAllCurrencies(), latestPrices)
|
|
if err != nil {
|
|
if errors.Is(err, gctcommon.ErrFunctionNotSupported) {
|
|
log.Warnf(common.LiveStrategy, "Closing all positions is not supported by strategy %v", bt.Strategy.Name())
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
if len(events) == 0 {
|
|
return nil
|
|
}
|
|
err = bt.LiveDataHandler.SetDataForClosingAllPositions(events...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range events {
|
|
k := events[i].ToKline()
|
|
err = bt.Statistic.SetEventForOffset(k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bt.EventQueue.AppendEvent(events[i])
|
|
}
|
|
err = bt.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = bt.LiveDataHandler.UpdateFunding(true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = bt.Funding.CreateSnapshot(events[0].GetTime())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range events {
|
|
var funds funding.IFundingPair
|
|
funds, err = bt.Funding.GetFundingForEvent(events[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = bt.Portfolio.SetHoldingsForEvent(funds.FundReader(), events[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
her := bt.Portfolio.GetLatestHoldingsForAllCurrencies()
|
|
for i := range her {
|
|
err = bt.Statistic.AddHoldingsForTime(&her[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateSummary creates a summary of a strategy task
|
|
// this summary contains many details of a task
|
|
func (bt *BackTest) GenerateSummary() (*TaskSummary, error) {
|
|
if bt == nil {
|
|
return nil, gctcommon.ErrNilPointer
|
|
}
|
|
bt.m.Lock()
|
|
defer bt.m.Unlock()
|
|
return &TaskSummary{
|
|
MetaData: bt.MetaData,
|
|
}, nil
|
|
}
|
|
|
|
// SetupMetaData will populate metadata fields
|
|
func (bt *BackTest) SetupMetaData() error {
|
|
if bt == nil {
|
|
return gctcommon.ErrNilPointer
|
|
}
|
|
bt.m.Lock()
|
|
defer bt.m.Unlock()
|
|
if !bt.MetaData.ID.IsNil() && !bt.MetaData.DateLoaded.IsZero() {
|
|
// already setup
|
|
return nil
|
|
}
|
|
id, err := uuid.NewV4()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bt.MetaData.ID = id
|
|
bt.MetaData.DateLoaded = time.Now()
|
|
return nil
|
|
}
|
|
|
|
// IsRunning checks if the task is running
|
|
func (bt *BackTest) IsRunning() bool {
|
|
if bt == nil {
|
|
return false
|
|
}
|
|
bt.m.Lock()
|
|
defer bt.m.Unlock()
|
|
return !bt.MetaData.DateStarted.IsZero() && !bt.MetaData.Closed
|
|
}
|
|
|
|
// HasRan checks if the task has been executed
|
|
func (bt *BackTest) HasRan() bool {
|
|
if bt == nil {
|
|
return false
|
|
}
|
|
bt.m.Lock()
|
|
defer bt.m.Unlock()
|
|
return bt.MetaData.Closed
|
|
}
|
|
|
|
// Equal checks if the incoming task matches
|
|
func (bt *BackTest) Equal(bt2 *BackTest) bool {
|
|
if bt == nil || bt2 == nil {
|
|
return false
|
|
}
|
|
bt.m.Lock()
|
|
btM := bt.MetaData
|
|
bt.m.Unlock()
|
|
// if they are actually the same pointer
|
|
// locks must be handled separately
|
|
bt2.m.Lock()
|
|
btM2 := bt2.MetaData
|
|
bt2.m.Unlock()
|
|
return btM == btM2
|
|
}
|
|
|
|
// MatchesID checks if the backtesting run's ID matches the supplied
|
|
func (bt *BackTest) MatchesID(id uuid.UUID) bool {
|
|
if bt == nil {
|
|
return false
|
|
}
|
|
if id.IsNil() {
|
|
return false
|
|
}
|
|
bt.m.Lock()
|
|
defer bt.m.Unlock()
|
|
if bt.MetaData.ID.IsNil() {
|
|
return false
|
|
}
|
|
return bt.MetaData.ID == id
|
|
}
|