mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
Exchanges: Add in exchange defined tolerance settings (#647)
* Exchanges: Add in exchange defined tolerance settings to conform to min max amounts/price/notional etc (Initial) * Add to tests fix linter * Binance: Implement CMF and usdtMarginFutures fetching of currency information, addr nits * binance: Add in test for tolerance set up * exchanges: add in more tolerance settings and add tests * nits: addr * fix linter issue * RPCServer: Use ordermanager instead of going direct to exchange * Nits: Addr * nits: glorious addr phase one * nits: glorious nits phase 2 * exchange: move tolerance -> limits in order package add wrapper function, split binance functions to asset files * nits: Addr thrasher + also include locking of limits struct when we update via syncer later on * nits: mdc addr * nits: glorious nits * limits: unexport mutex * limit: revert maths optim. and fix spelling * limit: Add decimal package * limit: don't check price on market order * Orders: Add order execution checks on fake orders so as to always conform to tight specifications even in simulation * binance: handle case where spot is not enabled but margin is * backtester: add in amount conforming to back tested events to simulate realistic orders * rm ln * order limit: return amount when limit is nil and conformToAmount is requested * nits: glorious nits + friends * backtester/orders: fix tests * nits: glorious nits * nits: glorious nits * RMLINE * nits: more glorious nits! * nits: pooosh * binance: fix margin logic * nits: Add warning, settings log and report item for exchange order execution limits * backtester: add specific warnings in report output * backtest: Adjust warnings
This commit is contained in:
@@ -69,9 +69,8 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
if bot == nil {
|
||||
return nil, errNilBot
|
||||
}
|
||||
bt := New()
|
||||
|
||||
var e exchange.Exchange
|
||||
bt := New()
|
||||
bt.Datas = &data.HandlerPerCurrency{}
|
||||
bt.EventQueue = &eventholder.Holder{}
|
||||
reports := &report.Data{
|
||||
@@ -86,7 +85,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e, err = bt.setupExchangeSettings(cfg)
|
||||
e, err := bt.setupExchangeSettings(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -280,6 +279,22 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
|
||||
MaximumTotal: cfg.CurrencySettings[i].SellSide.MaximumTotal,
|
||||
}
|
||||
sellRule.Validate()
|
||||
|
||||
limits, err := exch.GetOrderExecutionLimits(a, pair)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
if limits != nil {
|
||||
if !cfg.CurrencySettings[i].CanUseExchangeLimits {
|
||||
log.Warnf(log.BackTester, "exchange %s order execution limits supported but disabled for %s %s, results may not work when in production",
|
||||
cfg.CurrencySettings[i].ExchangeName,
|
||||
pair,
|
||||
a)
|
||||
cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true
|
||||
}
|
||||
}
|
||||
|
||||
resp.CurrencySettings = append(resp.CurrencySettings, exchange.Settings{
|
||||
ExchangeName: cfg.CurrencySettings[i].ExchangeName,
|
||||
InitialFunds: cfg.CurrencySettings[i].InitialFunds,
|
||||
@@ -298,6 +313,8 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange
|
||||
MaximumLeverageRate: cfg.CurrencySettings[i].Leverage.MaximumLeverageRate,
|
||||
MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].Leverage.MaximumOrdersWithLeverageRatio,
|
||||
},
|
||||
Limits: limits,
|
||||
CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ func (c *Config) PrintSetting() {
|
||||
log.Infof(log.BackTester, "Buy rules: %+v", c.CurrencySettings[i].BuySide)
|
||||
log.Infof(log.BackTester, "Sell rules: %+v", c.CurrencySettings[i].SellSide)
|
||||
log.Infof(log.BackTester, "Leverage rules: %+v", c.CurrencySettings[i].Leverage)
|
||||
log.Infof(log.BackTester, "Can use exchange defined order execution limits: %+v", c.CurrencySettings[i].CanUseExchangeLimits)
|
||||
}
|
||||
log.Info(log.BackTester, "-------------------------------------------------------------")
|
||||
log.Info(log.BackTester, "------------------Portfolio Settings-------------------------")
|
||||
|
||||
@@ -104,6 +104,9 @@ type CurrencySettings struct {
|
||||
TakerFee float64 `json:"taker-fee-override"`
|
||||
|
||||
MaximumHoldingsRatio float64 `json:"maximum-holdings-ratio"`
|
||||
|
||||
CanUseExchangeLimits bool `json:"use-exchange-order-limits"`
|
||||
ShowExchangeOrderLimitWarning bool `json:"-"`
|
||||
}
|
||||
|
||||
// APIData defines all fields to configure API based data
|
||||
|
||||
@@ -588,6 +588,11 @@ func addCurrencySetting(reader *bufio.Reader) (*config.CurrencySettings, error)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fmt.Println("Will the in-sample data amounts conform to current exchange defined order execution limits? i.e. If amount is 1337.001345 and the step size is 0.01 order amount will be re-adjusted to 1337. y/n")
|
||||
yn = quickParse(reader)
|
||||
if yn == y || yn == yes {
|
||||
setting.CanUseExchangeLimits = true
|
||||
}
|
||||
fmt.Println("Do you wish to include slippage? y/n")
|
||||
yn = quickParse(reader)
|
||||
if yn == y || yn == yes {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
},
|
||||
{
|
||||
"exchange-name": "binance",
|
||||
@@ -59,7 +60,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
},
|
||||
{
|
||||
"exchange-name": "binance",
|
||||
@@ -59,7 +60,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": true
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
},
|
||||
{
|
||||
"exchange-name": "binance",
|
||||
@@ -63,7 +64,8 @@
|
||||
"max-slippage-percent": 0,
|
||||
"maker-fee-override": 0.001,
|
||||
"taker-fee-override": 0.002,
|
||||
"maximum-holdings-ratio": 0
|
||||
"maximum-holdings-ratio": 0,
|
||||
"use-exchange-order-limits": false
|
||||
}
|
||||
],
|
||||
"data-settings": {
|
||||
|
||||
@@ -90,11 +90,30 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, bot *engine.En
|
||||
f.AppendReason(fmt.Sprintf("Order size shrunk from %v to %v to remain within portfolio limits", amount, reducedAmount))
|
||||
}
|
||||
|
||||
var orderID string
|
||||
orderID, err = e.placeOrder(adjustedPrice, reducedAmount, cs.UseRealOrders, f, bot)
|
||||
var limitReducedAmount float64
|
||||
if cs.CanUseExchangeLimits {
|
||||
// Conforms the amount to the exchange order defined step amount
|
||||
// reducing it when needed
|
||||
limitReducedAmount = cs.Limits.ConformToAmount(reducedAmount)
|
||||
if limitReducedAmount != reducedAmount {
|
||||
f.AppendReason(fmt.Sprintf("Order size shrunk from %v to %v to remain within exchange step amount limits",
|
||||
reducedAmount,
|
||||
limitReducedAmount))
|
||||
}
|
||||
} else {
|
||||
limitReducedAmount = reducedAmount
|
||||
}
|
||||
|
||||
orderID, err := e.placeOrder(adjustedPrice, limitReducedAmount, cs.UseRealOrders, cs.CanUseExchangeLimits, f, bot)
|
||||
if err != nil {
|
||||
if f.GetDirection() == gctorder.Buy {
|
||||
f.SetDirection(common.CouldNotBuy)
|
||||
} else if f.GetDirection() == gctorder.Sell {
|
||||
f.SetDirection(common.CouldNotSell)
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
ords, _ := bot.OrderManager.GetOrdersSnapshot("")
|
||||
for i := range ords {
|
||||
if ords[i].ID != orderID {
|
||||
@@ -105,7 +124,7 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, bot *engine.En
|
||||
ords[i].CloseTime = o.GetTime()
|
||||
f.Order = &ords[i]
|
||||
f.PurchasePrice = ords[i].Price
|
||||
f.Total = (f.PurchasePrice * reducedAmount) + f.ExchangeFee
|
||||
f.Total = (f.PurchasePrice * limitReducedAmount) + f.ExchangeFee
|
||||
}
|
||||
|
||||
if f.Order == nil {
|
||||
@@ -124,7 +143,7 @@ func reduceAmountToFitPortfolioLimit(adjustedPrice, amount, sizedPortfolioTotal
|
||||
return amount
|
||||
}
|
||||
|
||||
func (e *Exchange) placeOrder(price, amount float64, useRealOrders bool, f *fill.Fill, bot *engine.Engine) (string, error) {
|
||||
func (e *Exchange) placeOrder(price, amount float64, useRealOrders, useExchangeLimits bool, f *fill.Fill, bot *engine.Engine) (string, error) {
|
||||
if f == nil {
|
||||
return "", common.ErrNilEvent
|
||||
}
|
||||
@@ -164,7 +183,7 @@ func (e *Exchange) placeOrder(price, amount float64, useRealOrders bool, f *fill
|
||||
Cost: price,
|
||||
FullyMatched: true,
|
||||
}
|
||||
resp, err := bot.OrderManager.SubmitFakeOrder(o, submitResponse)
|
||||
resp, err := bot.OrderManager.SubmitFakeOrder(o, submitResponse, useExchangeLimits)
|
||||
if resp != nil {
|
||||
orderID = resp.OrderID
|
||||
}
|
||||
|
||||
@@ -148,30 +148,30 @@ func TestPlaceOrder(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
e := Exchange{}
|
||||
_, err = e.placeOrder(1, 1, false, nil, nil)
|
||||
_, err = e.placeOrder(1, 1, false, true, nil, nil)
|
||||
if !errors.Is(err, common.ErrNilEvent) {
|
||||
t.Errorf("expected: %v, received %v", common.ErrNilEvent, err)
|
||||
}
|
||||
f := &fill.Fill{}
|
||||
_, err = e.placeOrder(1, 1, false, f, bot)
|
||||
_, err = e.placeOrder(1, 1, false, true, f, bot)
|
||||
if err != nil && err.Error() != "order exchange name must be specified" {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
f.Exchange = testExchange
|
||||
_, err = e.placeOrder(1, 1, false, f, bot)
|
||||
if err != nil && err.Error() != "order pair is empty" {
|
||||
t.Error(err)
|
||||
_, err = e.placeOrder(1, 1, false, true, f, bot)
|
||||
if !errors.Is(err, gctorder.ErrPairIsEmpty) {
|
||||
t.Errorf("expected: %v, received %v", gctorder.ErrPairIsEmpty, err)
|
||||
}
|
||||
f.CurrencyPair = currency.NewPair(currency.BTC, currency.USDT)
|
||||
f.AssetType = asset.Spot
|
||||
f.Direction = gctorder.Buy
|
||||
_, err = e.placeOrder(1, 1, false, f, bot)
|
||||
_, err = e.placeOrder(1, 1, false, true, f, bot)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = e.placeOrder(1, 1, true, f, bot)
|
||||
_, err = e.placeOrder(1, 1, true, true, f, bot)
|
||||
if err != nil && !strings.Contains(err.Error(), "unset/default API keys") {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -179,8 +179,36 @@ func TestPlaceOrder(t *testing.T) {
|
||||
|
||||
func TestExecuteOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
bot, err := engine.NewFromSettings(&engine.Settings{
|
||||
ConfigFile: filepath.Join("..", "..", "..", "testdata", "configtest.json"),
|
||||
EnableDryRun: true,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = bot.OrderManager.Start(bot)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = bot.LoadExchange(testExchange, false, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
b := bot.GetExchangeByName(testExchange)
|
||||
|
||||
p := currency.NewPair(currency.BTC, currency.USDT)
|
||||
a := asset.Spot
|
||||
_, err = b.FetchOrderbook(p, a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
limits, err := b.GetOrderExecutionLimits(a, p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cs := Settings{
|
||||
ExchangeName: testExchange,
|
||||
UseRealOrders: false,
|
||||
@@ -195,6 +223,7 @@ func TestExecuteOrder(t *testing.T) {
|
||||
Leverage: config.Leverage{},
|
||||
MinimumSlippageRate: 0,
|
||||
MaximumSlippageRate: 1,
|
||||
Limits: limits,
|
||||
}
|
||||
e := Exchange{
|
||||
CurrencySettings: []Settings{cs},
|
||||
@@ -209,30 +238,10 @@ func TestExecuteOrder(t *testing.T) {
|
||||
o := &order.Order{
|
||||
Base: ev,
|
||||
Direction: gctorder.Buy,
|
||||
Amount: 1,
|
||||
Amount: 10,
|
||||
Funds: 1337,
|
||||
}
|
||||
|
||||
bot, err := engine.NewFromSettings(&engine.Settings{
|
||||
ConfigFile: filepath.Join("..", "..", "..", "testdata", "configtest.json"),
|
||||
EnableDryRun: true,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = bot.OrderManager.Start(bot)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = bot.LoadExchange(testExchange, false, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
b := bot.GetExchangeByName(testExchange)
|
||||
_, err = b.FetchOrderbook(p, a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := &kline.DataFromKline{
|
||||
Item: gctkline.Item{
|
||||
Exchange: "",
|
||||
@@ -260,6 +269,7 @@ func TestExecuteOrder(t *testing.T) {
|
||||
}
|
||||
|
||||
cs.UseRealOrders = true
|
||||
cs.CanUseExchangeLimits = true
|
||||
o.Direction = gctorder.Sell
|
||||
e.CurrencySettings = []Settings{cs}
|
||||
_, err = e.ExecuteOrder(o, d, bot)
|
||||
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/engine"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
var (
|
||||
errDataMayBeIncorrect = errors.New("data may be incorrect")
|
||||
errExchangeUnset = errors.New("exchange unset")
|
||||
)
|
||||
|
||||
// ExecutionHandler interface dictates what functions are required to submit an order
|
||||
@@ -51,4 +51,7 @@ type Settings struct {
|
||||
|
||||
MinimumSlippageRate float64
|
||||
MaximumSlippageRate float64
|
||||
|
||||
Limits *gctorder.Limits
|
||||
CanUseExchangeLimits bool
|
||||
}
|
||||
|
||||
@@ -254,6 +254,33 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="view view-cascade bg-warning">
|
||||
<h2 class="px-4 card-header-title white-text">Warnings</h2>
|
||||
</div>
|
||||
<div class="card-body card-body-cascade ">
|
||||
<table class="table table-hover table-bordered table-striped">
|
||||
<thead>
|
||||
<th>Exchange Name</th>
|
||||
<th>Asset</th>
|
||||
<th>Currency Base</th>
|
||||
<th>Currency Quote</th>
|
||||
<th>Warning</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Config.CurrencySettings}}
|
||||
{{if .ShowExchangeOrderLimitWarning}}
|
||||
<tr>
|
||||
<td>{{.ExchangeName}}</td>
|
||||
<td>{{.Asset}}</td>
|
||||
<td>{{.Base}}</td>
|
||||
<td>{{.Quote}}</td>
|
||||
<td>order execution limits supported but disabled, results may not work when in production</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div >
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
@@ -83,133 +84,137 @@ func testWrappers(e exchange.IBotExchange) []string {
|
||||
var funcs []string
|
||||
|
||||
_, err := e.FetchTicker(p, assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "FetchTicker")
|
||||
}
|
||||
|
||||
_, err = e.UpdateTicker(p, assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "UpdateTicker")
|
||||
}
|
||||
|
||||
_, err = e.FetchOrderbook(p, assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "FetchOrderbook")
|
||||
}
|
||||
|
||||
_, err = e.UpdateOrderbook(p, assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "UpdateOrderbook")
|
||||
}
|
||||
|
||||
_, err = e.FetchTradablePairs(asset.Spot)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "FetchTradablePairs")
|
||||
}
|
||||
|
||||
err = e.UpdateTradablePairs(false)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "UpdateTradablePairs")
|
||||
}
|
||||
|
||||
_, err = e.FetchAccountInfo(assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetAccountInfo")
|
||||
}
|
||||
|
||||
_, err = e.GetRecentTrades(p, assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetRecentTrades")
|
||||
}
|
||||
|
||||
_, err = e.GetHistoricTrades(p, assetType, time.Time{}, time.Time{})
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetHistoricTrades")
|
||||
}
|
||||
|
||||
_, err = e.GetFundingHistory()
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetFundingHistory")
|
||||
}
|
||||
|
||||
_, err = e.SubmitOrder(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "SubmitOrder")
|
||||
}
|
||||
|
||||
_, err = e.ModifyOrder(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "ModifyOrder")
|
||||
}
|
||||
|
||||
err = e.CancelOrder(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "CancelOrder")
|
||||
}
|
||||
|
||||
_, err = e.CancelBatchOrders(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "CancelBatchOrders")
|
||||
}
|
||||
|
||||
_, err = e.CancelAllOrders(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "CancelAllOrders")
|
||||
}
|
||||
|
||||
_, err = e.GetOrderInfo("1", p, assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetOrderInfo")
|
||||
}
|
||||
|
||||
_, err = e.GetOrderHistory(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetOrderHistory")
|
||||
}
|
||||
|
||||
_, err = e.GetActiveOrders(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetActiveOrders")
|
||||
}
|
||||
|
||||
_, err = e.GetDepositAddress(currency.BTC, "")
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetDepositAddress")
|
||||
}
|
||||
|
||||
_, err = e.WithdrawCryptocurrencyFunds(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "WithdrawCryptocurrencyFunds")
|
||||
}
|
||||
|
||||
_, err = e.WithdrawFiatFunds(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "WithdrawFiatFunds")
|
||||
}
|
||||
_, err = e.WithdrawFiatFundsToInternationalBank(nil)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "WithdrawFiatFundsToInternationalBank")
|
||||
}
|
||||
|
||||
_, err = e.GetHistoricCandles(currency.Pair{}, asset.Spot, time.Unix(0, 0), time.Unix(0, 0), kline.OneDay)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetHistoricCandles")
|
||||
}
|
||||
|
||||
_, err = e.GetHistoricCandlesExtended(currency.Pair{}, asset.Spot, time.Unix(0, 0), time.Unix(0, 0), kline.OneDay)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetHistoricCandlesExtended")
|
||||
}
|
||||
|
||||
_, err = e.UpdateAccountInfo(assetType)
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "UpdateAccountInfo")
|
||||
}
|
||||
|
||||
_, err = e.GetFeeByType(&exchange.FeeBuilder{})
|
||||
if err == common.ErrNotYetImplemented {
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "GetFeeByType")
|
||||
}
|
||||
|
||||
err = e.UpdateOrderExecutionLimits(asset.DownsideProfitContract)
|
||||
if errors.Is(err, common.ErrNotYetImplemented) {
|
||||
funcs = append(funcs, "UpdateOrderExecutionLimits")
|
||||
}
|
||||
return funcs
|
||||
}
|
||||
|
||||
@@ -420,14 +420,14 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config)
|
||||
})
|
||||
|
||||
var getHistoricTradesResponse []trade.Data
|
||||
getHistoricTradesResponse, err = e.GetHistoricTrades(p, assetTypes[i], time.Now().Add(-time.Hour*24), time.Now())
|
||||
getHistoricTradesResponse, err = e.GetHistoricTrades(p, assetTypes[i], time.Now().Add(-time.Hour), time.Now())
|
||||
msg = ""
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
responseContainer.ErrorCount++
|
||||
}
|
||||
responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{
|
||||
SentParams: jsonifyInterface([]interface{}{p, assetTypes[i], time.Now().Add(-time.Hour * 24), time.Now()}),
|
||||
SentParams: jsonifyInterface([]interface{}{p, assetTypes[i], time.Now().Add(-time.Hour), time.Now()}),
|
||||
Function: "GetHistoricTrades",
|
||||
Error: msg,
|
||||
Response: jsonifyInterface([]interface{}{getHistoricTradesResponse}),
|
||||
@@ -448,7 +448,7 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config)
|
||||
})
|
||||
|
||||
var getHistoricCandlesResponse kline.Item
|
||||
startTime, endTime := time.Now().AddDate(0, -1, 0), time.Now()
|
||||
startTime, endTime := time.Now().AddDate(0, 0, -1), time.Now()
|
||||
getHistoricCandlesResponse, err = e.GetHistoricCandles(p, assetTypes[i], startTime, endTime, kline.OneDay)
|
||||
msg = ""
|
||||
if err != nil {
|
||||
@@ -475,6 +475,20 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config)
|
||||
Response: getHisotirCandlesExtendedResponse,
|
||||
SentParams: jsonifyInterface([]interface{}{p, assetTypes[i], startTime, endTime, kline.OneDay}),
|
||||
})
|
||||
|
||||
err = e.UpdateOrderExecutionLimits(assetTypes[i])
|
||||
msg = ""
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
responseContainer.ErrorCount++
|
||||
}
|
||||
|
||||
responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{
|
||||
SentParams: jsonifyInterface([]interface{}{assetTypes[i]}),
|
||||
Function: "UpdateOrderExecutionLimits",
|
||||
Error: msg,
|
||||
Response: jsonifyInterface([]interface{}{""}),
|
||||
})
|
||||
}
|
||||
|
||||
var fetchAccountInfoResponse account.Holdings
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"orderType": "LIMIT",
|
||||
"amount": 1333333337,
|
||||
"price": 1333333337,
|
||||
"orderID": ""
|
||||
"orderID": "",
|
||||
"assetType": ""
|
||||
},
|
||||
"withdrawWalletAddress": "",
|
||||
"bankAccount": {
|
||||
|
||||
@@ -324,7 +324,7 @@ func (o *orderManager) validate(newOrder *order.Submit) error {
|
||||
}
|
||||
|
||||
if err := newOrder.Validate(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("order manager: %w", err)
|
||||
}
|
||||
|
||||
if o.cfg.EnforceLimitConfig {
|
||||
@@ -359,8 +359,21 @@ func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, err
|
||||
if exch == nil {
|
||||
return nil, ErrExchangeNotFound
|
||||
}
|
||||
var result order.SubmitResponse
|
||||
result, err = exch.SubmitOrder(newOrder)
|
||||
|
||||
// Checks for exchange min max limits for order amounts before order
|
||||
// execution can occur
|
||||
err = exch.CheckOrderExecutionLimits(newOrder.AssetType,
|
||||
newOrder.Pair,
|
||||
newOrder.Price,
|
||||
newOrder.Amount,
|
||||
newOrder.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w",
|
||||
newOrder.Exchange,
|
||||
err)
|
||||
}
|
||||
|
||||
result, err := exch.SubmitOrder(newOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -370,7 +383,7 @@ func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, err
|
||||
|
||||
// SubmitFakeOrder runs through the same process as order submission
|
||||
// but does not touch live endpoints
|
||||
func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse) (*orderSubmitResponse, error) {
|
||||
func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse, checkExchangeLimits bool) (*orderSubmitResponse, error) {
|
||||
err := o.validate(newOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -380,6 +393,20 @@ func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder or
|
||||
return nil, ErrExchangeNotFound
|
||||
}
|
||||
|
||||
if checkExchangeLimits {
|
||||
// Checks for exchange min max limits for order amounts before order
|
||||
// execution can occur
|
||||
err = exch.CheckOrderExecutionLimits(newOrder.AssetType,
|
||||
newOrder.Pair,
|
||||
newOrder.Price,
|
||||
newOrder.Amount,
|
||||
newOrder.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w",
|
||||
newOrder.Exchange,
|
||||
err)
|
||||
}
|
||||
}
|
||||
return o.processSubmittedOrder(newOrder, resultingOrder)
|
||||
}
|
||||
|
||||
|
||||
@@ -986,11 +986,6 @@ func (s *RPCServer) GetOrder(_ context.Context, r *gctrpc.GetOrderRequest) (*gct
|
||||
// SubmitOrder submits an order specified by exchange, currency pair and asset
|
||||
// type
|
||||
func (s *RPCServer) SubmitOrder(_ context.Context, r *gctrpc.SubmitOrderRequest) (*gctrpc.SubmitOrderResponse, error) {
|
||||
exch := s.GetExchangeByName(r.Exchange)
|
||||
if exch == nil {
|
||||
return nil, errExchangeNotLoaded
|
||||
}
|
||||
|
||||
p, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1012,7 +1007,7 @@ func (s *RPCServer) SubmitOrder(_ context.Context, r *gctrpc.SubmitOrderRequest)
|
||||
AssetType: a,
|
||||
}
|
||||
|
||||
resp, err := exch.SubmitOrder(submission)
|
||||
resp, err := s.OrderManager.Submit(submission)
|
||||
if err != nil {
|
||||
return &gctrpc.SubmitOrderResponse{}, err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
@@ -940,3 +941,62 @@ func (b *Binance) MaintainWsAuthStreamKey() error {
|
||||
HTTPRecording: b.HTTPRecording,
|
||||
})
|
||||
}
|
||||
|
||||
// FetchSpotExchangeLimits fetches spot order execution limits
|
||||
func (b *Binance) FetchSpotExchangeLimits() ([]order.MinMaxLevel, error) {
|
||||
var limits []order.MinMaxLevel
|
||||
spot, err := b.GetExchangeInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for x := range spot.Symbols {
|
||||
var cp currency.Pair
|
||||
cp, err = currency.NewPairFromStrings(spot.Symbols[x].BaseAsset,
|
||||
spot.Symbols[x].QuoteAsset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var assets []asset.Item
|
||||
for y := range spot.Symbols[x].Permissions {
|
||||
switch spot.Symbols[x].Permissions[y] {
|
||||
case "SPOT":
|
||||
assets = append(assets, asset.Spot)
|
||||
case "MARGIN":
|
||||
assets = append(assets, asset.Margin)
|
||||
case "LEVERAGED": // leveraged tokens not available for spot trading
|
||||
default:
|
||||
return nil, fmt.Errorf("unhandled asset type for exchange limits loading %s",
|
||||
spot.Symbols[x].Permissions[y])
|
||||
}
|
||||
}
|
||||
|
||||
for z := range assets {
|
||||
if len(spot.Symbols[x].Filters) < 8 {
|
||||
continue
|
||||
}
|
||||
|
||||
limits = append(limits, order.MinMaxLevel{
|
||||
Pair: cp,
|
||||
Asset: assets[z],
|
||||
MinPrice: spot.Symbols[x].Filters[0].MinPrice,
|
||||
MaxPrice: spot.Symbols[x].Filters[0].MaxPrice,
|
||||
StepPrice: spot.Symbols[x].Filters[0].TickSize,
|
||||
MultiplierUp: spot.Symbols[x].Filters[1].MultiplierUp,
|
||||
MultiplierDown: spot.Symbols[x].Filters[1].MultiplierDown,
|
||||
AveragePriceMinutes: spot.Symbols[x].Filters[1].AvgPriceMinutes,
|
||||
MaxAmount: spot.Symbols[x].Filters[2].MaxQty,
|
||||
MinAmount: spot.Symbols[x].Filters[2].MinQty,
|
||||
StepAmount: spot.Symbols[x].Filters[2].StepSize,
|
||||
MinNotional: spot.Symbols[x].Filters[3].MinNotional,
|
||||
MaxIcebergParts: spot.Symbols[x].Filters[4].Limit,
|
||||
MarketMinQty: spot.Symbols[x].Filters[5].MinQty,
|
||||
MarketMaxQty: spot.Symbols[x].Filters[5].MaxQty,
|
||||
MarketStepSize: spot.Symbols[x].Filters[5].StepSize,
|
||||
MaxTotalOrders: spot.Symbols[x].Filters[6].MaxNumOrders,
|
||||
MaxAlgoOrders: spot.Symbols[x].Filters[7].MaxNumAlgoOrders,
|
||||
})
|
||||
}
|
||||
}
|
||||
return limits, nil
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
)
|
||||
|
||||
@@ -1446,3 +1448,45 @@ func (b *Binance) FuturesPositionsADLEstimate(symbol currency.Pair) ([]ADLEstima
|
||||
}
|
||||
return resp, b.SendAuthHTTPRequest(exchange.RestCoinMargined, http.MethodGet, cfuturesADLQuantile, params, cFuturesAccountInformationRate, &resp)
|
||||
}
|
||||
|
||||
// FetchCoinMarginExchangeLimits fetches coin margined order execution limits
|
||||
func (b *Binance) FetchCoinMarginExchangeLimits() ([]order.MinMaxLevel, error) {
|
||||
var limits []order.MinMaxLevel
|
||||
coinFutures, err := b.FuturesExchangeInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for x := range coinFutures.Symbols {
|
||||
symbol := strings.Split(coinFutures.Symbols[x].Symbol, currency.UnderscoreDelimiter)
|
||||
var cp currency.Pair
|
||||
cp, err = currency.NewPairFromStrings(symbol[0], symbol[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(coinFutures.Symbols[x].Filters) < 6 {
|
||||
continue
|
||||
}
|
||||
|
||||
limits = append(limits, order.MinMaxLevel{
|
||||
Pair: cp,
|
||||
Asset: asset.CoinMarginedFutures,
|
||||
MinPrice: coinFutures.Symbols[x].Filters[0].MinPrice,
|
||||
MaxPrice: coinFutures.Symbols[x].Filters[0].MaxPrice,
|
||||
StepPrice: coinFutures.Symbols[x].Filters[0].TickSize,
|
||||
MaxAmount: coinFutures.Symbols[x].Filters[1].MaxQty,
|
||||
MinAmount: coinFutures.Symbols[x].Filters[1].MinQty,
|
||||
StepAmount: coinFutures.Symbols[x].Filters[1].StepSize,
|
||||
MarketMinQty: coinFutures.Symbols[x].Filters[2].MinQty,
|
||||
MarketMaxQty: coinFutures.Symbols[x].Filters[2].MaxQty,
|
||||
MarketStepSize: coinFutures.Symbols[x].Filters[2].StepSize,
|
||||
MaxTotalOrders: coinFutures.Symbols[x].Filters[3].Limit,
|
||||
MaxAlgoOrders: coinFutures.Symbols[x].Filters[4].Limit,
|
||||
MultiplierUp: coinFutures.Symbols[x].Filters[5].MultiplierUp,
|
||||
MultiplierDown: coinFutures.Symbols[x].Filters[5].MultiplierDown,
|
||||
MultiplierDecimal: coinFutures.Symbols[x].Filters[5].MultiplierDecimal,
|
||||
})
|
||||
}
|
||||
return limits, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package binance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -2482,3 +2483,50 @@ func TestUFuturesHistoricalTrades(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetExchangeOrderExecutionLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := b.UpdateOrderExecutionLimits(asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = b.UpdateOrderExecutionLimits(asset.CoinMarginedFutures)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = b.UpdateOrderExecutionLimits(asset.USDTMarginedFutures)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = b.UpdateOrderExecutionLimits(asset.Binary)
|
||||
if err == nil {
|
||||
t.Fatal("expected unhandled case")
|
||||
}
|
||||
|
||||
cmfCP, err := currency.NewPairFromStrings("BTCUSD", "PERP")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
limit, err := b.GetOrderExecutionLimits(asset.CoinMarginedFutures, cmfCP)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if limit == nil {
|
||||
t.Fatal("exchange limit should be loaded")
|
||||
}
|
||||
|
||||
err = limit.Conforms(0.000001, 0.1, order.Limit)
|
||||
if !errors.Is(err, order.ErrAmountBelowMin) {
|
||||
t.Fatalf("expected %v, but receieved %v", order.ErrAmountBelowMin, err)
|
||||
}
|
||||
|
||||
err = limit.Conforms(0.01, 1, order.Limit)
|
||||
if !errors.Is(err, order.ErrPriceBelowMin) {
|
||||
t.Fatalf("expected %v, but receieved %v", order.ErrPriceBelowMin, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ type ExchangeInfo struct {
|
||||
TickSize float64 `json:"tickSize,string"`
|
||||
MultiplierUp float64 `json:"multiplierUp,string"`
|
||||
MultiplierDown float64 `json:"multiplierDown,string"`
|
||||
AvgPriceMins int64 `json:"avgPriceMins"`
|
||||
AvgPriceMinutes int64 `json:"avgPriceMins"`
|
||||
MinQty float64 `json:"minQty,string"`
|
||||
MaxQty float64 `json:"maxQty,string"`
|
||||
StepSize float64 `json:"stepSize,string"`
|
||||
@@ -60,7 +60,9 @@ type ExchangeInfo struct {
|
||||
Limit int64 `json:"limit"`
|
||||
MaxNumAlgoOrders int64 `json:"maxNumAlgoOrders"`
|
||||
MaxNumIcebergOrders int64 `json:"maxNumIcebergOrders"`
|
||||
MaxNumOrders int64 `json:"maxNumOrders"`
|
||||
} `json:"filters"`
|
||||
Permissions []string `json:"permissions"`
|
||||
} `json:"symbols"`
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1114,3 +1115,46 @@ func (b *Binance) GetFundingRates(symbol currency.Pair, limit string, startTime,
|
||||
}
|
||||
return resp, b.SendHTTPRequest(exchange.RestUSDTMargined, fundingRate+params.Encode(), uFuturesDefaultRate, &resp)
|
||||
}
|
||||
|
||||
// FetchUSDTMarginExchangeLimits fetches USDT margined order execution limits
|
||||
func (b *Binance) FetchUSDTMarginExchangeLimits() ([]order.MinMaxLevel, error) {
|
||||
var limits []order.MinMaxLevel
|
||||
usdtFutures, err := b.UExchangeInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for x := range usdtFutures.Symbols {
|
||||
var cp currency.Pair
|
||||
cp, err = currency.NewPairFromStrings(usdtFutures.Symbols[x].BaseAsset,
|
||||
usdtFutures.Symbols[x].QuoteAsset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(usdtFutures.Symbols[x].Filters) < 7 {
|
||||
continue
|
||||
}
|
||||
|
||||
limits = append(limits, order.MinMaxLevel{
|
||||
Pair: cp,
|
||||
Asset: asset.USDTMarginedFutures,
|
||||
MinPrice: usdtFutures.Symbols[x].Filters[0].MinPrice,
|
||||
MaxPrice: usdtFutures.Symbols[x].Filters[0].MaxPrice,
|
||||
StepPrice: usdtFutures.Symbols[x].Filters[0].TickSize,
|
||||
MaxAmount: usdtFutures.Symbols[x].Filters[1].MaxQty,
|
||||
MinAmount: usdtFutures.Symbols[x].Filters[1].MinQty,
|
||||
StepAmount: usdtFutures.Symbols[x].Filters[1].StepSize,
|
||||
MarketMinQty: usdtFutures.Symbols[x].Filters[2].MinQty,
|
||||
MarketMaxQty: usdtFutures.Symbols[x].Filters[2].MaxQty,
|
||||
MarketStepSize: usdtFutures.Symbols[x].Filters[2].StepSize,
|
||||
MaxTotalOrders: usdtFutures.Symbols[x].Filters[3].Limit,
|
||||
MaxAlgoOrders: usdtFutures.Symbols[x].Filters[4].Limit,
|
||||
MinNotional: usdtFutures.Symbols[x].Filters[5].Notional,
|
||||
MultiplierUp: usdtFutures.Symbols[x].Filters[6].MultiplierUp,
|
||||
MultiplierDown: usdtFutures.Symbols[x].Filters[6].MultiplierDown,
|
||||
MultiplierDecimal: usdtFutures.Symbols[x].Filters[6].MultiplierDecimal,
|
||||
})
|
||||
}
|
||||
return limits, nil
|
||||
}
|
||||
|
||||
@@ -302,6 +302,19 @@ func (b *Binance) Run() {
|
||||
}
|
||||
}
|
||||
|
||||
a := b.GetAssetTypes()
|
||||
for x := range a {
|
||||
if err = b.CurrencyPairs.IsAssetEnabled(a[x]); err == nil {
|
||||
err = b.UpdateOrderExecutionLimits(a[x])
|
||||
if err != nil {
|
||||
log.Errorf(log.ExchangeSys,
|
||||
"Could not set %s exchange exchange limits: %v",
|
||||
b.Name,
|
||||
err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !b.GetEnabledFeatures().AutoPairUpdates && !forceUpdate {
|
||||
return
|
||||
}
|
||||
@@ -913,6 +926,9 @@ func (b *Binance) CancelBatchOrders(o []order.Cancel) (order.CancelBatchResponse
|
||||
|
||||
// CancelAllOrders cancels all orders associated with a currency pair
|
||||
func (b *Binance) CancelAllOrders(req *order.Cancel) (order.CancelAllResponse, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return order.CancelAllResponse{}, err
|
||||
}
|
||||
var cancelAllOrdersResponse order.CancelAllResponse
|
||||
cancelAllOrdersResponse.Status = make(map[string]string)
|
||||
switch req.AssetType {
|
||||
@@ -1556,3 +1572,29 @@ func compatibleOrderVars(side, status, orderType string) OrderVars {
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// UpdateOrderExecutionLimits sets exchange executions for a required asset type
|
||||
func (b *Binance) UpdateOrderExecutionLimits(a asset.Item) error {
|
||||
var limits []order.MinMaxLevel
|
||||
var err error
|
||||
switch a {
|
||||
case asset.Spot:
|
||||
limits, err = b.FetchSpotExchangeLimits()
|
||||
case asset.USDTMarginedFutures:
|
||||
limits, err = b.FetchUSDTMarginExchangeLimits()
|
||||
case asset.CoinMarginedFutures:
|
||||
limits, err = b.FetchCoinMarginExchangeLimits()
|
||||
case asset.Margin:
|
||||
if err = b.CurrencyPairs.IsAssetEnabled(asset.Spot); err != nil {
|
||||
limits, err = b.FetchSpotExchangeLimits()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unhandled asset type %s", a)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update exchange execution limits: %v", err)
|
||||
}
|
||||
return b.LoadLimits(limits)
|
||||
}
|
||||
|
||||
@@ -556,6 +556,7 @@ type UFuturesExchangeInfo struct {
|
||||
MultiplierDown float64 `json:"multiplierDown,string"`
|
||||
MultiplierUp float64 `json:"multiplierUp,string"`
|
||||
MultiplierDecimal float64 `json:"multiplierDecimal,string"`
|
||||
Notional float64 `json:"notional,string"`
|
||||
} `json:"filters"`
|
||||
OrderTypes []string `json:"orderTypes"`
|
||||
TimeInForce []string `json:"timeInForce"`
|
||||
@@ -579,6 +580,7 @@ type CExchangeInfo struct {
|
||||
MinPrice float64 `json:"minPrice,string"`
|
||||
MaxPrice float64 `json:"maxPrice,string"`
|
||||
StepSize float64 `json:"stepSize,string"`
|
||||
TickSize float64 `json:"tickSize,string"`
|
||||
MaxQty float64 `json:"maxQty,string"`
|
||||
MinQty float64 `json:"minQty,string"`
|
||||
Limit int64 `json:"limit"`
|
||||
|
||||
@@ -1261,3 +1261,8 @@ func (u URL) String() string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateOrderExecutionLimits updates order execution limits this is overridable
|
||||
func (e *Base) UpdateOrderExecutionLimits(a asset.Item) error {
|
||||
return common.ErrNotYetImplemented
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
@@ -222,6 +223,7 @@ type Base struct {
|
||||
Config *config.ExchangeConfig
|
||||
settingsMutex sync.RWMutex
|
||||
OrderbookVerificationBypass bool
|
||||
order.ExecutionLimits
|
||||
}
|
||||
|
||||
// url lookup consts
|
||||
|
||||
@@ -75,7 +75,6 @@ type IBotExchange interface {
|
||||
GetHistoricCandlesExtended(p currency.Pair, a asset.Item, timeStart, timeEnd time.Time, interval kline.Interval) (kline.Item, error)
|
||||
DisableRateLimiter() error
|
||||
EnableRateLimiter() error
|
||||
|
||||
// Websocket specific wrapper functionality
|
||||
// GetWebsocket returns a pointer to the websocket
|
||||
GetWebsocket() (*stream.Websocket, error)
|
||||
@@ -87,4 +86,8 @@ type IBotExchange interface {
|
||||
// pair,asset, url/proxy or subscription change
|
||||
FlushWebsocketChannels() error
|
||||
AuthenticateWebsocket() error
|
||||
// Exchange order related execution limits
|
||||
GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*order.Limits, error)
|
||||
CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error
|
||||
UpdateOrderExecutionLimits(a asset.Item) error
|
||||
}
|
||||
|
||||
@@ -792,6 +792,9 @@ func (k *Kraken) CancelBatchOrders(orders []order.Cancel) (order.CancelBatchResp
|
||||
|
||||
// CancelAllOrders cancels all orders associated with a currency pair
|
||||
func (k *Kraken) CancelAllOrders(req *order.Cancel) (order.CancelAllResponse, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
return order.CancelAllResponse{}, err
|
||||
}
|
||||
cancelAllOrdersResponse := order.CancelAllResponse{
|
||||
Status: make(map[string]string),
|
||||
}
|
||||
|
||||
381
exchanges/order/limits.go
Normal file
381
exchanges/order/limits.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrExchangeLimitNotLoaded defines if an exchange does not have minmax
|
||||
// values
|
||||
ErrExchangeLimitNotLoaded = errors.New("exchange limits not loaded")
|
||||
// ErrPriceBelowMin is when the price is lower than the minimum price
|
||||
// limit accepted by the exchange
|
||||
ErrPriceBelowMin = errors.New("price below minimum limit")
|
||||
// ErrPriceExceedsMax is when the price is higher than the maximum price
|
||||
// limit accepted by the exchange
|
||||
ErrPriceExceedsMax = errors.New("price exceeds maximum limit")
|
||||
// ErrPriceExceedsStep is when the price is not divisible by its step
|
||||
ErrPriceExceedsStep = errors.New("price exceeds step limit")
|
||||
// ErrAmountBelowMin is when the amount is lower than the minimum amount
|
||||
// limit accepted by the exchange
|
||||
ErrAmountBelowMin = errors.New("amount below minimum limit")
|
||||
// ErrAmountExceedsMax is when the amount is higher than the maximum amount
|
||||
// limit accepted by the exchange
|
||||
ErrAmountExceedsMax = errors.New("amount exceeds maximum limit")
|
||||
// ErrAmountExceedsStep is when the amount is not divisible by its step
|
||||
ErrAmountExceedsStep = errors.New("amount exceeds step limit")
|
||||
// ErrNotionalValue is when the notional value does not exceed currency pair
|
||||
// requirements
|
||||
ErrNotionalValue = errors.New("total notional value is under minimum limit")
|
||||
// ErrMarketAmountBelowMin is when the amount is lower than the minimum
|
||||
// amount limit accepted by the exchange for a market order
|
||||
ErrMarketAmountBelowMin = errors.New("market order amount below minimum limit")
|
||||
// ErrMarketAmountExceedsMax is when the amount is higher than the maximum
|
||||
// amount limit accepted by the exchange for a market order
|
||||
ErrMarketAmountExceedsMax = errors.New("market order amount exceeds maximum limit")
|
||||
// ErrMarketAmountExceedsStep is when the amount is not divisible by its
|
||||
// step for a market order
|
||||
ErrMarketAmountExceedsStep = errors.New("market order amount exceeds step limit")
|
||||
|
||||
errCannotValidateAsset = errors.New("cannot check limit, asset not loaded")
|
||||
errCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded")
|
||||
errCannotValidateQuoteCurrency = errors.New("cannot check limit, quote currency not loaded")
|
||||
errExchangeLimitAsset = errors.New("exchange limits not found for asset")
|
||||
errExchangeLimitBase = errors.New("exchange limits not found for base currency")
|
||||
errExchangeLimitQuote = errors.New("exchange limits not found for quote currency")
|
||||
errCannotLoadLimit = errors.New("cannot load limit, levels not supplied")
|
||||
errInvalidPriceLevels = errors.New("invalid price levels, cannot load limits")
|
||||
errInvalidAmountLevels = errors.New("invalid amount levels, cannot load limits")
|
||||
)
|
||||
|
||||
// ExecutionLimits defines minimum and maximum values in relation to
|
||||
// order size, order pricing, total notional values, total maximum orders etc
|
||||
// for execution on an exchange.
|
||||
type ExecutionLimits struct {
|
||||
m map[asset.Item]map[currency.Code]map[currency.Code]*Limits
|
||||
mtx sync.RWMutex
|
||||
}
|
||||
|
||||
// MinMaxLevel defines the minimum and maximum parameters for a currency pair
|
||||
// for outbound exchange execution
|
||||
type MinMaxLevel struct {
|
||||
Pair currency.Pair
|
||||
Asset asset.Item
|
||||
MinPrice float64
|
||||
MaxPrice float64
|
||||
StepPrice float64
|
||||
MultiplierUp float64
|
||||
MultiplierDown float64
|
||||
MultiplierDecimal float64
|
||||
AveragePriceMinutes int64
|
||||
MinAmount float64
|
||||
MaxAmount float64
|
||||
StepAmount float64
|
||||
MinNotional float64
|
||||
MaxIcebergParts int64
|
||||
MarketMinQty float64
|
||||
MarketMaxQty float64
|
||||
MarketStepSize float64
|
||||
MaxTotalOrders int64
|
||||
MaxAlgoOrders int64
|
||||
}
|
||||
|
||||
// LoadLimits loads all limits levels into memory
|
||||
func (e *ExecutionLimits) LoadLimits(levels []MinMaxLevel) error {
|
||||
if len(levels) == 0 {
|
||||
return errCannotLoadLimit
|
||||
}
|
||||
e.mtx.Lock()
|
||||
defer e.mtx.Unlock()
|
||||
if e.m == nil {
|
||||
e.m = make(map[asset.Item]map[currency.Code]map[currency.Code]*Limits)
|
||||
}
|
||||
|
||||
for x := range levels {
|
||||
m1, ok := e.m[levels[x].Asset]
|
||||
if !ok {
|
||||
m1 = make(map[currency.Code]map[currency.Code]*Limits)
|
||||
e.m[levels[x].Asset] = m1
|
||||
}
|
||||
|
||||
m2, ok := m1[levels[x].Pair.Base]
|
||||
if !ok {
|
||||
m2 = make(map[currency.Code]*Limits)
|
||||
m1[levels[x].Pair.Base] = m2
|
||||
}
|
||||
|
||||
limit, ok := m2[levels[x].Pair.Quote]
|
||||
if !ok {
|
||||
limit = new(Limits)
|
||||
m2[levels[x].Pair.Quote] = limit
|
||||
}
|
||||
|
||||
if levels[x].MinPrice > levels[x].MaxPrice {
|
||||
return fmt.Errorf("%w for %s %s supplied min: %f max: %f",
|
||||
errInvalidPriceLevels,
|
||||
levels[x].Asset,
|
||||
levels[x].Pair,
|
||||
levels[x].MinPrice,
|
||||
levels[x].MaxPrice)
|
||||
}
|
||||
|
||||
if levels[x].MinAmount > levels[x].MaxAmount {
|
||||
return fmt.Errorf("%w for %s %s supplied min: %f max: %f",
|
||||
errInvalidAmountLevels,
|
||||
levels[x].Asset,
|
||||
levels[x].Pair,
|
||||
levels[x].MinAmount,
|
||||
levels[x].MaxAmount)
|
||||
}
|
||||
limit.m.Lock()
|
||||
limit.minPrice = levels[x].MinPrice
|
||||
limit.maxPrice = levels[x].MaxPrice
|
||||
limit.stepIncrementSizePrice = levels[x].StepPrice
|
||||
limit.minAmount = levels[x].MinAmount
|
||||
limit.maxAmount = levels[x].MaxAmount
|
||||
limit.stepIncrementSizeAmount = levels[x].StepAmount
|
||||
limit.minNotional = levels[x].MinNotional
|
||||
limit.multiplierUp = levels[x].MultiplierUp
|
||||
limit.multiplierDown = levels[x].MultiplierDown
|
||||
limit.averagePriceMinutes = levels[x].AveragePriceMinutes
|
||||
limit.maxIcebergParts = levels[x].MaxIcebergParts
|
||||
limit.marketMinQty = levels[x].MarketMinQty
|
||||
limit.marketMaxQty = levels[x].MarketMaxQty
|
||||
limit.marketStepIncrementSize = levels[x].MarketStepSize
|
||||
limit.maxTotalOrders = levels[x].MaxTotalOrders
|
||||
limit.maxAlgoOrders = levels[x].MaxAlgoOrders
|
||||
limit.m.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrderExecutionLimits returns the exchange limit parameters for a currency
|
||||
func (e *ExecutionLimits) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*Limits, error) {
|
||||
e.mtx.RLock()
|
||||
defer e.mtx.RUnlock()
|
||||
|
||||
if e.m == nil {
|
||||
return nil, ErrExchangeLimitNotLoaded
|
||||
}
|
||||
|
||||
m1, ok := e.m[a]
|
||||
if !ok {
|
||||
return nil, errExchangeLimitAsset
|
||||
}
|
||||
|
||||
m2, ok := m1[cp.Base]
|
||||
if !ok {
|
||||
return nil, errExchangeLimitBase
|
||||
}
|
||||
|
||||
limit, ok := m2[cp.Quote]
|
||||
if !ok {
|
||||
return nil, errExchangeLimitQuote
|
||||
}
|
||||
|
||||
return limit, nil
|
||||
}
|
||||
|
||||
// CheckOrderExecutionLimits checks to see if the price and amount conforms with
|
||||
// exchange level order execution limits
|
||||
func (e *ExecutionLimits) CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType Type) error {
|
||||
e.mtx.RLock()
|
||||
defer e.mtx.RUnlock()
|
||||
|
||||
if e.m == nil {
|
||||
// No exchange limits loaded so we can nil this
|
||||
return nil
|
||||
}
|
||||
|
||||
m1, ok := e.m[a]
|
||||
if !ok {
|
||||
return errCannotValidateAsset
|
||||
}
|
||||
|
||||
m2, ok := m1[cp.Base]
|
||||
if !ok {
|
||||
return errCannotValidateBaseCurrency
|
||||
}
|
||||
|
||||
limit, ok := m2[cp.Quote]
|
||||
if !ok {
|
||||
return errCannotValidateQuoteCurrency
|
||||
}
|
||||
|
||||
err := limit.Conforms(price, amount, orderType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w for %s %s", err, a, cp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Limits defines total limit values for an associated currency to be checked
|
||||
// before execution on an exchange
|
||||
type Limits struct {
|
||||
minPrice float64
|
||||
maxPrice float64
|
||||
stepIncrementSizePrice float64
|
||||
minAmount float64
|
||||
maxAmount float64
|
||||
stepIncrementSizeAmount float64
|
||||
minNotional float64
|
||||
multiplierUp float64
|
||||
multiplierDown float64
|
||||
averagePriceMinutes int64
|
||||
maxIcebergParts int64
|
||||
marketMinQty float64
|
||||
marketMaxQty float64
|
||||
marketStepIncrementSize float64
|
||||
maxTotalOrders int64
|
||||
maxAlgoOrders int64
|
||||
m sync.RWMutex
|
||||
}
|
||||
|
||||
// Conforms checks outbound parameters
|
||||
func (l *Limits) Conforms(price, amount float64, orderType Type) error {
|
||||
if l == nil {
|
||||
// For when we return a nil pointer we can assume there's nothing to
|
||||
// check
|
||||
return nil
|
||||
}
|
||||
|
||||
l.m.RLock()
|
||||
defer l.m.RUnlock()
|
||||
if l.minAmount != 0 && amount < l.minAmount {
|
||||
return fmt.Errorf("%w min: %.8f supplied %.8f",
|
||||
ErrAmountBelowMin,
|
||||
l.minAmount,
|
||||
amount)
|
||||
}
|
||||
if l.maxAmount != 0 && amount > l.maxAmount {
|
||||
return fmt.Errorf("%w min: %.8f supplied %.8f",
|
||||
ErrAmountExceedsMax,
|
||||
l.maxAmount,
|
||||
amount)
|
||||
}
|
||||
if l.stepIncrementSizeAmount != 0 {
|
||||
dAmount := decimal.NewFromFloat(amount)
|
||||
dMinAmount := decimal.NewFromFloat(l.minAmount)
|
||||
dStep := decimal.NewFromFloat(l.stepIncrementSizeAmount)
|
||||
if !dAmount.Sub(dMinAmount).Mod(dStep).IsZero() {
|
||||
return fmt.Errorf("%w stepSize: %.8f supplied %.8f",
|
||||
ErrAmountExceedsStep,
|
||||
l.stepIncrementSizeAmount,
|
||||
amount)
|
||||
}
|
||||
}
|
||||
|
||||
// Multiplier checking not done due to the fact we need coherence with the
|
||||
// last average price (TODO)
|
||||
// l.multiplierUp will be used to determine how far our price can go up
|
||||
// l.multiplierDown will be used to determine how far our price can go down
|
||||
// l.averagePriceMinutes will be used to determine mean over this period
|
||||
|
||||
// Max iceberg parts checking not done as we do not have that
|
||||
// functionality yet (TODO)
|
||||
// l.maxIcebergParts // How many components in an iceberg order
|
||||
|
||||
// Max total orders not done due to order manager limitations (TODO)
|
||||
// l.maxTotalOrders
|
||||
|
||||
// Max algo orders not done due to order manager limitations (TODO)
|
||||
// l.maxAlgoOrders
|
||||
|
||||
// If order type is Market we do not need to do price checks
|
||||
if orderType != Market {
|
||||
if l.minPrice != 0 && price < l.minPrice {
|
||||
return fmt.Errorf("%w min: %.8f supplied %.8f",
|
||||
ErrPriceBelowMin,
|
||||
l.minPrice,
|
||||
price)
|
||||
}
|
||||
if l.maxPrice != 0 && price > l.maxPrice {
|
||||
return fmt.Errorf("%w max: %.8f supplied %.8f",
|
||||
ErrPriceExceedsMax,
|
||||
l.maxPrice,
|
||||
price)
|
||||
}
|
||||
if l.minNotional != 0 && (amount*price) < l.minNotional {
|
||||
return fmt.Errorf("%w minimum notional: %.8f value of order %.8f",
|
||||
ErrNotionalValue,
|
||||
l.minNotional,
|
||||
amount*price)
|
||||
}
|
||||
if l.stepIncrementSizePrice != 0 {
|
||||
dPrice := decimal.NewFromFloat(price)
|
||||
dMinPrice := decimal.NewFromFloat(l.minPrice)
|
||||
dStep := decimal.NewFromFloat(l.stepIncrementSizePrice)
|
||||
if !dPrice.Sub(dMinPrice).Mod(dStep).IsZero() {
|
||||
return fmt.Errorf("%w stepSize: %.8f supplied %.8f",
|
||||
ErrPriceExceedsStep,
|
||||
l.stepIncrementSizePrice,
|
||||
price)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if l.marketMinQty != 0 &&
|
||||
l.minAmount < l.marketMinQty &&
|
||||
amount < l.marketMinQty {
|
||||
return fmt.Errorf("%w min: %.8f supplied %.8f",
|
||||
ErrMarketAmountBelowMin,
|
||||
l.marketMinQty,
|
||||
amount)
|
||||
}
|
||||
if l.marketMaxQty != 0 &&
|
||||
l.maxAmount > l.marketMaxQty &&
|
||||
amount > l.marketMaxQty {
|
||||
return fmt.Errorf("%w max: %.8f supplied %.8f",
|
||||
ErrMarketAmountExceedsMax,
|
||||
l.marketMaxQty,
|
||||
amount)
|
||||
}
|
||||
if l.marketStepIncrementSize != 0 && l.stepIncrementSizeAmount != l.marketStepIncrementSize {
|
||||
dAmount := decimal.NewFromFloat(amount)
|
||||
dMinMAmount := decimal.NewFromFloat(l.marketMinQty)
|
||||
dStep := decimal.NewFromFloat(l.marketStepIncrementSize)
|
||||
if !dAmount.Sub(dMinMAmount).Mod(dStep).IsZero() {
|
||||
return fmt.Errorf("%w stepSize: %.8f supplied %.8f",
|
||||
ErrMarketAmountExceedsStep,
|
||||
l.marketStepIncrementSize,
|
||||
amount)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConformToAmount (POC) conforms amount to its amount interval
|
||||
func (l *Limits) ConformToAmount(amount float64) float64 {
|
||||
if l == nil {
|
||||
// For when we return a nil pointer we can assume there's nothing to
|
||||
// check
|
||||
return amount
|
||||
}
|
||||
l.m.Lock()
|
||||
defer l.m.Unlock()
|
||||
if l.stepIncrementSizeAmount == 0 || amount == l.stepIncrementSizeAmount {
|
||||
return amount
|
||||
}
|
||||
|
||||
if amount < l.stepIncrementSizeAmount {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Convert floats to decimal types
|
||||
dAmount := decimal.NewFromFloat(amount)
|
||||
dStep := decimal.NewFromFloat(l.stepIncrementSizeAmount)
|
||||
// derive modulus
|
||||
mod := dAmount.Mod(dStep)
|
||||
// subtract modulus to get the floor
|
||||
rVal := dAmount.Sub(mod)
|
||||
fVal, _ := rVal.Float64()
|
||||
return fVal
|
||||
}
|
||||
319
exchanges/order/limits_test.go
Normal file
319
exchanges/order/limits_test.go
Normal file
@@ -0,0 +1,319 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
)
|
||||
|
||||
var btcusd = currency.NewPair(currency.BTC, currency.USD)
|
||||
var ltcusd = currency.NewPair(currency.LTC, currency.USD)
|
||||
var btcltc = currency.NewPair(currency.BTC, currency.LTC)
|
||||
|
||||
func TestLoadLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := ExecutionLimits{}
|
||||
err := e.LoadLimits(nil)
|
||||
if !errors.Is(err, errCannotLoadLimit) {
|
||||
t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err)
|
||||
}
|
||||
|
||||
newLimits := []MinMaxLevel{
|
||||
{
|
||||
Pair: btcusd,
|
||||
Asset: asset.Spot,
|
||||
MinPrice: 100000,
|
||||
MaxPrice: 1000000,
|
||||
MinAmount: 1,
|
||||
MaxAmount: 10,
|
||||
},
|
||||
}
|
||||
|
||||
err = e.LoadLimits(newLimits)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
|
||||
badLimit := []MinMaxLevel{
|
||||
{
|
||||
Pair: btcusd,
|
||||
Asset: asset.Spot,
|
||||
MinPrice: 2,
|
||||
MaxPrice: 1,
|
||||
MinAmount: 1,
|
||||
MaxAmount: 10,
|
||||
},
|
||||
}
|
||||
|
||||
err = e.LoadLimits(badLimit)
|
||||
if !errors.Is(err, errInvalidPriceLevels) {
|
||||
t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err)
|
||||
}
|
||||
|
||||
badLimit = []MinMaxLevel{
|
||||
{
|
||||
Pair: btcusd,
|
||||
Asset: asset.Spot,
|
||||
MinPrice: 1,
|
||||
MaxPrice: 2,
|
||||
MinAmount: 10,
|
||||
MaxAmount: 9,
|
||||
},
|
||||
}
|
||||
|
||||
err = e.LoadLimits(badLimit)
|
||||
if !errors.Is(err, errInvalidAmountLevels) {
|
||||
t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err)
|
||||
}
|
||||
|
||||
goodLimit := []MinMaxLevel{
|
||||
{
|
||||
Pair: btcusd,
|
||||
Asset: asset.Spot,
|
||||
},
|
||||
}
|
||||
|
||||
err = e.LoadLimits(goodLimit)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrderExecutionLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := ExecutionLimits{}
|
||||
_, err := e.GetOrderExecutionLimits(asset.Spot, btcusd)
|
||||
if !errors.Is(err, ErrExchangeLimitNotLoaded) {
|
||||
t.Fatalf("expected error %v but received %v", ErrExchangeLimitNotLoaded, err)
|
||||
}
|
||||
|
||||
newLimits := []MinMaxLevel{
|
||||
{
|
||||
Pair: btcusd,
|
||||
Asset: asset.Spot,
|
||||
MinPrice: 100000,
|
||||
MaxPrice: 1000000,
|
||||
MinAmount: 1,
|
||||
MaxAmount: 10,
|
||||
},
|
||||
}
|
||||
|
||||
err = e.LoadLimits(newLimits)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err)
|
||||
}
|
||||
|
||||
_, err = e.GetOrderExecutionLimits(asset.Futures, ltcusd)
|
||||
if !errors.Is(err, errExchangeLimitAsset) {
|
||||
t.Fatalf("expected error %v but received %v", errExchangeLimitAsset, err)
|
||||
}
|
||||
|
||||
_, err = e.GetOrderExecutionLimits(asset.Spot, ltcusd)
|
||||
if !errors.Is(err, errExchangeLimitBase) {
|
||||
t.Fatalf("expected error %v but received %v", errExchangeLimitBase, err)
|
||||
}
|
||||
|
||||
_, err = e.GetOrderExecutionLimits(asset.Spot, btcltc)
|
||||
if !errors.Is(err, errExchangeLimitQuote) {
|
||||
t.Fatalf("expected error %v but received %v", errExchangeLimitQuote, err)
|
||||
}
|
||||
|
||||
tt, err := e.GetOrderExecutionLimits(asset.Spot, btcusd)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
|
||||
if tt.maxAmount != newLimits[0].MaxAmount ||
|
||||
tt.minAmount != newLimits[0].MinAmount ||
|
||||
tt.maxPrice != newLimits[0].MaxPrice ||
|
||||
tt.minPrice != newLimits[0].MinPrice {
|
||||
t.Fatal("unexpected values")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := ExecutionLimits{}
|
||||
err := e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 1337, Limit)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
|
||||
newLimits := []MinMaxLevel{
|
||||
{
|
||||
Pair: btcusd,
|
||||
Asset: asset.Spot,
|
||||
MinPrice: 100000,
|
||||
MaxPrice: 1000000,
|
||||
MinAmount: 1,
|
||||
MaxAmount: 10,
|
||||
},
|
||||
}
|
||||
|
||||
err = e.LoadLimits(newLimits)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Futures, ltcusd, 1337, 1337, Limit)
|
||||
if !errors.Is(err, errCannotValidateAsset) {
|
||||
t.Fatalf("expected error %v but received %v", errCannotValidateAsset, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, ltcusd, 1337, 1337, Limit)
|
||||
if !errors.Is(err, errCannotValidateBaseCurrency) {
|
||||
t.Fatalf("expected error %v but received %v", errCannotValidateBaseCurrency, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, btcltc, 1337, 1337, Limit)
|
||||
if !errors.Is(err, errCannotValidateQuoteCurrency) {
|
||||
t.Fatalf("expected error %v but received %v", errCannotValidateQuoteCurrency, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 9, Limit)
|
||||
if !errors.Is(err, ErrPriceBelowMin) {
|
||||
t.Fatalf("expected error %v but received %v", ErrPriceBelowMin, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1000001, 9, Limit)
|
||||
if !errors.Is(err, ErrPriceExceedsMax) {
|
||||
t.Fatalf("expected error %v but received %v", ErrPriceExceedsMax, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, .5, Limit)
|
||||
if !errors.Is(err, ErrAmountBelowMin) {
|
||||
t.Fatalf("expected error %v but received %v", ErrAmountBelowMin, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 11, Limit)
|
||||
if !errors.Is(err, ErrAmountExceedsMax) {
|
||||
t.Fatalf("expected error %v but received %v", ErrAmountExceedsMax, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Limit)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
|
||||
err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Market)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConforms(t *testing.T) {
|
||||
t.Parallel()
|
||||
var tt *Limits
|
||||
err := tt.Conforms(0, 0, Limit)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tt = &Limits{
|
||||
minNotional: 100,
|
||||
}
|
||||
|
||||
err = tt.Conforms(1, 1, Limit)
|
||||
if !errors.Is(err, ErrNotionalValue) {
|
||||
t.Fatalf("expected error %v but received %v", ErrNotionalValue, err)
|
||||
}
|
||||
|
||||
err = tt.Conforms(200, .5, Limit)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
|
||||
tt.stepIncrementSizePrice = 0.001
|
||||
err = tt.Conforms(200.0001, .5, Limit)
|
||||
if !errors.Is(err, ErrPriceExceedsStep) {
|
||||
t.Fatalf("expected error %v but received %v", ErrPriceExceedsStep, err)
|
||||
}
|
||||
err = tt.Conforms(200.004, .5, Limit)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
|
||||
tt.stepIncrementSizeAmount = 0.001
|
||||
err = tt.Conforms(200, .0002, Limit)
|
||||
if !errors.Is(err, ErrAmountExceedsStep) {
|
||||
t.Fatalf("expected error %v but received %v", ErrAmountExceedsStep, err)
|
||||
}
|
||||
err = tt.Conforms(200000, .003, Limit)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received %v", nil, err)
|
||||
}
|
||||
|
||||
tt.minAmount = 1
|
||||
tt.maxAmount = 10
|
||||
tt.marketMinQty = 1.1
|
||||
tt.marketMaxQty = 9.9
|
||||
|
||||
err = tt.Conforms(200000, 1, Market)
|
||||
if !errors.Is(err, ErrMarketAmountBelowMin) {
|
||||
t.Fatalf("expected error %v but received: %v", ErrMarketAmountBelowMin, err)
|
||||
}
|
||||
|
||||
err = tt.Conforms(200000, 10, Market)
|
||||
if !errors.Is(err, ErrMarketAmountExceedsMax) {
|
||||
t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsMax, err)
|
||||
}
|
||||
|
||||
tt.marketStepIncrementSize = 10
|
||||
err = tt.Conforms(200000, 9.1, Market)
|
||||
if !errors.Is(err, ErrMarketAmountExceedsStep) {
|
||||
t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsStep, err)
|
||||
}
|
||||
tt.marketStepIncrementSize = 1
|
||||
err = tt.Conforms(200000, 9.1, Market)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error %v but received: %v", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConformToAmount(t *testing.T) {
|
||||
t.Parallel()
|
||||
var tt *Limits
|
||||
if tt.ConformToAmount(1.001) != 1.001 {
|
||||
t.Fatal("value should not be changed")
|
||||
}
|
||||
|
||||
tt = &Limits{}
|
||||
val := tt.ConformToAmount(1)
|
||||
if val != 1 { // If there is no step amount set this should not change
|
||||
// the inputted amount
|
||||
t.Fatal("unexpected amount")
|
||||
}
|
||||
|
||||
tt.stepIncrementSizeAmount = 0.001
|
||||
val = tt.ConformToAmount(1.001)
|
||||
if val != 1.001 {
|
||||
t.Error("unexpected amount", val)
|
||||
}
|
||||
|
||||
val = tt.ConformToAmount(0.0001)
|
||||
if val != 0 {
|
||||
t.Error("unexpected amount", val)
|
||||
}
|
||||
|
||||
val = tt.ConformToAmount(0.7777)
|
||||
if val != 0.777 {
|
||||
t.Error("unexpected amount", val)
|
||||
}
|
||||
|
||||
tt.stepIncrementSizeAmount = 100
|
||||
val = tt.ConformToAmount(100)
|
||||
if val != 100 {
|
||||
t.Fatal("unexpected amount", val)
|
||||
}
|
||||
|
||||
val = tt.ConformToAmount(200)
|
||||
if val != 200 {
|
||||
t.Fatal("unexpected amount", val)
|
||||
}
|
||||
val = tt.ConformToAmount(150)
|
||||
if val != 100 {
|
||||
t.Fatal("unexpected amount", val)
|
||||
}
|
||||
}
|
||||
@@ -100,12 +100,8 @@ func TestValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
for x := range tester {
|
||||
if err := tester[x].Submit.Validate(tester[x].ValidOpts); err != tester[x].ExpectedErr {
|
||||
if err != nil && tester[x].ExpectedErr != nil {
|
||||
if err.Error() == tester[x].ExpectedErr.Error() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
err := tester[x].Submit.Validate(tester[x].ValidOpts)
|
||||
if !errors.Is(err, tester[x].ExpectedErr) {
|
||||
t.Errorf("Unexpected result. Got: %v, want: %v", err, tester[x].ExpectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ var (
|
||||
ErrAssetNotSet = errors.New("order asset type is not set")
|
||||
ErrSideIsInvalid = errors.New("order side is invalid")
|
||||
ErrTypeIsInvalid = errors.New("order type is invalid")
|
||||
ErrAmountIsInvalid = errors.New("order amount is invalid")
|
||||
ErrAmountIsInvalid = errors.New("order amount is equal or less than zero")
|
||||
ErrPriceMustBeSetIfLimitOrder = errors.New("order price must be set if limit order type is desired")
|
||||
ErrOrderIDNotSet = errors.New("order id or client order id is not set")
|
||||
)
|
||||
|
||||
@@ -38,25 +38,20 @@ func (s *Submit) Validate(opt ...validate.Checker) error {
|
||||
}
|
||||
|
||||
if s.Amount <= 0 {
|
||||
return ErrAmountIsInvalid
|
||||
return fmt.Errorf("submit validation error %w, suppled: %.8f", ErrAmountIsInvalid, s.Amount)
|
||||
}
|
||||
|
||||
if s.Type == Limit && s.Price <= 0 {
|
||||
return ErrPriceMustBeSetIfLimitOrder
|
||||
}
|
||||
|
||||
var errs common.Errors
|
||||
for _, o := range opt {
|
||||
err := o.Check()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
1
go.mod
1
go.mod
@@ -22,6 +22,7 @@ require (
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/shopspring/decimal v1.2.0
|
||||
github.com/spf13/afero v1.3.4 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/viper v1.7.1
|
||||
|
||||
2
go.sum
2
go.sum
@@ -301,6 +301,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
|
||||
Reference in New Issue
Block a user