Files
gocryptotrader/backtester/config/strategyconfig.go
Scott 017cdf1384 Backtester: Live trading upgrades (#1023)
* Modifications for a smoother live run

* Fixes data appending

* Successfully allows multi-currency live trading. Adds multiple currencies to live DCA strategy

* Attempting to get cash and carry working

* Poor attempts at sorting out data and appending it properly with USD in mind

* =designs new live data handler

* Updates cash and carry strat to work

* adds test coverage. begins closeallpositions function

* Updates cash and carry to work live

* New kline.Event type. Cancels orders on close. Rn types

* =Fixes USD funding issue

* =fixes tests

* fixes tests AGAIN

* adds coverage to close all orders

* crummy tests, should override

* more tests

* more tests

* more coverage

* removes scourge of currency.Pair maps. More tests

* missed currency stuff

* Fixes USD data issue & collateral issue. Needs to close ALL orders

* Now triggers updates on the very first data entry

* All my problems are solved now????

* fixes tests, extends coverage

* there is some really funky candle stuff going on

* my brain is melting

* better shutdown management, fixes freezing bug

* fixes data duplication issues, adds retries to requests

* reduces logging, adds verbose options

* expands coverage over all new functionality

* fixes fun bug from curr == curr to curr.Equal(curr)

* fixes setup issues and tests

* starts adding external wallet amounts for funding

* more setup for assets

* setup live fund calcs and placing orders

* successfully performs automated cash and carry

* merge fixes

* funding properly set at all times

* fixes some bugs, need to address currencystatistics still

* adds 'appeneded' trait, attempts to fix some stats

* fixes stat bugs, adds cool new fetchfees feature

* fixes terrible processing bugs

* tightens realorder stats, sadly loses some live stats

* this actually sets everything correctly for bothcd ..cd ..cd ..cd ..cd ..!

* fix tests

* coverage

* beautiful new test coverage

* docs

* adds new fee getter delayer

* commits from the correct directory

* Lint

* adds verbose to fund manager

* Fix bug in t2b2 strat. Update dca live config. Docs

* go mod tidy

* update buf

* buf + test improvement

* Post merge fixes

* fixes surprise offset bug

* fix sizing restrictions for cash and carry

* fix server lints

* merge fixes

* test fixesss

* lintle fixles

* slowloris

* rn run to task, bug fixes, close all on close

* rpc lint and fixes

* bugfix: order manager not processing orders properly

* somewhat addresses nits

* absolutely broken end of day commit

* absolutely massive knockon effects from nits

* massive knockon effects continue

* fixes things

* address remaining nits

* jk now fixes things

* addresses the easier nits

* more nit fixers

* more niterinos addressederinos

* refactors holdings and does some nits

* so buf

* addresses some nits, fixes holdings bugs

* cleanup

* attempts to fix alert chans to prevent many chans waiting?

* terrible code, will revert

* to be reviewed in detail tomorrow

* Fixes up channel system

* smashes those nits

* fixes extra candles, fixes collateral bug, tests

* fixes data races, introduces reflection

* more checks n tests

* Fixes cash and carry issues. Fixes more cool bugs

* fixes ~typer~ typo

* replace spot strats from ftx to binance

* fixes all the tests I just destroyed

* removes example path, rm verbose

* 1) what 2) removes FTX references from the Backtester

* renamed, non-working strategies

* Removes FTX references almost as fast as sbf removes funds

* regen docs, add contrib names,sort contrib names

* fixes merge renamings

* Addresses nits. Fixes setting API credentials. Fixes Binance limit retrieval

* Fixes live order bugs with real orders and without

* Apply suggestions from code review

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/engine/live.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/engine/live.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update backtester/config/strategyconfigbuilder/main.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* updates docs

* even better docs

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
2023-01-05 13:03:17 +11:00

336 lines
14 KiB
Go

package config
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies"
"github.com/thrasher-corp/gocryptotrader/backtester/eventhandlers/strategies/base"
gctcommon "github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/log"
)
// ReadStrategyConfigFromFile will take a config from a path
func ReadStrategyConfigFromFile(path string) (*Config, error) {
if !file.Exists(path) {
return nil, fmt.Errorf("%w %v", common.ErrFileNotFound, path)
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var resp *Config
err = json.Unmarshal(data, &resp)
return resp, err
}
// Validate checks all config settings
func (c *Config) Validate() error {
if c == nil {
return fmt.Errorf("%w config", gctcommon.ErrNilPointer)
}
err := c.validateDate()
if err != nil {
return err
}
err = c.validateStrategySettings()
if err != nil {
return err
}
err = c.validateCurrencySettings()
if err != nil {
return err
}
return c.validateMinMaxes()
}
// validate ensures no one sets bad config values on purpose
func (m *MinMax) validate() error {
if m.MaximumSize.IsNegative() {
return fmt.Errorf("invalid maximum size %w", errSizeLessThanZero)
}
if m.MinimumSize.IsNegative() {
return fmt.Errorf("invalid minimum size %w", errSizeLessThanZero)
}
if m.MaximumTotal.IsNegative() {
return fmt.Errorf("invalid maximum total set to %w", errSizeLessThanZero)
}
if m.MaximumSize.LessThan(m.MinimumSize) && !m.MinimumSize.IsZero() && !m.MaximumSize.IsZero() {
return fmt.Errorf("%w maximum size %v vs minimum size %v",
errMaxSizeMinSizeMismatch,
m.MaximumSize,
m.MinimumSize)
}
if m.MaximumSize.Equal(m.MinimumSize) && !m.MinimumSize.IsZero() && !m.MaximumSize.IsZero() {
return fmt.Errorf("%w %v",
errMinMaxEqual,
m.MinimumSize)
}
return nil
}
func (c *Config) validateMinMaxes() (err error) {
for i := range c.CurrencySettings {
err = c.CurrencySettings[i].BuySide.validate()
if err != nil {
return err
}
err = c.CurrencySettings[i].SellSide.validate()
if err != nil {
return err
}
}
err = c.PortfolioSettings.BuySide.validate()
if err != nil {
return err
}
err = c.PortfolioSettings.SellSide.validate()
if err != nil {
return err
}
return nil
}
func (c *Config) validateStrategySettings() error {
if c.FundingSettings.UseExchangeLevelFunding && !c.StrategySettings.SimultaneousSignalProcessing {
return errSimultaneousProcessingRequired
}
if len(c.FundingSettings.ExchangeLevelFunding) > 0 && !c.FundingSettings.UseExchangeLevelFunding {
return errExchangeLevelFundingRequired
}
if c.FundingSettings.UseExchangeLevelFunding && len(c.FundingSettings.ExchangeLevelFunding) == 0 {
return errExchangeLevelFundingDataRequired
}
if c.FundingSettings.UseExchangeLevelFunding {
for i := range c.FundingSettings.ExchangeLevelFunding {
if c.FundingSettings.ExchangeLevelFunding[i].InitialFunds.IsNegative() {
return fmt.Errorf("%w for %v %v %v",
errBadInitialFunds,
c.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
c.FundingSettings.ExchangeLevelFunding[i].Asset,
c.FundingSettings.ExchangeLevelFunding[i].Currency,
)
}
}
}
strats := strategies.GetSupportedStrategies()
for i := range strats {
if strings.EqualFold(strats[i].Name(), c.StrategySettings.Name) {
return nil
}
}
return fmt.Errorf("strategty %v %w", c.StrategySettings.Name, base.ErrStrategyNotFound)
}
// validateDate checks whether someone has set a date poorly in their config
func (c *Config) validateDate() error {
if c.DataSettings.DatabaseData != nil {
if err := gctcommon.StartEndTimeCheck(c.DataSettings.DatabaseData.StartDate, c.DataSettings.DatabaseData.EndDate); err != nil {
return err
}
}
if c.DataSettings.APIData != nil {
if err := gctcommon.StartEndTimeCheck(c.DataSettings.APIData.StartDate, c.DataSettings.APIData.EndDate); err != nil {
return err
}
}
return nil
}
// validateCurrencySettings checks whether someone has set invalid currency setting data in their config
func (c *Config) validateCurrencySettings() error {
if len(c.CurrencySettings) == 0 {
return errNoCurrencySettings
}
var hasFutures, hasSlippage bool
for i := range c.CurrencySettings {
if c.CurrencySettings[i].Asset == asset.PerpetualSwap ||
c.CurrencySettings[i].Asset == asset.PerpetualContract {
return errPerpetualsUnsupported
}
if c.CurrencySettings[i].Asset.IsFutures() {
hasFutures = true
if c.CurrencySettings[i].Quote.String() == "PERP" || c.CurrencySettings[i].Base.String() == "PI" {
return errPerpetualsUnsupported
}
}
if c.CurrencySettings[i].SpotDetails != nil {
if c.FundingSettings.UseExchangeLevelFunding {
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds.GreaterThan(decimal.Zero) {
return fmt.Errorf("non-nil quote %w", errBadInitialFunds)
}
if c.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds.GreaterThan(decimal.Zero) {
return fmt.Errorf("non-nil base %w", errBadInitialFunds)
}
} else {
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds == nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds == nil {
return fmt.Errorf("nil base and quote %w", errBadInitialFunds)
}
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil &&
c.CurrencySettings[i].SpotDetails.InitialBaseFunds.IsZero() &&
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds.IsZero() {
return fmt.Errorf("base or quote funds set to zero %w", errBadInitialFunds)
}
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds == nil {
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds = &decimal.Zero
}
if c.CurrencySettings[i].SpotDetails.InitialBaseFunds == nil {
c.CurrencySettings[i].SpotDetails.InitialBaseFunds = &decimal.Zero
}
}
}
if c.CurrencySettings[i].Base.IsEmpty() {
return errUnsetCurrency
}
if !c.CurrencySettings[i].Asset.IsValid() {
return fmt.Errorf("%v %w", c.CurrencySettings[i].Asset, asset.ErrNotSupported)
}
if c.CurrencySettings[i].ExchangeName == "" {
return errUnsetExchange
}
if !c.CurrencySettings[i].MinimumSlippagePercent.IsZero() ||
!c.CurrencySettings[i].MaximumSlippagePercent.IsZero() {
hasSlippage = true
}
if c.CurrencySettings[i].MinimumSlippagePercent.LessThan(decimal.Zero) ||
c.CurrencySettings[i].MaximumSlippagePercent.LessThan(decimal.Zero) ||
c.CurrencySettings[i].MinimumSlippagePercent.GreaterThan(c.CurrencySettings[i].MaximumSlippagePercent) {
return errBadSlippageRates
}
c.CurrencySettings[i].ExchangeName = strings.ToLower(c.CurrencySettings[i].ExchangeName)
}
if hasSlippage && hasFutures {
return fmt.Errorf("%w futures sizing currently incompatible with slippage", errFeatureIncompatible)
}
return nil
}
// PrintSetting prints relevant settings to the console for easy reading
func (c *Config) PrintSetting() {
log.Info(common.Config, common.CMDColours.H1+"------------------Backtester Settings------------------------"+common.CMDColours.Default)
log.Info(common.Config, common.CMDColours.H2+"------------------Strategy Settings--------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Strategy: %s", c.StrategySettings.Name)
if len(c.StrategySettings.CustomSettings) > 0 {
log.Info(common.Config, "Custom strategy variables:")
for k, v := range c.StrategySettings.CustomSettings {
log.Infof(common.Config, "%s: %v", k, v)
}
} else {
log.Info(common.Config, "Custom strategy variables: unset")
}
log.Infof(common.Config, "Simultaneous Signal Processing: %v", c.StrategySettings.SimultaneousSignalProcessing)
log.Infof(common.Config, "USD value tracking: %v", !c.StrategySettings.DisableUSDTracking)
if c.FundingSettings.UseExchangeLevelFunding && c.StrategySettings.SimultaneousSignalProcessing {
log.Info(common.Config, common.CMDColours.H2+"------------------Funding Settings---------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Use Exchange Level Funding: %v", c.FundingSettings.UseExchangeLevelFunding)
if c.DataSettings.LiveData != nil && c.DataSettings.LiveData.RealOrders {
log.Infof(common.Config, "Funding levels will be set by the exchange")
} else {
for i := range c.FundingSettings.ExchangeLevelFunding {
log.Infof(common.Config, "Initial funds for %v %v %v: %v",
c.FundingSettings.ExchangeLevelFunding[i].ExchangeName,
c.FundingSettings.ExchangeLevelFunding[i].Asset,
c.FundingSettings.ExchangeLevelFunding[i].Currency,
c.FundingSettings.ExchangeLevelFunding[i].InitialFunds.Round(8))
}
}
}
for i := range c.CurrencySettings {
currStr := fmt.Sprintf(common.CMDColours.H2+"------------------%v %v-%v Currency Settings---------------------------------------------------------"+common.CMDColours.Default,
c.CurrencySettings[i].Asset,
c.CurrencySettings[i].Base,
c.CurrencySettings[i].Quote)
log.Infof(common.Config, currStr[:61])
log.Infof(common.Config, "Exchange: %v", c.CurrencySettings[i].ExchangeName)
switch {
case c.DataSettings.LiveData != nil && c.DataSettings.LiveData.RealOrders:
log.Infof(common.Config, "Funding levels will be set by the exchange")
case !c.FundingSettings.UseExchangeLevelFunding && c.CurrencySettings[i].SpotDetails != nil:
if c.CurrencySettings[i].SpotDetails.InitialBaseFunds != nil {
log.Infof(common.Config, "Initial base funds: %v %v",
c.CurrencySettings[i].SpotDetails.InitialBaseFunds.Round(8),
c.CurrencySettings[i].Base)
}
if c.CurrencySettings[i].SpotDetails.InitialQuoteFunds != nil {
log.Infof(common.Config, "Initial quote funds: %v %v",
c.CurrencySettings[i].SpotDetails.InitialQuoteFunds.Round(8),
c.CurrencySettings[i].Quote)
}
}
if c.CurrencySettings[i].TakerFee != nil {
if c.CurrencySettings[i].UsingExchangeTakerFee {
log.Infof(common.Config, "Taker fee: Using Exchange's API default taker rate: %v", c.CurrencySettings[i].TakerFee.Round(8))
} else {
log.Infof(common.Config, "Taker fee: %v", c.CurrencySettings[i].TakerFee.Round(8))
}
}
if c.CurrencySettings[i].MakerFee != nil {
if c.CurrencySettings[i].UsingExchangeMakerFee {
log.Infof(common.Config, "Maker fee: Using Exchange's API default maker rate: %v", c.CurrencySettings[i].MakerFee.Round(8))
} else {
log.Infof(common.Config, "Maker fee: %v", c.CurrencySettings[i].MakerFee.Round(8))
}
}
log.Infof(common.Config, "Minimum slippage percent: %v", c.CurrencySettings[i].MinimumSlippagePercent.Round(8))
log.Infof(common.Config, "Maximum slippage percent: %v", c.CurrencySettings[i].MaximumSlippagePercent.Round(8))
log.Infof(common.Config, "Buy rules: %+v", c.CurrencySettings[i].BuySide)
log.Infof(common.Config, "Sell rules: %+v", c.CurrencySettings[i].SellSide)
if c.CurrencySettings[i].FuturesDetails != nil && c.CurrencySettings[i].Asset == asset.Futures {
log.Infof(common.Config, "Leverage rules: %+v", c.CurrencySettings[i].FuturesDetails.Leverage)
}
log.Infof(common.Config, "Can use exchange defined order execution limits: %+v", c.CurrencySettings[i].CanUseExchangeLimits)
}
log.Info(common.Config, common.CMDColours.H2+"------------------Portfolio Settings-------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Buy rules: %+v", c.PortfolioSettings.BuySide)
log.Infof(common.Config, "Sell rules: %+v", c.PortfolioSettings.SellSide)
log.Infof(common.Config, "Leverage rules: %+v", c.PortfolioSettings.Leverage)
if c.DataSettings.LiveData != nil {
log.Info(common.Config, common.CMDColours.H2+"------------------Live Settings------------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "Using real orders: %v", c.DataSettings.LiveData.RealOrders)
log.Infof(common.Config, "Data check timer: %v", c.DataSettings.LiveData.DataCheckTimer)
log.Infof(common.Config, "New event timeout: %v", c.DataSettings.LiveData.NewEventTimeout)
for i := range c.DataSettings.LiveData.ExchangeCredentials {
log.Infof(common.Config, "%s credentials: %s", c.DataSettings.LiveData.ExchangeCredentials[i].Exchange, c.DataSettings.LiveData.ExchangeCredentials[i].Keys.String())
}
}
if c.DataSettings.APIData != nil {
log.Info(common.Config, common.CMDColours.H2+"------------------API Settings-------------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "Start date: %v", c.DataSettings.APIData.StartDate.Format(gctcommon.SimpleTimeFormat))
log.Infof(common.Config, "End date: %v", c.DataSettings.APIData.EndDate.Format(gctcommon.SimpleTimeFormat))
}
if c.DataSettings.CSVData != nil {
log.Info(common.Config, common.CMDColours.H2+"------------------CSV Settings-------------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "CSV file: %v", c.DataSettings.CSVData.FullPath)
}
if c.DataSettings.DatabaseData != nil {
log.Info(common.Config, common.CMDColours.H2+"------------------Database Settings--------------------------"+common.CMDColours.Default)
log.Infof(common.Config, "Data type: %v", c.DataSettings.DataType)
log.Infof(common.Config, "Interval: %v", c.DataSettings.Interval)
log.Infof(common.Config, "Start date: %v", c.DataSettings.DatabaseData.StartDate.Format(gctcommon.SimpleTimeFormat))
log.Infof(common.Config, "End date: %v", c.DataSettings.DatabaseData.EndDate.Format(gctcommon.SimpleTimeFormat))
}
}