Files
gocryptotrader/backtester/engine/backtest.go
Scott 91d699be9d maps: expansion of Key concept (#1349)
* 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
2023-10-04 10:19:41 +11:00

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
}