Files
gocryptotrader/backtester/eventhandlers/exchange/exchange_test.go
Scott 6eaa2e4073 Backtester: USD tracking (#818)
* Initial concept for creating price tracking pairs

* Completes coverage, even with a slow test

* I dont know what point to hook this stuff up

* Bit of a broken way of handling tracking pairs

* Correctly calculates USD rates against all currencies

* Removes dependency on GCT config

* Failed currency statistics redesign

* initial Update chart to use highcharts

* Minor changes to stats

* Creats funding stats to handle the stat calculations. Needs more work

* tracks USD snapshots and BREAKS THINGS FURTHER

* Fixed!

* Adds ratio calculations and such, but its WRONG. do it at totals level dummy

* End of day basic lint

* Remaining lints

* USD totals statistics

* Minor panic fixes

* Printing of funding stats, but its bad

* Properly calculates overall benchmark, moves funding stat output

* Adds some template charge, removes duplicate fields

* New charts!

* Darkcharts. funding protection when disabled

* Now works with usd tracking/funding disabled!

* Attempting to only show working stats based on settings.

* Spruces up the goose/reporting

* Completes report HTML rendering

* lint and test fixes

* funding statistics testing

* slightly more test coverage

* Test coverage

* Initial documentation

* Fixes tests

* Database testing and rendering improvements and breakages

* report and cmd rendering, linting. fix comma output. rm gct cfg

* PR mode 🎉 Path field, config builder support,testing,linting,docs

* minor calculation improvement

* Secret lint that did not show up locally

* Disable USD tracking for example configs

* ShazNitNoScope

* Forgotten errors

* ""

* literally Logarithmically logically renders the date 👀

* Fixes typos, fixes parallel test, fixes chart gui and exporting
2021-11-08 12:10:15 +11:00

563 lines
16 KiB
Go

package exchange
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/backtester/common"
"github.com/thrasher-corp/gocryptotrader/backtester/data/kline"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/event"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/fill"
"github.com/thrasher-corp/gocryptotrader/backtester/eventtypes/order"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
exchange "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"
)
const testExchange = "binance"
type fakeFund struct{}
func (f *fakeFund) IncreaseAvailable(decimal.Decimal, gctorder.Side) {}
func (f *fakeFund) Release(decimal.Decimal, decimal.Decimal, gctorder.Side) error {
return nil
}
func TestReset(t *testing.T) {
t.Parallel()
e := Exchange{
CurrencySettings: []Settings{},
}
e.Reset()
if e.CurrencySettings != nil {
t.Error("expected nil")
}
}
func TestSetCurrency(t *testing.T) {
t.Parallel()
e := Exchange{}
e.SetExchangeAssetCurrencySettings("", "", currency.Pair{}, &Settings{})
if len(e.CurrencySettings) != 0 {
t.Error("expected 0")
}
cs := &Settings{
Exchange: testExchange,
UseRealOrders: true,
Pair: currency.NewPair(currency.BTC, currency.USDT),
Asset: asset.Spot,
ExchangeFee: decimal.Zero,
MakerFee: decimal.Zero,
TakerFee: decimal.Zero,
BuySide: MinMax{},
SellSide: MinMax{},
Leverage: Leverage{},
MinimumSlippageRate: decimal.Zero,
MaximumSlippageRate: decimal.Zero,
}
e.SetExchangeAssetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
result, err := e.GetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
}
if !result.UseRealOrders {
t.Error("expected true")
}
e.SetExchangeAssetCurrencySettings(testExchange, asset.Spot, currency.NewPair(currency.BTC, currency.USDT), cs)
if len(e.CurrencySettings) != 1 {
t.Error("expected 1")
}
}
func TestEnsureOrderFitsWithinHLV(t *testing.T) {
t.Parallel()
adjustedPrice, adjustedAmount := ensureOrderFitsWithinHLV(decimal.NewFromInt(123), decimal.NewFromInt(1), decimal.NewFromInt(100), decimal.NewFromInt(99), decimal.NewFromInt(100))
if !adjustedAmount.Equal(decimal.NewFromInt(1)) {
t.Error("expected 1")
}
if !adjustedPrice.Equal(decimal.NewFromInt(100)) {
t.Error("expected 100")
}
adjustedPrice, adjustedAmount = ensureOrderFitsWithinHLV(decimal.NewFromInt(123), decimal.NewFromInt(1), decimal.NewFromInt(100), decimal.NewFromInt(99), decimal.NewFromInt(80))
if !adjustedAmount.Equal(decimal.NewFromFloat(0.799999992)) {
t.Errorf("received: %v, expected: %v", adjustedAmount, decimal.NewFromFloat(0.799999992))
}
if !adjustedPrice.Equal(decimal.NewFromInt(100)) {
t.Error("expected 100")
}
}
func TestCalculateExchangeFee(t *testing.T) {
t.Parallel()
fee := calculateExchangeFee(decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.NewFromFloat(0.1))
if !fee.Equal(decimal.NewFromFloat(0.1)) {
t.Error("expected 0.1")
}
fee = calculateExchangeFee(decimal.NewFromInt(2), decimal.NewFromFloat(1), decimal.NewFromFloat(0.005))
if !fee.Equal(decimal.NewFromFloat(0.01)) {
t.Error("expected 0.01")
}
}
func TestSizeOrder(t *testing.T) {
t.Parallel()
e := Exchange{}
_, _, err := e.sizeOfflineOrder(decimal.Zero, decimal.Zero, decimal.Zero, nil, nil)
if !errors.Is(err, common.ErrNilArguments) {
t.Error(err)
}
cs := &Settings{}
f := &fill.Fill{
ClosePrice: decimal.NewFromInt(1337),
Amount: decimal.NewFromInt(1),
}
_, _, err = e.sizeOfflineOrder(decimal.Zero, decimal.Zero, decimal.Zero, cs, f)
if !errors.Is(err, errDataMayBeIncorrect) {
t.Errorf("received: %v, expected: %v", err, errDataMayBeIncorrect)
}
var p, a decimal.Decimal
p, a, err = e.sizeOfflineOrder(decimal.NewFromInt(10), decimal.NewFromInt(2), decimal.NewFromInt(10), cs, f)
if err != nil {
t.Error(err)
}
if !p.Equal(decimal.NewFromInt(10)) {
t.Error("expected 10")
}
if !a.Equal(decimal.NewFromInt(1)) {
t.Error("expected 1")
}
}
func TestPlaceOrder(t *testing.T) {
t.Parallel()
bot := &engine.Engine{}
var err error
em := engine.SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = exch.Setup(cfg)
if err != nil {
t.Fatal(err)
}
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false)
if err != nil {
t.Error(err)
}
err = bot.OrderManager.Start()
if err != nil {
t.Error(err)
}
e := Exchange{}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, nil, nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received: %v, expected: %v", err, common.ErrNilEvent)
}
f := &fill.Fill{}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
if err != nil && err.Error() != "order exchange name must be specified" {
t.Error(err)
}
f.Exchange = testExchange
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
if !errors.Is(err, gctorder.ErrPairIsEmpty) {
t.Errorf("received: %v, expected: %v", err, gctorder.ErrPairIsEmpty)
}
f.CurrencyPair = currency.NewPair(currency.BTC, currency.USDT)
f.AssetType = asset.Spot
f.Direction = gctorder.Buy
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), false, true, f, bot.OrderManager)
if err != nil {
t.Error(err)
}
_, err = e.placeOrder(context.Background(), decimal.NewFromInt(1), decimal.NewFromInt(1), true, true, f, bot.OrderManager)
if err != nil && !strings.Contains(err.Error(), "unset/default API keys") {
t.Error(err)
}
}
func TestExecuteOrder(t *testing.T) {
t.Parallel()
bot := &engine.Engine{}
var err error
em := engine.SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = exch.Setup(cfg)
if err != nil {
t.Fatal(err)
}
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false)
if err != nil {
t.Error(err)
}
err = bot.OrderManager.Start()
if err != nil {
t.Error(err)
}
p := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
_, err = exch.FetchOrderbook(context.Background(), p, a)
if err != nil {
t.Fatal(err)
}
cs := Settings{
Exchange: testExchange,
UseRealOrders: false,
Pair: p,
Asset: a,
ExchangeFee: decimal.NewFromFloat(0.01),
MakerFee: decimal.NewFromFloat(0.01),
TakerFee: decimal.NewFromFloat(0.01),
BuySide: MinMax{},
SellSide: MinMax{},
Leverage: Leverage{},
MinimumSlippageRate: decimal.Zero,
MaximumSlippageRate: decimal.NewFromInt(1),
}
e := Exchange{
CurrencySettings: []Settings{cs},
}
ev := event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.FifteenMin,
CurrencyPair: p,
AssetType: a,
}
o := &order.Order{
Base: ev,
Direction: gctorder.Buy,
Amount: decimal.NewFromInt(10),
AllocatedFunds: decimal.NewFromInt(1337),
}
d := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: "",
Pair: currency.Pair{},
Asset: "",
Interval: 0,
Candles: []gctkline.Candle{
{
Close: 1,
High: 1,
Low: 1,
Volume: 1,
},
},
},
}
err = d.Load()
if err != nil {
t.Error(err)
}
d.Next()
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if err != nil {
t.Error(err)
}
cs.UseRealOrders = true
cs.CanUseExchangeLimits = true
o.Direction = gctorder.Sell
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if err != nil && !strings.Contains(err.Error(), "unset/default API keys") {
t.Error(err)
}
}
func TestExecuteOrderBuySellSizeLimit(t *testing.T) {
t.Parallel()
bot := &engine.Engine{}
var err error
em := engine.SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = exch.Setup(cfg)
if err != nil {
t.Fatal(err)
}
em.Add(exch)
bot.ExchangeManager = em
bot.OrderManager, err = engine.SetupOrderManager(em, &engine.CommunicationManager{}, &bot.ServicesWG, false)
if err != nil {
t.Error(err)
}
err = bot.OrderManager.Start()
if err != nil {
t.Error(err)
}
p := currency.NewPair(currency.BTC, currency.USDT)
a := asset.Spot
_, err = exch.FetchOrderbook(context.Background(), p, a)
if err != nil {
t.Fatal(err)
}
err = exch.UpdateOrderExecutionLimits(context.Background(), asset.Spot)
if err != nil {
t.Fatal(err)
}
limits, err := exch.GetOrderExecutionLimits(a, p)
if err != nil {
t.Fatal(err)
}
cs := Settings{
Exchange: testExchange,
UseRealOrders: false,
Pair: p,
Asset: a,
ExchangeFee: decimal.NewFromFloat(0.01),
MakerFee: decimal.NewFromFloat(0.01),
TakerFee: decimal.NewFromFloat(0.01),
BuySide: MinMax{
MaximumSize: decimal.NewFromFloat(0.01),
MinimumSize: decimal.Zero,
},
SellSide: MinMax{
MaximumSize: decimal.NewFromFloat(0.1),
MinimumSize: decimal.Zero,
},
Leverage: Leverage{},
MinimumSlippageRate: decimal.Zero,
MaximumSlippageRate: decimal.NewFromInt(1),
Limits: limits,
}
e := Exchange{
CurrencySettings: []Settings{cs},
}
ev := event.Base{
Exchange: testExchange,
Time: time.Now(),
Interval: gctkline.FifteenMin,
CurrencyPair: p,
AssetType: a,
}
o := &order.Order{
Base: ev,
Direction: gctorder.Buy,
Amount: decimal.NewFromInt(10),
AllocatedFunds: decimal.NewFromInt(1337),
}
d := &kline.DataFromKline{
Item: gctkline.Item{
Exchange: "",
Pair: currency.Pair{},
Asset: "",
Interval: 0,
Candles: []gctkline.Candle{
{
Close: 1,
High: 1,
Low: 1,
Volume: 1,
},
},
},
}
err = d.Load()
if err != nil {
t.Error(err)
}
d.Next()
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
}
o = &order.Order{
Base: ev,
Direction: gctorder.Buy,
Amount: decimal.NewFromInt(10),
AllocatedFunds: decimal.NewFromInt(1337),
}
cs.BuySide.MaximumSize = decimal.Zero
cs.BuySide.MinimumSize = decimal.NewFromFloat(0.01)
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if err != nil && !strings.Contains(err.Error(), "exceed minimum size") {
t.Error(err)
}
if err != nil {
t.Error("limitReducedAmount adjusted to 0.99999999, direction BUY, should fall in buyside {MinimumSize:0.01 MaximumSize:0 MaximumTotal:0}")
}
o = &order.Order{
Base: ev,
Direction: gctorder.Sell,
Amount: decimal.NewFromInt(10),
AllocatedFunds: decimal.NewFromInt(1337),
}
cs.SellSide.MaximumSize = decimal.Zero
cs.SellSide.MinimumSize = decimal.NewFromFloat(0.01)
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if err != nil && !strings.Contains(err.Error(), "exceed minimum size") {
t.Error(err)
}
if err != nil {
t.Error("limitReducedAmount adjust to 0.99999999, should fall in sell size {MinimumSize:0.01 MaximumSize:0 MaximumTotal:0}")
}
o = &order.Order{
Base: ev,
Direction: gctorder.Sell,
Amount: decimal.NewFromFloat(0.5),
AllocatedFunds: decimal.NewFromInt(1337),
}
cs.SellSide.MaximumSize = decimal.Zero
cs.SellSide.MinimumSize = decimal.NewFromInt(1)
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
}
o = &order.Order{
Base: ev,
Direction: gctorder.Sell,
Amount: decimal.NewFromFloat(0.02),
AllocatedFunds: decimal.NewFromFloat(0.01337),
}
cs.SellSide.MaximumSize = decimal.Zero
cs.SellSide.MinimumSize = decimal.NewFromFloat(0.01)
cs.UseRealOrders = true
cs.CanUseExchangeLimits = true
o.Direction = gctorder.Sell
e.CurrencySettings = []Settings{cs}
_, err = e.ExecuteOrder(o, d, bot.OrderManager, &fakeFund{})
if !errors.Is(err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet) {
t.Errorf("received %v expected %v", err, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
}
}
func TestApplySlippageToPrice(t *testing.T) {
t.Parallel()
resp := applySlippageToPrice(gctorder.Buy, decimal.NewFromInt(1), decimal.NewFromFloat(0.9))
if !resp.Equal(decimal.NewFromFloat(1.1)) {
t.Errorf("received: %v, expected: %v", resp, decimal.NewFromFloat(1.1))
}
resp = applySlippageToPrice(gctorder.Sell, decimal.NewFromInt(1), decimal.NewFromFloat(0.9))
if !resp.Equal(decimal.NewFromFloat(0.9)) {
t.Errorf("received: %v, expected: %v", resp, decimal.NewFromFloat(0.9))
}
}
func TestReduceAmountToFitPortfolioLimit(t *testing.T) {
t.Parallel()
initialPrice := decimal.NewFromInt(100)
initialAmount := decimal.NewFromInt(10).Div(initialPrice)
portfolioAdjustedTotal := initialAmount.Mul(initialPrice)
adjustedPrice := decimal.NewFromInt(1000)
amount := decimal.NewFromInt(2)
finalAmount := reduceAmountToFitPortfolioLimit(adjustedPrice, amount, portfolioAdjustedTotal, gctorder.Buy)
if !finalAmount.Mul(adjustedPrice).Equal(portfolioAdjustedTotal) {
t.Errorf("expected value %v to match portfolio total %v", finalAmount.Mul(adjustedPrice), portfolioAdjustedTotal)
}
finalAmount = reduceAmountToFitPortfolioLimit(adjustedPrice, decimal.NewFromInt(133333333337), portfolioAdjustedTotal, gctorder.Sell)
if finalAmount != portfolioAdjustedTotal {
t.Errorf("expected value %v to match portfolio total %v", finalAmount, portfolioAdjustedTotal)
}
finalAmount = reduceAmountToFitPortfolioLimit(adjustedPrice, decimal.NewFromInt(1), portfolioAdjustedTotal, gctorder.Sell)
if !finalAmount.Equal(decimal.NewFromInt(1)) {
t.Errorf("expected value %v to match portfolio total %v", finalAmount, portfolioAdjustedTotal)
}
}
func TestVerifyOrderWithinLimits(t *testing.T) {
t.Parallel()
err := verifyOrderWithinLimits(nil, decimal.Zero, nil)
if !errors.Is(err, common.ErrNilEvent) {
t.Errorf("received %v expected %v", err, common.ErrNilEvent)
}
err = verifyOrderWithinLimits(&fill.Fill{}, decimal.Zero, nil)
if !errors.Is(err, errNilCurrencySettings) {
t.Errorf("received %v expected %v", err, errNilCurrencySettings)
}
err = verifyOrderWithinLimits(&fill.Fill{}, decimal.Zero, &Settings{})
if !errors.Is(err, errInvalidDirection) {
t.Errorf("received %v expected %v", err, errInvalidDirection)
}
f := &fill.Fill{
Direction: gctorder.Buy,
}
err = verifyOrderWithinLimits(f, decimal.Zero, &Settings{})
if !errors.Is(err, nil) {
t.Errorf("received %v expected %v", err, nil)
}
s := &Settings{
BuySide: MinMax{
MinimumSize: decimal.NewFromInt(1),
MaximumSize: decimal.NewFromInt(1),
},
}
err = verifyOrderWithinLimits(f, decimal.NewFromFloat(0.5), s)
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
}
f.Direction = gctorder.Buy
err = verifyOrderWithinLimits(f, decimal.NewFromInt(2), s)
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
}
f.Direction = gctorder.Sell
s.SellSide = MinMax{
MinimumSize: decimal.NewFromInt(1),
MaximumSize: decimal.NewFromInt(1),
}
err = verifyOrderWithinLimits(f, decimal.NewFromFloat(0.5), s)
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
}
f.Direction = gctorder.Sell
err = verifyOrderWithinLimits(f, decimal.NewFromInt(2), s)
if !errors.Is(err, errExceededPortfolioLimit) {
t.Errorf("received %v expected %v", err, errExceededPortfolioLimit)
}
}