mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge branch 'c9s:main' into feat/tradestats
This commit is contained in:
commit
cb8ca56afc
13
.env.local.example
Normal file
13
.env.local.example
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
SLACK_TOKEN=YOUR_TOKEN
|
||||||
|
SLACK_CHANNEL=CHANNEL_NAME
|
||||||
|
|
||||||
|
# DB_DRIVER="sqlite3"
|
||||||
|
# DB_DSN="bbgo.sqlite3"
|
||||||
|
DB_DRIVER=mysql
|
||||||
|
DB_DSN=root@tcp(127.0.0.1:3306)/bbgo
|
||||||
|
|
||||||
|
MAX_API_KEY=YOUR_API_KEY
|
||||||
|
MAX_API_SECRET=YOUR_API_SECRET
|
||||||
|
|
||||||
|
BINANCE_API_KEY=YOUR_API_KEY
|
||||||
|
BINANCE_API_SECRET=YOUR_API_SECRET
|
54
config/liquiditymaker.yaml
Normal file
54
config/liquiditymaker.yaml
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
sessions:
|
||||||
|
max:
|
||||||
|
exchange: max
|
||||||
|
envVarPrefix: max
|
||||||
|
makerFeeRate: 0%
|
||||||
|
takerFeeRate: 0.025%
|
||||||
|
|
||||||
|
#services:
|
||||||
|
# googleSpreadSheet:
|
||||||
|
# jsonTokenFile: ".credentials/google-cloud/service-account-json-token.json"
|
||||||
|
# spreadSheetId: "YOUR_SPREADSHEET_ID"
|
||||||
|
|
||||||
|
exchangeStrategies:
|
||||||
|
- on: max
|
||||||
|
liquiditymaker:
|
||||||
|
symbol: &symbol USDTTWD
|
||||||
|
|
||||||
|
## adjustmentUpdateInterval is the interval for adjusting position
|
||||||
|
adjustmentUpdateInterval: 1m
|
||||||
|
|
||||||
|
## liquidityUpdateInterval is the interval for updating liquidity orders
|
||||||
|
liquidityUpdateInterval: 1h
|
||||||
|
|
||||||
|
numOfLiquidityLayers: 30
|
||||||
|
askLiquidityAmount: 20_000.0
|
||||||
|
bidLiquidityAmount: 20_000.0
|
||||||
|
liquidityPriceRange: 2%
|
||||||
|
useLastTradePrice: true
|
||||||
|
spread: 1.1%
|
||||||
|
|
||||||
|
liquidityScale:
|
||||||
|
exp:
|
||||||
|
domain: [1, 30]
|
||||||
|
range: [1, 4]
|
||||||
|
|
||||||
|
## maxExposure controls how much balance should be used for placing the maker orders
|
||||||
|
maxExposure: 200_000
|
||||||
|
minProfit: 0.01%
|
||||||
|
|
||||||
|
|
||||||
|
backtest:
|
||||||
|
sessions:
|
||||||
|
- max
|
||||||
|
startTime: "2023-05-20"
|
||||||
|
endTime: "2023-06-01"
|
||||||
|
symbols:
|
||||||
|
- *symbol
|
||||||
|
account:
|
||||||
|
max:
|
||||||
|
makerFeeRate: 0.0%
|
||||||
|
takerFeeRate: 0.025%
|
||||||
|
balances:
|
||||||
|
USDT: 5000
|
||||||
|
TWD: 150_000
|
|
@ -58,4 +58,4 @@ bbgo [flags]
|
||||||
* [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)
|
* [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)
|
||||||
* [bbgo version](bbgo_version.md) - show version name
|
* [bbgo version](bbgo_version.md) - show version name
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -41,4 +41,4 @@ bbgo account [--session SESSION] [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -50,4 +50,4 @@ bbgo backtest [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -40,4 +40,4 @@ bbgo balances [--session SESSION] [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -39,4 +39,4 @@ bbgo build [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -49,4 +49,4 @@ bbgo cancel-order [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -41,4 +41,4 @@ bbgo deposits [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -48,4 +48,4 @@ bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quanti
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -42,4 +42,4 @@ bbgo get-order --session SESSION --order-id ORDER_ID [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -45,4 +45,4 @@ bbgo hoptimize [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -42,4 +42,4 @@ bbgo kline [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -41,4 +41,4 @@ bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -38,4 +38,4 @@ margin related history
|
||||||
* [bbgo margin loans](bbgo_margin_loans.md) - query loans history
|
* [bbgo margin loans](bbgo_margin_loans.md) - query loans history
|
||||||
* [bbgo margin repays](bbgo_margin_repays.md) - query repay history
|
* [bbgo margin repays](bbgo_margin_repays.md) - query repay history
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -41,4 +41,4 @@ bbgo margin interests --session=SESSION_NAME --asset=ASSET [flags]
|
||||||
|
|
||||||
* [bbgo margin](bbgo_margin.md) - margin related history
|
* [bbgo margin](bbgo_margin.md) - margin related history
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -41,4 +41,4 @@ bbgo margin loans --session=SESSION_NAME --asset=ASSET [flags]
|
||||||
|
|
||||||
* [bbgo margin](bbgo_margin.md) - margin related history
|
* [bbgo margin](bbgo_margin.md) - margin related history
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -41,4 +41,4 @@ bbgo margin repays --session=SESSION_NAME --asset=ASSET [flags]
|
||||||
|
|
||||||
* [bbgo margin](bbgo_margin.md) - margin related history
|
* [bbgo margin](bbgo_margin.md) - margin related history
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -40,4 +40,4 @@ bbgo market [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -44,4 +44,4 @@ bbgo optimize [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -42,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -40,4 +40,4 @@ bbgo orderupdate [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -49,4 +49,4 @@ bbgo pnl [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -51,4 +51,4 @@ bbgo run [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -46,4 +46,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -42,4 +42,4 @@ bbgo sync [--session=[exchange_name]] [--symbol=[pair_name]] [[--since=yyyy/mm/d
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -42,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -40,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -42,4 +42,4 @@ bbgo transfer-history [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -40,4 +40,4 @@ bbgo userdatastream [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
|
@ -39,4 +39,4 @@ bbgo version [flags]
|
||||||
|
|
||||||
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 2-Oct-2023
|
###### Auto generated by spf13/cobra on 9-Nov-2023
|
||||||
|
|
65
doc/release/v1.53.0.md
Normal file
65
doc/release/v1.53.0.md
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
[Full Changelog](https://github.com/c9s/bbgo/compare/v1.52.0...main)
|
||||||
|
|
||||||
|
- [#1401](https://github.com/c9s/bbgo/pull/1401): STRATEGY: add liquidity maker
|
||||||
|
- [#1403](https://github.com/c9s/bbgo/pull/1403): FEATURE: [bybit] add assertion for API response
|
||||||
|
- [#1394](https://github.com/c9s/bbgo/pull/1394): FEATURE: [bitget] support query closed orders
|
||||||
|
- [#1392](https://github.com/c9s/bbgo/pull/1392): FEATURE: [bitget] add query open orders
|
||||||
|
- [#1396](https://github.com/c9s/bbgo/pull/1396): FEATURE: add ttl for position/grid2.profit stats persistence
|
||||||
|
- [#1395](https://github.com/c9s/bbgo/pull/1395): FIX: fix skip syncing active order
|
||||||
|
- [#1398](https://github.com/c9s/bbgo/pull/1398): FIX: [bybit] rm retry and add fee recover
|
||||||
|
- [#1397](https://github.com/c9s/bbgo/pull/1397): FEATURE: [bybit] to periodically fetch the fee rate
|
||||||
|
- [#1391](https://github.com/c9s/bbgo/pull/1391): FIX: [grid2] respect BaseGridNum and add a failing test case
|
||||||
|
- [#1390](https://github.com/c9s/bbgo/pull/1390): FIX: [rebalance] fix buy quantity
|
||||||
|
- [#1380](https://github.com/c9s/bbgo/pull/1380): FEATURE: [bitget] support kline subscription on stream
|
||||||
|
- [#1385](https://github.com/c9s/bbgo/pull/1385): FEATURE: [bitget] add query tickers api
|
||||||
|
- [#1376](https://github.com/c9s/bbgo/pull/1376): FEATURE: query trades from db page by page
|
||||||
|
- [#1386](https://github.com/c9s/bbgo/pull/1386): REFACTOR: [wall] refactor wall strategy with common.Strategy
|
||||||
|
- [#1382](https://github.com/c9s/bbgo/pull/1382): REFACTOR: [bitget] add rate limiter for account, ticker
|
||||||
|
- [#1384](https://github.com/c9s/bbgo/pull/1384): CHORE: minor improvements on backtest cmd
|
||||||
|
- [#1381](https://github.com/c9s/bbgo/pull/1381): DOC: grammatical errors in the README.md
|
||||||
|
- [#1377](https://github.com/c9s/bbgo/pull/1377): REFACTOR: [rebalance] submit one order at a time
|
||||||
|
- [#1378](https://github.com/c9s/bbgo/pull/1378): REFACTOR: [bitget] get symbol api
|
||||||
|
- [#1375](https://github.com/c9s/bbgo/pull/1375): DOC: grammatical error in the code_of_conduct file
|
||||||
|
- [#1374](https://github.com/c9s/bbgo/pull/1374): FIX: retry to get open orders only for 5 times and do not sync orders…
|
||||||
|
- [#1368](https://github.com/c9s/bbgo/pull/1368): FEATURE: merge grid recover and active orders recover logic
|
||||||
|
- [#1367](https://github.com/c9s/bbgo/pull/1367): DOC: fix typos in doc/development
|
||||||
|
- [#1372](https://github.com/c9s/bbgo/pull/1372): FIX: [bybit][kucoin] fix negative volume, price precision
|
||||||
|
- [#1373](https://github.com/c9s/bbgo/pull/1373): FEATURE: [xalign] adjust quantity by max amount
|
||||||
|
- [#1363](https://github.com/c9s/bbgo/pull/1363): FEATURE: [bitget] support ping/pong
|
||||||
|
- [#1370](https://github.com/c9s/bbgo/pull/1370): REFACTOR: [stream] move ping into stream level
|
||||||
|
- [#1361](https://github.com/c9s/bbgo/pull/1361): FEATURE: prepare query trades funtion for new recover
|
||||||
|
- [#1365](https://github.com/c9s/bbgo/pull/1365): FEATURE: [batch] add jumpIfEmpty opts to closed order batch query
|
||||||
|
- [#1364](https://github.com/c9s/bbgo/pull/1364): FEATURE: [batch] add a jumpIfEmpty to batch trade option
|
||||||
|
- [#1362](https://github.com/c9s/bbgo/pull/1362): DOC: Modified README.md file's language.
|
||||||
|
- [#1360](https://github.com/c9s/bbgo/pull/1360): DOC: Update CONTRIBUTING.md
|
||||||
|
- [#1351](https://github.com/c9s/bbgo/pull/1351): DOC: Update README.md
|
||||||
|
- [#1355](https://github.com/c9s/bbgo/pull/1355): REFACTOR: rename file and variable
|
||||||
|
- [#1358](https://github.com/c9s/bbgo/pull/1358): MINOR: [indicator] remove zero padding from RMA
|
||||||
|
- [#1357](https://github.com/c9s/bbgo/pull/1357): FIX: Fix duplicate RMA values and add test cases
|
||||||
|
- [#1356](https://github.com/c9s/bbgo/pull/1356): FIX: fix rma zero value issue
|
||||||
|
- [#1350](https://github.com/c9s/bbgo/pull/1350): FEATURE: [grid2] twin orderbook
|
||||||
|
- [#1353](https://github.com/c9s/bbgo/pull/1353): CHORE: go: update requestgen to v1.3.5
|
||||||
|
- [#1349](https://github.com/c9s/bbgo/pull/1349): MINOR: remove profit entries from profit stats
|
||||||
|
- [#1352](https://github.com/c9s/bbgo/pull/1352): DOC: Fixed a typo in README.md
|
||||||
|
- [#1347](https://github.com/c9s/bbgo/pull/1347): FEATURE: [bitget] support market trade stream
|
||||||
|
- [#1344](https://github.com/c9s/bbgo/pull/1344): FEATURE: [bitget] support book stream on bitget
|
||||||
|
- [#1280](https://github.com/c9s/bbgo/pull/1280): FEATURE: [bitget] integrate QueryMarkets, QueryTicker and QueryAccount api
|
||||||
|
- [#1346](https://github.com/c9s/bbgo/pull/1346): FIX: [xnav] skip public only session
|
||||||
|
- [#1345](https://github.com/c9s/bbgo/pull/1345): FIX: [bbgo] check symbol length for injection
|
||||||
|
- [#1343](https://github.com/c9s/bbgo/pull/1343): FIX: [max] remove outdated margin fields
|
||||||
|
- [#1328](https://github.com/c9s/bbgo/pull/1328): FEATURE: recover active orders with open orders periodically
|
||||||
|
- [#1341](https://github.com/c9s/bbgo/pull/1341): REFACTOR: [random] remove adjustQuantity from config
|
||||||
|
- [#1342](https://github.com/c9s/bbgo/pull/1342): CHORE: make rightWindow possible to be set as zero
|
||||||
|
- [#1339](https://github.com/c9s/bbgo/pull/1339): FEATURE: [BYBIT] support order book depth 200 on bybit
|
||||||
|
- [#1340](https://github.com/c9s/bbgo/pull/1340): CHORE: update xfixedmaker config for backtest
|
||||||
|
- [#1335](https://github.com/c9s/bbgo/pull/1335): FEATURE: add custom private channel support to max
|
||||||
|
- [#1338](https://github.com/c9s/bbgo/pull/1338): FIX: [grid2] set max retries to 5
|
||||||
|
- [#1337](https://github.com/c9s/bbgo/pull/1337): REFACTOR: rename randomtrader to random
|
||||||
|
- [#1327](https://github.com/c9s/bbgo/pull/1327): FIX: Fix duplicate orders caused by position risk control
|
||||||
|
- [#1331](https://github.com/c9s/bbgo/pull/1331): FEATURE: add xfixedmaker strategy
|
||||||
|
- [#1336](https://github.com/c9s/bbgo/pull/1336): FEATURE: add randomtrader strategy
|
||||||
|
- [#1332](https://github.com/c9s/bbgo/pull/1332): FEATURE: add supported interval for okex
|
||||||
|
- [#1232](https://github.com/c9s/bbgo/pull/1232): FEATURE: add forceOrder api for binance to show liquid info
|
||||||
|
- [#1334](https://github.com/c9s/bbgo/pull/1334): CHORE: [maxapi] change default http transport settings
|
||||||
|
- [#1330](https://github.com/c9s/bbgo/pull/1330): REFACTOR: Make fixedmaker simpler
|
||||||
|
- [#1312](https://github.com/c9s/bbgo/pull/1312): FEATURE: add QueryClosedOrders() and QueryTrades() for okex
|
2
go.mod
2
go.mod
|
@ -9,7 +9,7 @@ require (
|
||||||
github.com/Masterminds/squirrel v1.5.3
|
github.com/Masterminds/squirrel v1.5.3
|
||||||
github.com/adshao/go-binance/v2 v2.4.2
|
github.com/adshao/go-binance/v2 v2.4.2
|
||||||
github.com/c-bata/goptuna v0.8.1
|
github.com/c-bata/goptuna v0.8.1
|
||||||
github.com/c9s/requestgen v1.3.5
|
github.com/c9s/requestgen v1.3.6
|
||||||
github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b
|
github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b
|
||||||
github.com/cenkalti/backoff/v4 v4.2.0
|
github.com/cenkalti/backoff/v4 v4.2.0
|
||||||
github.com/cheggaaa/pb/v3 v3.0.8
|
github.com/cheggaaa/pb/v3 v3.0.8
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -86,6 +86,8 @@ github.com/c9s/requestgen v1.3.4 h1:kK2rIO3OAt9JoY5gT0OSkSpq0dy/+JeuI22FwSKpUrY=
|
||||||
github.com/c9s/requestgen v1.3.4/go.mod h1:wp4saiPdh0zLF5AkopGCqPQfy9Q5xvRh+TQBOA1l1r4=
|
github.com/c9s/requestgen v1.3.4/go.mod h1:wp4saiPdh0zLF5AkopGCqPQfy9Q5xvRh+TQBOA1l1r4=
|
||||||
github.com/c9s/requestgen v1.3.5 h1:iGYAP0rWQW3JOo+Z3S0SoenSt581IQ9mupJxRFCrCJs=
|
github.com/c9s/requestgen v1.3.5 h1:iGYAP0rWQW3JOo+Z3S0SoenSt581IQ9mupJxRFCrCJs=
|
||||||
github.com/c9s/requestgen v1.3.5/go.mod h1:QwkZudcv84kJ8g9+E0RDTj+13btFXbTvv2aI+zbuLbc=
|
github.com/c9s/requestgen v1.3.5/go.mod h1:QwkZudcv84kJ8g9+E0RDTj+13btFXbTvv2aI+zbuLbc=
|
||||||
|
github.com/c9s/requestgen v1.3.6 h1:ul7dZ2uwGYjNBjreooRfSY10WTXvQmQSjZsHebz6QfE=
|
||||||
|
github.com/c9s/requestgen v1.3.6/go.mod h1:QwkZudcv84kJ8g9+E0RDTj+13btFXbTvv2aI+zbuLbc=
|
||||||
github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs=
|
github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b h1:wT8c03PHLv7+nZUIGqxAzRvIfYHNxMCNVWwvdGkOXTs=
|
||||||
github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ=
|
github.com/c9s/rockhopper v1.2.2-0.20220617053729-ffdc87df194b/go.mod h1:EKObf66Cp7erWxym2de+07qNN5T1N9PXxHdh97N44EQ=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/irr"
|
_ "github.com/c9s/bbgo/pkg/strategy/irr"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/kline"
|
_ "github.com/c9s/bbgo/pkg/strategy/kline"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/linregmaker"
|
_ "github.com/c9s/bbgo/pkg/strategy/linregmaker"
|
||||||
|
_ "github.com/c9s/bbgo/pkg/strategy/liquiditymaker"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/marketcap"
|
_ "github.com/c9s/bbgo/pkg/strategy/marketcap"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/pivotshort"
|
_ "github.com/c9s/bbgo/pkg/strategy/pivotshort"
|
||||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
||||||
|
|
29
pkg/exchange/bitget/bitgetapi/v2/cancel_order_request.go
Normal file
29
pkg/exchange/bitget/bitgetapi/v2/cancel_order_request.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
|
||||||
|
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/requestgen"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CancelOrder struct {
|
||||||
|
// OrderId are always numeric. It's confirmed with official customer service. https://t.me/bitgetOpenapi/24172
|
||||||
|
OrderId types.StrInt64 `json:"orderId"`
|
||||||
|
ClientOrderId string `json:"clientOid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate PostRequest -url "/api/v2/spot/trade/cancel-order" -type CancelOrderRequest -responseDataType .CancelOrder
|
||||||
|
type CancelOrderRequest struct {
|
||||||
|
client requestgen.AuthenticatedAPIClient
|
||||||
|
|
||||||
|
symbol string `param:"symbol"`
|
||||||
|
orderId *string `param:"orderId"`
|
||||||
|
clientOrderId *string `param:"clientOid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) NewCancelOrderRequest() *CancelOrderRequest {
|
||||||
|
return &CancelOrderRequest{client: c.Client}
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/cancel-order -type CancelOrderRequest -responseDataType .CancelOrder"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CancelOrderRequest) Symbol(symbol string) *CancelOrderRequest {
|
||||||
|
c.symbol = symbol
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelOrderRequest) OrderId(orderId string) *CancelOrderRequest {
|
||||||
|
c.orderId = &orderId
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelOrderRequest) ClientOrderId(clientOrderId string) *CancelOrderRequest {
|
||||||
|
c.clientOrderId = &clientOrderId
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||||
|
func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
for _k, _v := range params {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParameters builds and checks the parameters and return the result in a map object
|
||||||
|
func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
// check symbol field -> json key symbol
|
||||||
|
symbol := c.symbol
|
||||||
|
|
||||||
|
// assign parameter of symbol
|
||||||
|
params["symbol"] = symbol
|
||||||
|
// check orderId field -> json key orderId
|
||||||
|
if c.orderId != nil {
|
||||||
|
orderId := *c.orderId
|
||||||
|
|
||||||
|
// assign parameter of orderId
|
||||||
|
params["orderId"] = orderId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check clientOrderId field -> json key clientOid
|
||||||
|
if c.clientOrderId != nil {
|
||||||
|
clientOrderId := *c.clientOrderId
|
||||||
|
|
||||||
|
// assign parameter of clientOrderId
|
||||||
|
params["clientOid"] = clientOrderId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
|
||||||
|
func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) {
|
||||||
|
query := url.Values{}
|
||||||
|
|
||||||
|
params, err := c.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
if c.isVarSlice(_v) {
|
||||||
|
c.iterateSlice(_v, func(it interface{}) {
|
||||||
|
query.Add(_k+"[]", fmt.Sprintf("%v", it))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersJSON converts the parameters from GetParameters into the JSON format
|
||||||
|
func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||||
|
params, err := c.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
|
||||||
|
func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string {
|
||||||
|
for _k, _v := range slugs {
|
||||||
|
needleRE := regexp.MustCompile(":" + _k + "\\b")
|
||||||
|
url = needleRE.ReplaceAllString(url, _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
|
||||||
|
sliceValue := reflect.ValueOf(slice)
|
||||||
|
for _i := 0; _i < sliceValue.Len(); _i++ {
|
||||||
|
it := sliceValue.Index(_i).Interface()
|
||||||
|
_f(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelOrderRequest) isVarSlice(_v interface{}) bool {
|
||||||
|
rt := reflect.TypeOf(_v)
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
|
slugs := map[string]string{}
|
||||||
|
params, err := c.GetSlugParameters()
|
||||||
|
if err != nil {
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
slugs[_k] = fmt.Sprintf("%v", _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (c *CancelOrderRequest) GetPath() string {
|
||||||
|
return "/api/v2/spot/trade/cancel-order"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
|
func (c *CancelOrderRequest) Do(ctx context.Context) (*CancelOrder, error) {
|
||||||
|
|
||||||
|
params, err := c.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
query := url.Values{}
|
||||||
|
|
||||||
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = c.GetPath()
|
||||||
|
|
||||||
|
req, err := c.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := c.client.SendRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse bitgetapi.APIResponse
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var data CancelOrder
|
||||||
|
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
17
pkg/exchange/bitget/bitgetapi/v2/client.go
Normal file
17
pkg/exchange/bitget/bitgetapi/v2/client.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/requestgen"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIResponse = bitgetapi.APIResponse
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Client requestgen.AuthenticatedAPIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(client *bitgetapi.RestClient) *Client {
|
||||||
|
return &Client{Client: client}
|
||||||
|
}
|
81
pkg/exchange/bitget/bitgetapi/v2/client_test.go
Normal file
81
pkg/exchange/bitget/bitgetapi/v2/client_test.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
"github.com/c9s/bbgo/pkg/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTestClientOrSkip(t *testing.T) *Client {
|
||||||
|
if b, _ := strconv.ParseBool(os.Getenv("CI")); b {
|
||||||
|
t.Skip("skip test for CI")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, secret, ok := testutil.IntegrationTestConfigured(t, "BITGET")
|
||||||
|
if !ok {
|
||||||
|
t.Skip("BITGET_* env vars are not configured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := bitgetapi.NewClient()
|
||||||
|
client.Auth(key, secret, os.Getenv("BITGET_API_PASSPHRASE"))
|
||||||
|
return NewClient(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient(t *testing.T) {
|
||||||
|
client := getTestClientOrSkip(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("GetUnfilledOrdersRequest", func(t *testing.T) {
|
||||||
|
req := client.NewGetUnfilledOrdersRequest().StartTime(1)
|
||||||
|
resp, err := req.Do(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Logf("resp: %+v", resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetHistoryOrdersRequest", func(t *testing.T) {
|
||||||
|
// market buy
|
||||||
|
req, err := client.NewGetHistoryOrdersRequest().Symbol("APEUSDT").Do(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("place order resp: %+v", req)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PlaceOrderRequest", func(t *testing.T) {
|
||||||
|
req, err := client.NewPlaceOrderRequest().Symbol("APEUSDT").OrderType(OrderTypeLimit).
|
||||||
|
Side(SideTypeSell).
|
||||||
|
Price("2").
|
||||||
|
Size("5").
|
||||||
|
Force(OrderForceGTC).
|
||||||
|
Do(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("place order resp: %+v", req)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetTradeFillsRequest", func(t *testing.T) {
|
||||||
|
req, err := client.NewGetTradeFillsRequest().Symbol("APEUSDT").Do(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("get trade fills resp: %+v", req)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CancelOrderRequest", func(t *testing.T) {
|
||||||
|
req, err := client.NewPlaceOrderRequest().Symbol("APEUSDT").OrderType(OrderTypeLimit).
|
||||||
|
Side(SideTypeSell).
|
||||||
|
Price("2").
|
||||||
|
Size("5").
|
||||||
|
Force(OrderForceGTC).
|
||||||
|
Do(context.Background())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := client.NewCancelOrderRequest().Symbol("APEUSDT").OrderId(req.OrderId).Do(ctx)
|
||||||
|
t.Logf("cancel order resp: %+v", resp)
|
||||||
|
})
|
||||||
|
}
|
102
pkg/exchange/bitget/bitgetapi/v2/get_history_orders_request.go
Normal file
102
pkg/exchange/bitget/bitgetapi/v2/get_history_orders_request.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
|
||||||
|
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
"github.com/c9s/requestgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeeDetail struct {
|
||||||
|
// NewFees should have a value because when I was integrating, it already prompted,
|
||||||
|
// "If there is no 'newFees' field, this data represents earlier historical data."
|
||||||
|
NewFees struct {
|
||||||
|
// Amount deducted by coupons, unit:currency obtained from the transaction.
|
||||||
|
DeductedByCoupon fixedpoint.Value `json:"c"`
|
||||||
|
// Amount deducted in BGB (Bitget Coin), unit:BGB
|
||||||
|
DeductedInBGB fixedpoint.Value `json:"d"`
|
||||||
|
// If the BGB balance is insufficient to cover the fees, the remaining amount is deducted from the
|
||||||
|
//currency obtained from the transaction.
|
||||||
|
DeductedFromCurrency fixedpoint.Value `json:"r"`
|
||||||
|
// The total fee amount to be paid, unit :currency obtained from the transaction.
|
||||||
|
ToBePaid fixedpoint.Value `json:"t"`
|
||||||
|
// ignored
|
||||||
|
Deduction bool `json:"deduction"`
|
||||||
|
// ignored
|
||||||
|
TotalDeductionFee fixedpoint.Value `json:"totalDeductionFee"`
|
||||||
|
} `json:"newFees"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderDetail struct {
|
||||||
|
UserId types.StrInt64 `json:"userId"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
// OrderId are always numeric. It's confirmed with official customer service. https://t.me/bitgetOpenapi/24172
|
||||||
|
OrderId types.StrInt64 `json:"orderId"`
|
||||||
|
ClientOrderId string `json:"clientOid"`
|
||||||
|
Price fixedpoint.Value `json:"price"`
|
||||||
|
// Size is base coin when orderType=limit; quote coin when orderType=market
|
||||||
|
Size fixedpoint.Value `json:"size"`
|
||||||
|
OrderType OrderType `json:"orderType"`
|
||||||
|
Side SideType `json:"side"`
|
||||||
|
Status OrderStatus `json:"status"`
|
||||||
|
PriceAvg fixedpoint.Value `json:"priceAvg"`
|
||||||
|
BaseVolume fixedpoint.Value `json:"baseVolume"`
|
||||||
|
QuoteVolume fixedpoint.Value `json:"quoteVolume"`
|
||||||
|
EnterPointSource string `json:"enterPointSource"`
|
||||||
|
// The value is json string, so we unmarshal it after unmarshal OrderDetail
|
||||||
|
FeeDetailRaw string `json:"feeDetail"`
|
||||||
|
OrderSource string `json:"orderSource"`
|
||||||
|
CTime types.MillisecondTimestamp `json:"cTime"`
|
||||||
|
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||||
|
|
||||||
|
FeeDetail FeeDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OrderDetail) UnmarshalJSON(data []byte) error {
|
||||||
|
if o == nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal json from nil pointer order detail")
|
||||||
|
}
|
||||||
|
// define new type to avoid loop reference
|
||||||
|
type AuxOrderDetail OrderDetail
|
||||||
|
|
||||||
|
var aux AuxOrderDetail
|
||||||
|
if err := json.Unmarshal(data, &aux); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*o = OrderDetail(aux)
|
||||||
|
|
||||||
|
if len(aux.FeeDetailRaw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var feeDetail FeeDetail
|
||||||
|
if err := json.Unmarshal([]byte(aux.FeeDetailRaw), &feeDetail); err != nil {
|
||||||
|
return fmt.Errorf("unexpected fee detail raw: %s, err: %w", aux.FeeDetailRaw, err)
|
||||||
|
}
|
||||||
|
o.FeeDetail = feeDetail
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate GetRequest -url "/api/v2/spot/trade/history-orders" -type GetHistoryOrdersRequest -responseDataType []OrderDetail
|
||||||
|
type GetHistoryOrdersRequest struct {
|
||||||
|
client requestgen.AuthenticatedAPIClient
|
||||||
|
|
||||||
|
symbol *string `param:"symbol,query"`
|
||||||
|
// Limit number default 100 max 100
|
||||||
|
limit *string `param:"limit,query"`
|
||||||
|
// idLessThan requests the content on the page before this ID (older data), the value input should be the orderId of the corresponding interface.
|
||||||
|
idLessThan *string `param:"idLessThan,query"`
|
||||||
|
startTime *int64 `param:"startTime,query"`
|
||||||
|
endTime *int64 `param:"endTime,query"`
|
||||||
|
orderId *string `param:"orderId,query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) NewGetHistoryOrdersRequest() *GetHistoryOrdersRequest {
|
||||||
|
return &GetHistoryOrdersRequest{client: c.Client}
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/history-orders -type GetHistoryOrdersRequest -responseDataType []OrderDetail"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) Symbol(symbol string) *GetHistoryOrdersRequest {
|
||||||
|
g.symbol = &symbol
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) Limit(limit string) *GetHistoryOrdersRequest {
|
||||||
|
g.limit = &limit
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) IdLessThan(idLessThan string) *GetHistoryOrdersRequest {
|
||||||
|
g.idLessThan = &idLessThan
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) StartTime(startTime int64) *GetHistoryOrdersRequest {
|
||||||
|
g.startTime = &startTime
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) EndTime(endTime int64) *GetHistoryOrdersRequest {
|
||||||
|
g.endTime = &endTime
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) OrderId(orderId string) *GetHistoryOrdersRequest {
|
||||||
|
g.orderId = &orderId
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||||
|
func (g *GetHistoryOrdersRequest) GetQueryParameters() (url.Values, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
// check symbol field -> json key symbol
|
||||||
|
if g.symbol != nil {
|
||||||
|
symbol := *g.symbol
|
||||||
|
|
||||||
|
// assign parameter of symbol
|
||||||
|
params["symbol"] = symbol
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check limit field -> json key limit
|
||||||
|
if g.limit != nil {
|
||||||
|
limit := *g.limit
|
||||||
|
|
||||||
|
// assign parameter of limit
|
||||||
|
params["limit"] = limit
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check idLessThan field -> json key idLessThan
|
||||||
|
if g.idLessThan != nil {
|
||||||
|
idLessThan := *g.idLessThan
|
||||||
|
|
||||||
|
// assign parameter of idLessThan
|
||||||
|
params["idLessThan"] = idLessThan
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check startTime field -> json key startTime
|
||||||
|
if g.startTime != nil {
|
||||||
|
startTime := *g.startTime
|
||||||
|
|
||||||
|
// assign parameter of startTime
|
||||||
|
params["startTime"] = startTime
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check endTime field -> json key endTime
|
||||||
|
if g.endTime != nil {
|
||||||
|
endTime := *g.endTime
|
||||||
|
|
||||||
|
// assign parameter of endTime
|
||||||
|
params["endTime"] = endTime
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check orderId field -> json key orderId
|
||||||
|
if g.orderId != nil {
|
||||||
|
orderId := *g.orderId
|
||||||
|
|
||||||
|
// assign parameter of orderId
|
||||||
|
params["orderId"] = orderId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
for _k, _v := range params {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParameters builds and checks the parameters and return the result in a map object
|
||||||
|
func (g *GetHistoryOrdersRequest) GetParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
|
||||||
|
func (g *GetHistoryOrdersRequest) GetParametersQuery() (url.Values, error) {
|
||||||
|
query := url.Values{}
|
||||||
|
|
||||||
|
params, err := g.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
if g.isVarSlice(_v) {
|
||||||
|
g.iterateSlice(_v, func(it interface{}) {
|
||||||
|
query.Add(_k+"[]", fmt.Sprintf("%v", it))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersJSON converts the parameters from GetParameters into the JSON format
|
||||||
|
func (g *GetHistoryOrdersRequest) GetParametersJSON() ([]byte, error) {
|
||||||
|
params, err := g.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
|
||||||
|
func (g *GetHistoryOrdersRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string {
|
||||||
|
for _k, _v := range slugs {
|
||||||
|
needleRE := regexp.MustCompile(":" + _k + "\\b")
|
||||||
|
url = needleRE.ReplaceAllString(url, _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
|
||||||
|
sliceValue := reflect.ValueOf(slice)
|
||||||
|
for _i := 0; _i < sliceValue.Len(); _i++ {
|
||||||
|
it := sliceValue.Index(_i).Interface()
|
||||||
|
_f(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) isVarSlice(_v interface{}) bool {
|
||||||
|
rt := reflect.TypeOf(_v)
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
|
slugs := map[string]string{}
|
||||||
|
params, err := g.GetSlugParameters()
|
||||||
|
if err != nil {
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
slugs[_k] = fmt.Sprintf("%v", _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetHistoryOrdersRequest) Do(ctx context.Context) ([]OrderDetail, error) {
|
||||||
|
|
||||||
|
// no body params
|
||||||
|
var params interface{}
|
||||||
|
query, err := g.GetQueryParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := "/api/v2/spot/trade/history-orders"
|
||||||
|
|
||||||
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := g.client.SendRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse bitgetapi.APIResponse
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []OrderDetail
|
||||||
|
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOrderDetail_UnmarshalJSON(t *testing.T) {
|
||||||
|
var (
|
||||||
|
assert = assert.New(t)
|
||||||
|
)
|
||||||
|
t.Run("empty fee", func(t *testing.T) {
|
||||||
|
input := `{
|
||||||
|
"userId":"8672173294",
|
||||||
|
"symbol":"APEUSDT",
|
||||||
|
"orderId":"1104342023170068480",
|
||||||
|
"clientOid":"f3d6a1ee-4e94-48b5-a6e0-25f3e93d92e1",
|
||||||
|
"price":"1.2000000000000000",
|
||||||
|
"size":"5.0000000000000000",
|
||||||
|
"orderType":"limit",
|
||||||
|
"side":"buy",
|
||||||
|
"status":"cancelled",
|
||||||
|
"priceAvg":"0",
|
||||||
|
"baseVolume":"0.0000000000000000",
|
||||||
|
"quoteVolume":"0.0000000000000000",
|
||||||
|
"enterPointSource":"API",
|
||||||
|
"feeDetail":"",
|
||||||
|
"orderSource":"normal",
|
||||||
|
"cTime":"1699021576683",
|
||||||
|
"uTime":"1699021649099"
|
||||||
|
}`
|
||||||
|
var od OrderDetail
|
||||||
|
err := json.Unmarshal([]byte(input), &od)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(OrderDetail{
|
||||||
|
UserId: types.StrInt64(8672173294),
|
||||||
|
Symbol: "APEUSDT",
|
||||||
|
OrderId: types.StrInt64(1104342023170068480),
|
||||||
|
ClientOrderId: "f3d6a1ee-4e94-48b5-a6e0-25f3e93d92e1",
|
||||||
|
Price: fixedpoint.NewFromFloat(1.2),
|
||||||
|
Size: fixedpoint.NewFromFloat(5),
|
||||||
|
OrderType: OrderTypeLimit,
|
||||||
|
Side: SideTypeBuy,
|
||||||
|
Status: OrderStatusCancelled,
|
||||||
|
PriceAvg: fixedpoint.Zero,
|
||||||
|
BaseVolume: fixedpoint.Zero,
|
||||||
|
QuoteVolume: fixedpoint.Zero,
|
||||||
|
EnterPointSource: "API",
|
||||||
|
FeeDetailRaw: "",
|
||||||
|
OrderSource: "normal",
|
||||||
|
CTime: types.NewMillisecondTimestampFromInt(1699021576683),
|
||||||
|
UTime: types.NewMillisecondTimestampFromInt(1699021649099),
|
||||||
|
FeeDetail: FeeDetail{},
|
||||||
|
}, od)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fee", func(t *testing.T) {
|
||||||
|
input := `{
|
||||||
|
"userId":"8672173294",
|
||||||
|
"symbol":"APEUSDT",
|
||||||
|
"orderId":"1104337778433757184",
|
||||||
|
"clientOid":"8afea7bd-d873-44fe-aff8-6a1fae3cc765",
|
||||||
|
"price":"1.4000000000000000",
|
||||||
|
"size":"5.0000000000000000",
|
||||||
|
"orderType":"limit",
|
||||||
|
"side":"sell",
|
||||||
|
"status":"filled",
|
||||||
|
"priceAvg":"1.4001000000000000",
|
||||||
|
"baseVolume":"5.0000000000000000",
|
||||||
|
"quoteVolume":"7.0005000000000000",
|
||||||
|
"enterPointSource":"API",
|
||||||
|
"feeDetail":"{\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0070005,\"t\":-0.0070005,\"totalDeductionFee\":0},\"USDT\":{\"deduction\":false,\"feeCoinCode\":\"USDT\",\"totalDeductionFee\":0,\"totalFee\":-0.007000500000}}",
|
||||||
|
"orderSource":"normal",
|
||||||
|
"cTime":"1699020564659",
|
||||||
|
"uTime":"1699020564688"
|
||||||
|
}`
|
||||||
|
var od OrderDetail
|
||||||
|
err := json.Unmarshal([]byte(input), &od)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(OrderDetail{
|
||||||
|
UserId: types.StrInt64(8672173294),
|
||||||
|
Symbol: "APEUSDT",
|
||||||
|
OrderId: types.StrInt64(1104337778433757184),
|
||||||
|
ClientOrderId: "8afea7bd-d873-44fe-aff8-6a1fae3cc765",
|
||||||
|
Price: fixedpoint.NewFromFloat(1.4),
|
||||||
|
Size: fixedpoint.NewFromFloat(5),
|
||||||
|
OrderType: OrderTypeLimit,
|
||||||
|
Side: SideTypeSell,
|
||||||
|
Status: OrderStatusFilled,
|
||||||
|
PriceAvg: fixedpoint.NewFromFloat(1.4001),
|
||||||
|
BaseVolume: fixedpoint.NewFromFloat(5),
|
||||||
|
QuoteVolume: fixedpoint.NewFromFloat(7.0005),
|
||||||
|
EnterPointSource: "API",
|
||||||
|
FeeDetailRaw: `{"newFees":{"c":0,"d":0,"deduction":false,"r":-0.0070005,"t":-0.0070005,"totalDeductionFee":0},"USDT":{"deduction":false,"feeCoinCode":"USDT","totalDeductionFee":0,"totalFee":-0.007000500000}}`,
|
||||||
|
OrderSource: "normal",
|
||||||
|
CTime: types.NewMillisecondTimestampFromInt(1699020564659),
|
||||||
|
UTime: types.NewMillisecondTimestampFromInt(1699020564688),
|
||||||
|
FeeDetail: FeeDetail{
|
||||||
|
NewFees: struct {
|
||||||
|
DeductedByCoupon fixedpoint.Value `json:"c"`
|
||||||
|
DeductedInBGB fixedpoint.Value `json:"d"`
|
||||||
|
DeductedFromCurrency fixedpoint.Value `json:"r"`
|
||||||
|
ToBePaid fixedpoint.Value `json:"t"`
|
||||||
|
Deduction bool `json:"deduction"`
|
||||||
|
TotalDeductionFee fixedpoint.Value `json:"totalDeductionFee"`
|
||||||
|
}{DeductedByCoupon: fixedpoint.NewFromFloat(0),
|
||||||
|
DeductedInBGB: fixedpoint.NewFromFloat(0),
|
||||||
|
DeductedFromCurrency: fixedpoint.NewFromFloat(-0.0070005),
|
||||||
|
ToBePaid: fixedpoint.NewFromFloat(-0.0070005),
|
||||||
|
Deduction: false,
|
||||||
|
TotalDeductionFee: fixedpoint.Zero,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, od)
|
||||||
|
})
|
||||||
|
}
|
70
pkg/exchange/bitget/bitgetapi/v2/get_trade_fills.go
Normal file
70
pkg/exchange/bitget/bitgetapi/v2/get_trade_fills.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/requestgen"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
|
||||||
|
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
|
||||||
|
|
||||||
|
type TradeScope string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TradeMaker TradeScope = "maker"
|
||||||
|
TradeTaker TradeScope = "taker"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscountStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DiscountYes DiscountStatus = "yes"
|
||||||
|
DiscountNo DiscountStatus = "no"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TradeFee struct {
|
||||||
|
// Discount or not
|
||||||
|
Deduction DiscountStatus `json:"deduction"`
|
||||||
|
// Transaction fee coin
|
||||||
|
FeeCoin string `json:"feeCoin"`
|
||||||
|
// Total transaction fee discount
|
||||||
|
TotalDeductionFee fixedpoint.Value `json:"totalDeductionFee"`
|
||||||
|
// Total transaction fee
|
||||||
|
TotalFee fixedpoint.Value `json:"totalFee"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Trade struct {
|
||||||
|
UserId types.StrInt64 `json:"userId"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
OrderId types.StrInt64 `json:"orderId"`
|
||||||
|
TradeId types.StrInt64 `json:"tradeId"`
|
||||||
|
OrderType OrderType `json:"orderType"`
|
||||||
|
Side SideType `json:"side"`
|
||||||
|
PriceAvg fixedpoint.Value `json:"priceAvg"`
|
||||||
|
Size fixedpoint.Value `json:"size"`
|
||||||
|
Amount fixedpoint.Value `json:"amount"`
|
||||||
|
FeeDetail TradeFee `json:"feeDetail"`
|
||||||
|
TradeScope TradeScope `json:"tradeScope"`
|
||||||
|
CTime types.MillisecondTimestamp `json:"cTime"`
|
||||||
|
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate GetRequest -url "/api/v2/spot/trade/fills" -type GetTradeFillsRequest -responseDataType []Trade
|
||||||
|
type GetTradeFillsRequest struct {
|
||||||
|
client requestgen.AuthenticatedAPIClient
|
||||||
|
|
||||||
|
symbol string `param:"symbol,query"`
|
||||||
|
// Limit number default 100 max 100
|
||||||
|
limit *string `param:"limit,query"`
|
||||||
|
// idLessThan requests the content on the page before this ID (older data), the value input should be the orderId of the corresponding interface.
|
||||||
|
idLessThan *string `param:"idLessThan,query"`
|
||||||
|
startTime *int64 `param:"startTime,query"`
|
||||||
|
endTime *int64 `param:"endTime,query"`
|
||||||
|
orderId *string `param:"orderId,query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Client) NewGetTradeFillsRequest() *GetTradeFillsRequest {
|
||||||
|
return &GetTradeFillsRequest{client: s.Client}
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/fills -type GetTradeFillsRequest -responseDataType []Trade"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) Symbol(symbol string) *GetTradeFillsRequest {
|
||||||
|
s.symbol = symbol
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) Limit(limit string) *GetTradeFillsRequest {
|
||||||
|
s.limit = &limit
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) IdLessThan(idLessThan string) *GetTradeFillsRequest {
|
||||||
|
s.idLessThan = &idLessThan
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) StartTime(startTime int64) *GetTradeFillsRequest {
|
||||||
|
s.startTime = &startTime
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) EndTime(endTime int64) *GetTradeFillsRequest {
|
||||||
|
s.endTime = &endTime
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) OrderId(orderId string) *GetTradeFillsRequest {
|
||||||
|
s.orderId = &orderId
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||||
|
func (s *GetTradeFillsRequest) GetQueryParameters() (url.Values, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
// check symbol field -> json key symbol
|
||||||
|
symbol := s.symbol
|
||||||
|
|
||||||
|
// assign parameter of symbol
|
||||||
|
params["symbol"] = symbol
|
||||||
|
// check limit field -> json key limit
|
||||||
|
if s.limit != nil {
|
||||||
|
limit := *s.limit
|
||||||
|
|
||||||
|
// assign parameter of limit
|
||||||
|
params["limit"] = limit
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check idLessThan field -> json key idLessThan
|
||||||
|
if s.idLessThan != nil {
|
||||||
|
idLessThan := *s.idLessThan
|
||||||
|
|
||||||
|
// assign parameter of idLessThan
|
||||||
|
params["idLessThan"] = idLessThan
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check startTime field -> json key startTime
|
||||||
|
if s.startTime != nil {
|
||||||
|
startTime := *s.startTime
|
||||||
|
|
||||||
|
// assign parameter of startTime
|
||||||
|
params["startTime"] = startTime
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check endTime field -> json key endTime
|
||||||
|
if s.endTime != nil {
|
||||||
|
endTime := *s.endTime
|
||||||
|
|
||||||
|
// assign parameter of endTime
|
||||||
|
params["endTime"] = endTime
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check orderId field -> json key orderId
|
||||||
|
if s.orderId != nil {
|
||||||
|
orderId := *s.orderId
|
||||||
|
|
||||||
|
// assign parameter of orderId
|
||||||
|
params["orderId"] = orderId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
for _k, _v := range params {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParameters builds and checks the parameters and return the result in a map object
|
||||||
|
func (s *GetTradeFillsRequest) GetParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
|
||||||
|
func (s *GetTradeFillsRequest) GetParametersQuery() (url.Values, error) {
|
||||||
|
query := url.Values{}
|
||||||
|
|
||||||
|
params, err := s.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
if s.isVarSlice(_v) {
|
||||||
|
s.iterateSlice(_v, func(it interface{}) {
|
||||||
|
query.Add(_k+"[]", fmt.Sprintf("%v", it))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersJSON converts the parameters from GetParameters into the JSON format
|
||||||
|
func (s *GetTradeFillsRequest) GetParametersJSON() ([]byte, error) {
|
||||||
|
params, err := s.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
|
||||||
|
func (s *GetTradeFillsRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) applySlugsToUrl(url string, slugs map[string]string) string {
|
||||||
|
for _k, _v := range slugs {
|
||||||
|
needleRE := regexp.MustCompile(":" + _k + "\\b")
|
||||||
|
url = needleRE.ReplaceAllString(url, _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
|
||||||
|
sliceValue := reflect.ValueOf(slice)
|
||||||
|
for _i := 0; _i < sliceValue.Len(); _i++ {
|
||||||
|
it := sliceValue.Index(_i).Interface()
|
||||||
|
_f(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) isVarSlice(_v interface{}) bool {
|
||||||
|
rt := reflect.TypeOf(_v)
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
|
slugs := map[string]string{}
|
||||||
|
params, err := s.GetSlugParameters()
|
||||||
|
if err != nil {
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
slugs[_k] = fmt.Sprintf("%v", _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GetTradeFillsRequest) Do(ctx context.Context) ([]Trade, error) {
|
||||||
|
|
||||||
|
// no body params
|
||||||
|
var params interface{}
|
||||||
|
query, err := s.GetQueryParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := "/api/v2/spot/trade/fills"
|
||||||
|
|
||||||
|
req, err := s.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s.client.SendRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse bitgetapi.APIResponse
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data []Trade
|
||||||
|
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
|
||||||
|
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/requestgen"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UnfilledOrder struct {
|
||||||
|
UserId types.StrInt64 `json:"userId"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
// OrderId are always numeric. It's confirmed with official customer service. https://t.me/bitgetOpenapi/24172
|
||||||
|
OrderId types.StrInt64 `json:"orderId"`
|
||||||
|
ClientOrderId string `json:"clientOid"`
|
||||||
|
PriceAvg fixedpoint.Value `json:"priceAvg"`
|
||||||
|
// Size is base coin when orderType=limit; quote coin when orderType=market
|
||||||
|
Size fixedpoint.Value `json:"size"`
|
||||||
|
OrderType OrderType `json:"orderType"`
|
||||||
|
Side SideType `json:"side"`
|
||||||
|
Status OrderStatus `json:"status"`
|
||||||
|
BasePrice fixedpoint.Value `json:"basePrice"`
|
||||||
|
BaseVolume fixedpoint.Value `json:"baseVolume"`
|
||||||
|
QuoteVolume fixedpoint.Value `json:"quoteVolume"`
|
||||||
|
EnterPointSource string `json:"enterPointSource"`
|
||||||
|
OrderSource string `json:"orderSource"`
|
||||||
|
CTime types.MillisecondTimestamp `json:"cTime"`
|
||||||
|
UTime types.MillisecondTimestamp `json:"uTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate GetRequest -url "/api/v2/spot/trade/unfilled-orders" -type GetUnfilledOrdersRequest -responseDataType []UnfilledOrder
|
||||||
|
type GetUnfilledOrdersRequest struct {
|
||||||
|
client requestgen.AuthenticatedAPIClient
|
||||||
|
|
||||||
|
symbol *string `param:"symbol,query"`
|
||||||
|
// Limit number default 100 max 100
|
||||||
|
limit *string `param:"limit,query"`
|
||||||
|
// idLessThan requests the content on the page before this ID (older data), the value input should be the orderId of the corresponding interface.
|
||||||
|
idLessThan *string `param:"idLessThan,query"`
|
||||||
|
startTime *int64 `param:"startTime,query"`
|
||||||
|
endTime *int64 `param:"endTime,query"`
|
||||||
|
orderId *string `param:"orderId,query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) NewGetUnfilledOrdersRequest() *GetUnfilledOrdersRequest {
|
||||||
|
return &GetUnfilledOrdersRequest{client: c.Client}
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/unfilled-orders -type GetUnfilledOrdersRequest -responseDataType []UnfilledOrder"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) Symbol(symbol string) *GetUnfilledOrdersRequest {
|
||||||
|
g.symbol = &symbol
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) Limit(limit string) *GetUnfilledOrdersRequest {
|
||||||
|
g.limit = &limit
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) IdLessThan(idLessThan string) *GetUnfilledOrdersRequest {
|
||||||
|
g.idLessThan = &idLessThan
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) StartTime(startTime int64) *GetUnfilledOrdersRequest {
|
||||||
|
g.startTime = &startTime
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) EndTime(endTime int64) *GetUnfilledOrdersRequest {
|
||||||
|
g.endTime = &endTime
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) OrderId(orderId string) *GetUnfilledOrdersRequest {
|
||||||
|
g.orderId = &orderId
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||||
|
func (g *GetUnfilledOrdersRequest) GetQueryParameters() (url.Values, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
// check symbol field -> json key symbol
|
||||||
|
if g.symbol != nil {
|
||||||
|
symbol := *g.symbol
|
||||||
|
|
||||||
|
// assign parameter of symbol
|
||||||
|
params["symbol"] = symbol
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check limit field -> json key limit
|
||||||
|
if g.limit != nil {
|
||||||
|
limit := *g.limit
|
||||||
|
|
||||||
|
// assign parameter of limit
|
||||||
|
params["limit"] = limit
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check idLessThan field -> json key idLessThan
|
||||||
|
if g.idLessThan != nil {
|
||||||
|
idLessThan := *g.idLessThan
|
||||||
|
|
||||||
|
// assign parameter of idLessThan
|
||||||
|
params["idLessThan"] = idLessThan
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check startTime field -> json key startTime
|
||||||
|
if g.startTime != nil {
|
||||||
|
startTime := *g.startTime
|
||||||
|
|
||||||
|
// assign parameter of startTime
|
||||||
|
params["startTime"] = startTime
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check endTime field -> json key endTime
|
||||||
|
if g.endTime != nil {
|
||||||
|
endTime := *g.endTime
|
||||||
|
|
||||||
|
// assign parameter of endTime
|
||||||
|
params["endTime"] = endTime
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check orderId field -> json key orderId
|
||||||
|
if g.orderId != nil {
|
||||||
|
orderId := *g.orderId
|
||||||
|
|
||||||
|
// assign parameter of orderId
|
||||||
|
params["orderId"] = orderId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
for _k, _v := range params {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParameters builds and checks the parameters and return the result in a map object
|
||||||
|
func (g *GetUnfilledOrdersRequest) GetParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
|
||||||
|
func (g *GetUnfilledOrdersRequest) GetParametersQuery() (url.Values, error) {
|
||||||
|
query := url.Values{}
|
||||||
|
|
||||||
|
params, err := g.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
if g.isVarSlice(_v) {
|
||||||
|
g.iterateSlice(_v, func(it interface{}) {
|
||||||
|
query.Add(_k+"[]", fmt.Sprintf("%v", it))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersJSON converts the parameters from GetParameters into the JSON format
|
||||||
|
func (g *GetUnfilledOrdersRequest) GetParametersJSON() ([]byte, error) {
|
||||||
|
params, err := g.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
|
||||||
|
func (g *GetUnfilledOrdersRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string {
|
||||||
|
for _k, _v := range slugs {
|
||||||
|
needleRE := regexp.MustCompile(":" + _k + "\\b")
|
||||||
|
url = needleRE.ReplaceAllString(url, _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
|
||||||
|
sliceValue := reflect.ValueOf(slice)
|
||||||
|
for _i := 0; _i < sliceValue.Len(); _i++ {
|
||||||
|
it := sliceValue.Index(_i).Interface()
|
||||||
|
_f(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) isVarSlice(_v interface{}) bool {
|
||||||
|
rt := reflect.TypeOf(_v)
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
|
slugs := map[string]string{}
|
||||||
|
params, err := g.GetSlugParameters()
|
||||||
|
if err != nil {
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
slugs[_k] = fmt.Sprintf("%v", _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GetUnfilledOrdersRequest) Do(ctx context.Context) ([]UnfilledOrder, error) {
|
||||||
|
|
||||||
|
// no body params
|
||||||
|
var params interface{}
|
||||||
|
query, err := g.GetQueryParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiURL := "/api/v2/spot/trade/unfilled-orders"
|
||||||
|
|
||||||
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := g.client.SendRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse bitgetapi.APIResponse
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data []UnfilledOrder
|
||||||
|
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
29
pkg/exchange/bitget/bitgetapi/v2/place_order_request.go
Normal file
29
pkg/exchange/bitget/bitgetapi/v2/place_order_request.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
|
||||||
|
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/requestgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlaceOrderResponse struct {
|
||||||
|
OrderId string `json:"orderId"`
|
||||||
|
ClientOrderId string `json:"clientOrderId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate PostRequest -url "/api/v2/spot/trade/place-order" -type PlaceOrderRequest -responseDataType .PlaceOrderResponse
|
||||||
|
type PlaceOrderRequest struct {
|
||||||
|
client requestgen.AuthenticatedAPIClient
|
||||||
|
symbol string `param:"symbol"`
|
||||||
|
orderType OrderType `param:"orderType"`
|
||||||
|
side SideType `param:"side"`
|
||||||
|
force OrderForce `param:"force"`
|
||||||
|
price *string `param:"price"`
|
||||||
|
size string `param:"size"`
|
||||||
|
clientOrderId *string `param:"clientOid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) NewPlaceOrderRequest() *PlaceOrderRequest {
|
||||||
|
return &PlaceOrderRequest{client: c.Client}
|
||||||
|
}
|
|
@ -0,0 +1,251 @@
|
||||||
|
// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/place-order -type PlaceOrderRequest -responseDataType .PlaceOrderResponse"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) Symbol(symbol string) *PlaceOrderRequest {
|
||||||
|
p.symbol = symbol
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest {
|
||||||
|
p.orderType = orderType
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest {
|
||||||
|
p.side = side
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) Force(force OrderForce) *PlaceOrderRequest {
|
||||||
|
p.force = force
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) Price(price string) *PlaceOrderRequest {
|
||||||
|
p.price = &price
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) Size(size string) *PlaceOrderRequest {
|
||||||
|
p.size = size
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) ClientOrderId(clientOrderId string) *PlaceOrderRequest {
|
||||||
|
p.clientOrderId = &clientOrderId
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueryParameters builds and checks the query parameters and returns url.Values
|
||||||
|
func (p *PlaceOrderRequest) GetQueryParameters() (url.Values, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
query := url.Values{}
|
||||||
|
for _k, _v := range params {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParameters builds and checks the parameters and return the result in a map object
|
||||||
|
func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
// check symbol field -> json key symbol
|
||||||
|
symbol := p.symbol
|
||||||
|
|
||||||
|
// assign parameter of symbol
|
||||||
|
params["symbol"] = symbol
|
||||||
|
// check orderType field -> json key orderType
|
||||||
|
orderType := p.orderType
|
||||||
|
|
||||||
|
// TEMPLATE check-valid-values
|
||||||
|
switch orderType {
|
||||||
|
case OrderTypeLimit, OrderTypeMarket:
|
||||||
|
params["orderType"] = orderType
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("orderType value %v is invalid", orderType)
|
||||||
|
|
||||||
|
}
|
||||||
|
// END TEMPLATE check-valid-values
|
||||||
|
|
||||||
|
// assign parameter of orderType
|
||||||
|
params["orderType"] = orderType
|
||||||
|
// check side field -> json key side
|
||||||
|
side := p.side
|
||||||
|
|
||||||
|
// TEMPLATE check-valid-values
|
||||||
|
switch side {
|
||||||
|
case SideTypeBuy, SideTypeSell:
|
||||||
|
params["side"] = side
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("side value %v is invalid", side)
|
||||||
|
|
||||||
|
}
|
||||||
|
// END TEMPLATE check-valid-values
|
||||||
|
|
||||||
|
// assign parameter of side
|
||||||
|
params["side"] = side
|
||||||
|
// check force field -> json key force
|
||||||
|
force := p.force
|
||||||
|
|
||||||
|
// TEMPLATE check-valid-values
|
||||||
|
switch force {
|
||||||
|
case OrderForceGTC, OrderForcePostOnly, OrderForceFOK, OrderForceIOC:
|
||||||
|
params["force"] = force
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("force value %v is invalid", force)
|
||||||
|
|
||||||
|
}
|
||||||
|
// END TEMPLATE check-valid-values
|
||||||
|
|
||||||
|
// assign parameter of force
|
||||||
|
params["force"] = force
|
||||||
|
// check price field -> json key price
|
||||||
|
if p.price != nil {
|
||||||
|
price := *p.price
|
||||||
|
|
||||||
|
// assign parameter of price
|
||||||
|
params["price"] = price
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
// check size field -> json key size
|
||||||
|
size := p.size
|
||||||
|
|
||||||
|
// assign parameter of size
|
||||||
|
params["size"] = size
|
||||||
|
// check clientOrderId field -> json key clientOid
|
||||||
|
if p.clientOrderId != nil {
|
||||||
|
clientOrderId := *p.clientOrderId
|
||||||
|
|
||||||
|
// assign parameter of clientOrderId
|
||||||
|
params["clientOid"] = clientOrderId
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
|
||||||
|
func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) {
|
||||||
|
query := url.Values{}
|
||||||
|
|
||||||
|
params, err := p.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return query, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
if p.isVarSlice(_v) {
|
||||||
|
p.iterateSlice(_v, func(it interface{}) {
|
||||||
|
query.Add(_k+"[]", fmt.Sprintf("%v", it))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
query.Add(_k, fmt.Sprintf("%v", _v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParametersJSON converts the parameters from GetParameters into the JSON format
|
||||||
|
func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||||
|
params, err := p.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
|
||||||
|
func (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) {
|
||||||
|
var params = map[string]interface{}{}
|
||||||
|
|
||||||
|
return params, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string {
|
||||||
|
for _k, _v := range slugs {
|
||||||
|
needleRE := regexp.MustCompile(":" + _k + "\\b")
|
||||||
|
url = needleRE.ReplaceAllString(url, _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) {
|
||||||
|
sliceValue := reflect.ValueOf(slice)
|
||||||
|
for _i := 0; _i < sliceValue.Len(); _i++ {
|
||||||
|
it := sliceValue.Index(_i).Interface()
|
||||||
|
_f(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) isVarSlice(_v interface{}) bool {
|
||||||
|
rt := reflect.TypeOf(_v)
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
|
slugs := map[string]string{}
|
||||||
|
params, err := p.GetSlugParameters()
|
||||||
|
if err != nil {
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _k, _v := range params {
|
||||||
|
slugs[_k] = fmt.Sprintf("%v", _v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slugs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error) {
|
||||||
|
|
||||||
|
params, err := p.GetParameters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
query := url.Values{}
|
||||||
|
|
||||||
|
apiURL := "/api/v2/spot/trade/place-order"
|
||||||
|
|
||||||
|
req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := p.client.SendRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiResponse bitgetapi.APIResponse
|
||||||
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data PlaceOrderResponse
|
||||||
|
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
42
pkg/exchange/bitget/bitgetapi/v2/types.go
Normal file
42
pkg/exchange/bitget/bitgetapi/v2/types.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package bitgetapi
|
||||||
|
|
||||||
|
type SideType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SideTypeBuy SideType = "buy"
|
||||||
|
SideTypeSell SideType = "sell"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderTypeLimit OrderType = "limit"
|
||||||
|
OrderTypeMarket OrderType = "market"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderForce string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderForceGTC OrderForce = "gtc"
|
||||||
|
OrderForcePostOnly OrderForce = "post_only"
|
||||||
|
OrderForceFOK OrderForce = "fok"
|
||||||
|
OrderForceIOC OrderForce = "ioc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderStatusInit OrderStatus = "init"
|
||||||
|
OrderStatusNew OrderStatus = "new"
|
||||||
|
OrderStatusLive OrderStatus = "live"
|
||||||
|
OrderStatusPartialFilled OrderStatus = "partially_filled"
|
||||||
|
OrderStatusFilled OrderStatus = "filled"
|
||||||
|
OrderStatusCancelled OrderStatus = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (o OrderStatus) IsWorking() bool {
|
||||||
|
return o == OrderStatusInit ||
|
||||||
|
o == OrderStatusNew ||
|
||||||
|
o == OrderStatusLive ||
|
||||||
|
o == OrderStatusPartialFilled
|
||||||
|
}
|
|
@ -1,10 +1,14 @@
|
||||||
package bitget
|
package bitget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
@ -59,3 +63,269 @@ func toGlobalTicker(ticker bitgetapi.Ticker) types.Ticker {
|
||||||
Sell: ticker.SellOne,
|
Sell: ticker.SellOne,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toGlobalSideType(side v2.SideType) (types.SideType, error) {
|
||||||
|
switch side {
|
||||||
|
case v2.SideTypeBuy:
|
||||||
|
return types.SideTypeBuy, nil
|
||||||
|
|
||||||
|
case v2.SideTypeSell:
|
||||||
|
return types.SideTypeSell, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return types.SideType(side), fmt.Errorf("unexpected side: %s", side)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalOrderType(s v2.OrderType) (types.OrderType, error) {
|
||||||
|
switch s {
|
||||||
|
case v2.OrderTypeMarket:
|
||||||
|
return types.OrderTypeMarket, nil
|
||||||
|
|
||||||
|
case v2.OrderTypeLimit:
|
||||||
|
return types.OrderTypeLimit, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return types.OrderType(s), fmt.Errorf("unexpected order type: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalOrderStatus(status v2.OrderStatus) (types.OrderStatus, error) {
|
||||||
|
switch status {
|
||||||
|
case v2.OrderStatusInit, v2.OrderStatusNew, v2.OrderStatusLive:
|
||||||
|
return types.OrderStatusNew, nil
|
||||||
|
|
||||||
|
case v2.OrderStatusPartialFilled:
|
||||||
|
return types.OrderStatusPartiallyFilled, nil
|
||||||
|
|
||||||
|
case v2.OrderStatusFilled:
|
||||||
|
return types.OrderStatusFilled, nil
|
||||||
|
|
||||||
|
case v2.OrderStatusCancelled:
|
||||||
|
return types.OrderStatusCanceled, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return types.OrderStatus(status), fmt.Errorf("unexpected order status: %s", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMaker(s v2.TradeScope) (bool, error) {
|
||||||
|
switch s {
|
||||||
|
case v2.TradeMaker:
|
||||||
|
return true, nil
|
||||||
|
|
||||||
|
case v2.TradeTaker:
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("unexpected trade scope: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFeeDiscount(s v2.DiscountStatus) (bool, error) {
|
||||||
|
switch s {
|
||||||
|
case v2.DiscountYes:
|
||||||
|
return true, nil
|
||||||
|
|
||||||
|
case v2.DiscountNo:
|
||||||
|
return false, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("unexpected discount status: %s", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalTrade(trade v2.Trade) (*types.Trade, error) {
|
||||||
|
side, err := toGlobalSideType(trade.Side)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isMaker, err := isMaker(trade.TradeScope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isDiscount, err := isFeeDiscount(trade.FeeDetail.Deduction)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Trade{
|
||||||
|
ID: uint64(trade.TradeId),
|
||||||
|
OrderID: uint64(trade.OrderId),
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
Price: trade.PriceAvg,
|
||||||
|
Quantity: trade.Size,
|
||||||
|
QuoteQuantity: trade.Amount,
|
||||||
|
Symbol: trade.Symbol,
|
||||||
|
Side: side,
|
||||||
|
IsBuyer: side == types.SideTypeBuy,
|
||||||
|
IsMaker: isMaker,
|
||||||
|
Time: types.Time(trade.CTime),
|
||||||
|
Fee: trade.FeeDetail.TotalFee.Abs(),
|
||||||
|
FeeCurrency: trade.FeeDetail.FeeCoin,
|
||||||
|
FeeDiscounted: isDiscount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unfilledOrderToGlobalOrder convert the local order to global.
|
||||||
|
//
|
||||||
|
// Note that the quantity unit, according official document: Base coin when orderType=limit; Quote coin when orderType=market
|
||||||
|
// https://bitgetlimited.github.io/apidoc/zh/spot/#19671a1099
|
||||||
|
func unfilledOrderToGlobalOrder(order v2.UnfilledOrder) (*types.Order, error) {
|
||||||
|
side, err := toGlobalSideType(order.Side)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderType, err := toGlobalOrderType(order.OrderType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := toGlobalOrderStatus(order.Status)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qty := order.Size
|
||||||
|
price := order.PriceAvg
|
||||||
|
|
||||||
|
// The market order will be executed immediately, so this check is used to handle corner cases.
|
||||||
|
if orderType == types.OrderTypeMarket {
|
||||||
|
qty = order.BaseVolume
|
||||||
|
log.Warnf("!!! The price(%f) and quantity(%f) are not verified for market orders, because we only receive limit orders in the test environment !!!", price.Float64(), qty.Float64())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
ClientOrderID: order.ClientOrderId,
|
||||||
|
Symbol: order.Symbol,
|
||||||
|
Side: side,
|
||||||
|
Type: orderType,
|
||||||
|
Quantity: qty,
|
||||||
|
Price: price,
|
||||||
|
// Bitget does not include the "time-in-force" field in its API response for spot trading, so we set GTC.
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
OrderID: uint64(order.OrderId),
|
||||||
|
UUID: strconv.FormatInt(int64(order.OrderId), 10),
|
||||||
|
Status: status,
|
||||||
|
ExecutedQuantity: order.BaseVolume,
|
||||||
|
IsWorking: order.Status.IsWorking(),
|
||||||
|
CreationTime: types.Time(order.CTime.Time()),
|
||||||
|
UpdateTime: types.Time(order.UTime.Time()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGlobalOrder(order v2.OrderDetail) (*types.Order, error) {
|
||||||
|
side, err := toGlobalSideType(order.Side)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orderType, err := toGlobalOrderType(order.OrderType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := toGlobalOrderStatus(order.Status)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qty := order.Size
|
||||||
|
price := order.Price
|
||||||
|
|
||||||
|
if orderType == types.OrderTypeMarket {
|
||||||
|
price = order.PriceAvg
|
||||||
|
if side == types.SideTypeBuy {
|
||||||
|
qty, err = processMarketBuyQuantity(order.BaseVolume, order.QuoteVolume, order.PriceAvg, order.Size, order.Status)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
ClientOrderID: order.ClientOrderId,
|
||||||
|
Symbol: order.Symbol,
|
||||||
|
Side: side,
|
||||||
|
Type: orderType,
|
||||||
|
Quantity: qty,
|
||||||
|
Price: price,
|
||||||
|
// Bitget does not include the "time-in-force" field in its API response for spot trading, so we set GTC.
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
OrderID: uint64(order.OrderId),
|
||||||
|
UUID: strconv.FormatInt(int64(order.OrderId), 10),
|
||||||
|
Status: status,
|
||||||
|
ExecutedQuantity: order.BaseVolume,
|
||||||
|
IsWorking: order.Status.IsWorking(),
|
||||||
|
CreationTime: types.Time(order.CTime.Time()),
|
||||||
|
UpdateTime: types.Time(order.UTime.Time()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processMarketBuyQuantity returns the estimated base quantity or real. The order size will be 'quote quantity' when side is buy and
|
||||||
|
// type is market, so we need to convert that. This is because the unit of types.Order.Quantity is base coin.
|
||||||
|
//
|
||||||
|
// If the order status is PartialFilled, return estimated base coin quantity.
|
||||||
|
// If the order status is Filled, return the filled base quantity instead of the buy quantity, because a market order on the buy side
|
||||||
|
// cannot execute all.
|
||||||
|
// Otherwise, return zero.
|
||||||
|
func processMarketBuyQuantity(filledQty, filledPrice, priceAvg, buyQty fixedpoint.Value, orderStatus v2.OrderStatus) (fixedpoint.Value, error) {
|
||||||
|
switch orderStatus {
|
||||||
|
case v2.OrderStatusInit, v2.OrderStatusNew, v2.OrderStatusLive, v2.OrderStatusCancelled:
|
||||||
|
return fixedpoint.Zero, nil
|
||||||
|
|
||||||
|
case v2.OrderStatusPartialFilled:
|
||||||
|
// sanity check for avoid divide 0
|
||||||
|
if priceAvg.IsZero() {
|
||||||
|
return fixedpoint.Zero, errors.New("priceAvg for a partialFilled should not be zero")
|
||||||
|
}
|
||||||
|
// calculate the remaining quote coin quantity.
|
||||||
|
remainPrice := buyQty.Sub(filledPrice)
|
||||||
|
// calculate the remaining base coin quantity.
|
||||||
|
remainBaseCoinQty := remainPrice.Div(priceAvg)
|
||||||
|
// Estimated quantity that may be purchased.
|
||||||
|
return filledQty.Add(remainBaseCoinQty), nil
|
||||||
|
|
||||||
|
case v2.OrderStatusFilled:
|
||||||
|
// Market buy orders may not purchase the entire quantity, hence the use of filledQty here.
|
||||||
|
return filledQty, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fixedpoint.Zero, fmt.Errorf("failed to execute market buy quantity due to unexpected order status %s ", orderStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLocalOrderType(orderType types.OrderType) (v2.OrderType, error) {
|
||||||
|
switch orderType {
|
||||||
|
case types.OrderTypeLimit:
|
||||||
|
return v2.OrderTypeLimit, nil
|
||||||
|
|
||||||
|
case types.OrderTypeMarket:
|
||||||
|
return v2.OrderTypeMarket, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("order type %s not supported", orderType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toLocalSide(side types.SideType) (v2.SideType, error) {
|
||||||
|
switch side {
|
||||||
|
case types.SideTypeSell:
|
||||||
|
return v2.SideTypeSell, nil
|
||||||
|
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
return v2.SideTypeBuy, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("side type %s not supported", side)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package bitget
|
package bitget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
@ -143,3 +145,437 @@ func Test_toGlobalTicker(t *testing.T) {
|
||||||
Sell: fixedpoint.NewFromFloat(24014.06),
|
Sell: fixedpoint.NewFromFloat(24014.06),
|
||||||
}, toGlobalTicker(ticker))
|
}, toGlobalTicker(ticker))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_toGlobalSideType(t *testing.T) {
|
||||||
|
side, err := toGlobalSideType(v2.SideTypeBuy)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.SideTypeBuy, side)
|
||||||
|
|
||||||
|
side, err = toGlobalSideType(v2.SideTypeSell)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.SideTypeSell, side)
|
||||||
|
|
||||||
|
_, err = toGlobalSideType("xxx")
|
||||||
|
assert.ErrorContains(t, err, "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_toGlobalOrderType(t *testing.T) {
|
||||||
|
orderType, err := toGlobalOrderType(v2.OrderTypeMarket)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderTypeMarket, orderType)
|
||||||
|
|
||||||
|
orderType, err = toGlobalOrderType(v2.OrderTypeLimit)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderTypeLimit, orderType)
|
||||||
|
|
||||||
|
_, err = toGlobalOrderType("xxx")
|
||||||
|
assert.ErrorContains(t, err, "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_toGlobalOrderStatus(t *testing.T) {
|
||||||
|
status, err := toGlobalOrderStatus(v2.OrderStatusInit)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusNew, status)
|
||||||
|
|
||||||
|
status, err = toGlobalOrderStatus(v2.OrderStatusNew)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusNew, status)
|
||||||
|
|
||||||
|
status, err = toGlobalOrderStatus(v2.OrderStatusLive)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusNew, status)
|
||||||
|
|
||||||
|
status, err = toGlobalOrderStatus(v2.OrderStatusFilled)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusFilled, status)
|
||||||
|
|
||||||
|
status, err = toGlobalOrderStatus(v2.OrderStatusPartialFilled)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusPartiallyFilled, status)
|
||||||
|
|
||||||
|
status, err = toGlobalOrderStatus(v2.OrderStatusCancelled)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, types.OrderStatusCanceled, status)
|
||||||
|
|
||||||
|
_, err = toGlobalOrderStatus("xxx")
|
||||||
|
assert.ErrorContains(t, err, "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_unfilledOrderToGlobalOrder(t *testing.T) {
|
||||||
|
var (
|
||||||
|
assert = assert.New(t)
|
||||||
|
orderId = 1105087175647989764
|
||||||
|
unfilledOrder = v2.UnfilledOrder{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
OrderId: types.StrInt64(orderId),
|
||||||
|
ClientOrderId: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||||
|
PriceAvg: fixedpoint.NewFromFloat(1.2),
|
||||||
|
Size: fixedpoint.NewFromFloat(5),
|
||||||
|
OrderType: v2.OrderTypeLimit,
|
||||||
|
Side: v2.SideTypeBuy,
|
||||||
|
Status: v2.OrderStatusLive,
|
||||||
|
BasePrice: fixedpoint.NewFromFloat(0),
|
||||||
|
BaseVolume: fixedpoint.NewFromFloat(0),
|
||||||
|
QuoteVolume: fixedpoint.NewFromFloat(0),
|
||||||
|
EnterPointSource: "API",
|
||||||
|
OrderSource: "normal",
|
||||||
|
CTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||||
|
UTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("succeeds", func(t *testing.T) {
|
||||||
|
order, err := unfilledOrderToGlobalOrder(unfilledOrder)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(&types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(5),
|
||||||
|
Price: fixedpoint.NewFromFloat(1.2),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
OrderID: uint64(orderId),
|
||||||
|
UUID: strconv.FormatInt(int64(orderId), 10),
|
||||||
|
Status: types.OrderStatusNew,
|
||||||
|
ExecutedQuantity: fixedpoint.NewFromFloat(0),
|
||||||
|
IsWorking: true,
|
||||||
|
CreationTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||||
|
UpdateTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||||
|
}, order)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to convert side", func(t *testing.T) {
|
||||||
|
newOrder := unfilledOrder
|
||||||
|
newOrder.Side = "xxx"
|
||||||
|
|
||||||
|
_, err := unfilledOrderToGlobalOrder(newOrder)
|
||||||
|
assert.ErrorContains(err, "xxx")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to convert oder type", func(t *testing.T) {
|
||||||
|
newOrder := unfilledOrder
|
||||||
|
newOrder.OrderType = "xxx"
|
||||||
|
|
||||||
|
_, err := unfilledOrderToGlobalOrder(newOrder)
|
||||||
|
assert.ErrorContains(err, "xxx")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to convert oder status", func(t *testing.T) {
|
||||||
|
newOrder := unfilledOrder
|
||||||
|
newOrder.Status = "xxx"
|
||||||
|
|
||||||
|
_, err := unfilledOrderToGlobalOrder(newOrder)
|
||||||
|
assert.ErrorContains(err, "xxx")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_toGlobalOrder(t *testing.T) {
|
||||||
|
var (
|
||||||
|
assert = assert.New(t)
|
||||||
|
orderId = 1105087175647989764
|
||||||
|
unfilledOrder = v2.OrderDetail{
|
||||||
|
UserId: 123456,
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
OrderId: types.StrInt64(orderId),
|
||||||
|
ClientOrderId: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||||
|
Price: fixedpoint.NewFromFloat(1.2),
|
||||||
|
Size: fixedpoint.NewFromFloat(5),
|
||||||
|
OrderType: v2.OrderTypeLimit,
|
||||||
|
Side: v2.SideTypeBuy,
|
||||||
|
Status: v2.OrderStatusFilled,
|
||||||
|
PriceAvg: fixedpoint.NewFromFloat(1.4),
|
||||||
|
BaseVolume: fixedpoint.NewFromFloat(5),
|
||||||
|
QuoteVolume: fixedpoint.NewFromFloat(7.0005),
|
||||||
|
EnterPointSource: "API",
|
||||||
|
FeeDetailRaw: `{\"newFees\":{\"c\":0,\"d\":0,\"deduction\":false,\"r\":-0.0070005,\"t\":-0.0070005,\"totalDeductionFee\":0},\"USDT\":{\"deduction\":false,\"feeCoinCode\":\"USDT\",\"totalDeductionFee\":0,\"totalFee\":-0.007000500000}}`,
|
||||||
|
OrderSource: "normal",
|
||||||
|
CTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||||
|
UTime: types.NewMillisecondTimestampFromInt(1660704288118),
|
||||||
|
}
|
||||||
|
|
||||||
|
expOrder = &types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(5),
|
||||||
|
Price: fixedpoint.NewFromFloat(1.2),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
OrderID: uint64(orderId),
|
||||||
|
UUID: strconv.FormatInt(int64(orderId), 10),
|
||||||
|
Status: types.OrderStatusFilled,
|
||||||
|
ExecutedQuantity: fixedpoint.NewFromFloat(5),
|
||||||
|
IsWorking: false,
|
||||||
|
CreationTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||||
|
UpdateTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("succeeds with limit buy", func(t *testing.T) {
|
||||||
|
order, err := toGlobalOrder(unfilledOrder)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(expOrder, order)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("succeeds with limit sell", func(t *testing.T) {
|
||||||
|
newUnfilledOrder := unfilledOrder
|
||||||
|
newUnfilledOrder.Side = v2.SideTypeSell
|
||||||
|
|
||||||
|
newExpOrder := *expOrder
|
||||||
|
newExpOrder.Side = types.SideTypeSell
|
||||||
|
|
||||||
|
order, err := toGlobalOrder(newUnfilledOrder)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(&newExpOrder, order)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("succeeds with market sell", func(t *testing.T) {
|
||||||
|
newUnfilledOrder := unfilledOrder
|
||||||
|
newUnfilledOrder.Side = v2.SideTypeSell
|
||||||
|
newUnfilledOrder.OrderType = v2.OrderTypeMarket
|
||||||
|
|
||||||
|
newExpOrder := *expOrder
|
||||||
|
newExpOrder.Side = types.SideTypeSell
|
||||||
|
newExpOrder.Type = types.OrderTypeMarket
|
||||||
|
newExpOrder.Price = newUnfilledOrder.PriceAvg
|
||||||
|
|
||||||
|
order, err := toGlobalOrder(newUnfilledOrder)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(&newExpOrder, order)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("succeeds with market buy", func(t *testing.T) {
|
||||||
|
newUnfilledOrder := unfilledOrder
|
||||||
|
newUnfilledOrder.Side = v2.SideTypeBuy
|
||||||
|
newUnfilledOrder.OrderType = v2.OrderTypeMarket
|
||||||
|
|
||||||
|
newExpOrder := *expOrder
|
||||||
|
newExpOrder.Side = types.SideTypeBuy
|
||||||
|
newExpOrder.Type = types.OrderTypeMarket
|
||||||
|
newExpOrder.Price = newUnfilledOrder.PriceAvg
|
||||||
|
newExpOrder.Quantity = newUnfilledOrder.BaseVolume
|
||||||
|
|
||||||
|
order, err := toGlobalOrder(newUnfilledOrder)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(&newExpOrder, order)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("succeeds with limit buy", func(t *testing.T) {
|
||||||
|
order, err := toGlobalOrder(unfilledOrder)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(&types.Order{
|
||||||
|
SubmitOrder: types.SubmitOrder{
|
||||||
|
ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3",
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Type: types.OrderTypeLimit,
|
||||||
|
Quantity: fixedpoint.NewFromFloat(5),
|
||||||
|
Price: fixedpoint.NewFromFloat(1.2),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
},
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
OrderID: uint64(orderId),
|
||||||
|
UUID: strconv.FormatInt(int64(orderId), 10),
|
||||||
|
Status: types.OrderStatusFilled,
|
||||||
|
ExecutedQuantity: fixedpoint.NewFromFloat(5),
|
||||||
|
IsWorking: false,
|
||||||
|
CreationTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||||
|
UpdateTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()),
|
||||||
|
}, order)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to convert side", func(t *testing.T) {
|
||||||
|
newOrder := unfilledOrder
|
||||||
|
newOrder.Side = "xxx"
|
||||||
|
|
||||||
|
_, err := toGlobalOrder(newOrder)
|
||||||
|
assert.ErrorContains(err, "xxx")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to convert oder type", func(t *testing.T) {
|
||||||
|
newOrder := unfilledOrder
|
||||||
|
newOrder.OrderType = "xxx"
|
||||||
|
|
||||||
|
_, err := toGlobalOrder(newOrder)
|
||||||
|
assert.ErrorContains(err, "xxx")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to convert oder status", func(t *testing.T) {
|
||||||
|
newOrder := unfilledOrder
|
||||||
|
newOrder.Status = "xxx"
|
||||||
|
|
||||||
|
_, err := toGlobalOrder(newOrder)
|
||||||
|
assert.ErrorContains(err, "xxx")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_processMarketBuyQuantity(t *testing.T) {
|
||||||
|
var (
|
||||||
|
assert = assert.New(t)
|
||||||
|
filledBaseCoinQty = fixedpoint.NewFromFloat(3.5648)
|
||||||
|
filledPrice = fixedpoint.NewFromFloat(4.99998848)
|
||||||
|
priceAvg = fixedpoint.NewFromFloat(1.4026)
|
||||||
|
buyQty = fixedpoint.NewFromFloat(5)
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("zero quantity on Init/New/Live/Cancelled", func(t *testing.T) {
|
||||||
|
qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusInit)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(fixedpoint.Zero, qty)
|
||||||
|
|
||||||
|
qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusNew)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(fixedpoint.Zero, qty)
|
||||||
|
|
||||||
|
qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusLive)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(fixedpoint.Zero, qty)
|
||||||
|
|
||||||
|
qty, err = processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusCancelled)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(fixedpoint.Zero, qty)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("5 on PartialFilled", func(t *testing.T) {
|
||||||
|
priceAvg := fixedpoint.NewFromFloat(2)
|
||||||
|
buyQty := fixedpoint.NewFromFloat(10)
|
||||||
|
filledPrice := fixedpoint.NewFromFloat(4)
|
||||||
|
filledBaseCoinQty := fixedpoint.NewFromFloat(2)
|
||||||
|
|
||||||
|
qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusPartialFilled)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(fixedpoint.NewFromFloat(5), qty)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("3.5648 on Filled", func(t *testing.T) {
|
||||||
|
qty, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, v2.OrderStatusFilled)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal(fixedpoint.NewFromFloat(3.5648), qty)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unexpected order status", func(t *testing.T) {
|
||||||
|
_, err := processMarketBuyQuantity(filledBaseCoinQty, filledPrice, priceAvg, buyQty, "xxx")
|
||||||
|
assert.ErrorContains(err, "xxx")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_toLocalOrderType(t *testing.T) {
|
||||||
|
orderType, err := toLocalOrderType(types.OrderTypeLimit)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, v2.OrderTypeLimit, orderType)
|
||||||
|
|
||||||
|
orderType, err = toLocalOrderType(types.OrderTypeMarket)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, v2.OrderTypeMarket, orderType)
|
||||||
|
|
||||||
|
_, err = toLocalOrderType("xxx")
|
||||||
|
assert.ErrorContains(t, err, "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_toLocalSide(t *testing.T) {
|
||||||
|
orderType, err := toLocalSide(types.SideTypeSell)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, v2.SideTypeSell, orderType)
|
||||||
|
|
||||||
|
orderType, err = toLocalSide(types.SideTypeBuy)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, v2.SideTypeBuy, orderType)
|
||||||
|
|
||||||
|
_, err = toLocalOrderType("xxx")
|
||||||
|
assert.ErrorContains(t, err, "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isMaker(t *testing.T) {
|
||||||
|
isM, err := isMaker(v2.TradeTaker)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, isM)
|
||||||
|
|
||||||
|
isM, err = isMaker(v2.TradeMaker)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, isM)
|
||||||
|
|
||||||
|
_, err = isMaker("xxx")
|
||||||
|
assert.ErrorContains(t, err, "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isFeeDiscount(t *testing.T) {
|
||||||
|
isDiscount, err := isFeeDiscount(v2.DiscountNo)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, isDiscount)
|
||||||
|
|
||||||
|
isDiscount, err = isFeeDiscount(v2.DiscountYes)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, isDiscount)
|
||||||
|
|
||||||
|
_, err = isFeeDiscount("xxx")
|
||||||
|
assert.ErrorContains(t, err, "xxx")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_toGlobalTrade(t *testing.T) {
|
||||||
|
// {
|
||||||
|
// "userId":"8672173294",
|
||||||
|
// "symbol":"APEUSDT",
|
||||||
|
// "orderId":"1104337778433757184",
|
||||||
|
// "tradeId":"1104337778504044545",
|
||||||
|
// "orderType":"limit",
|
||||||
|
// "side":"sell",
|
||||||
|
// "priceAvg":"1.4001",
|
||||||
|
// "size":"5",
|
||||||
|
// "amount":"7.0005",
|
||||||
|
// "feeDetail":{
|
||||||
|
// "deduction":"no",
|
||||||
|
// "feeCoin":"USDT",
|
||||||
|
// "totalDeductionFee":"",
|
||||||
|
// "totalFee":"-0.0070005"
|
||||||
|
// },
|
||||||
|
// "tradeScope":"taker",
|
||||||
|
// "cTime":"1699020564676",
|
||||||
|
// "uTime":"1699020564687"
|
||||||
|
//}
|
||||||
|
trade := v2.Trade{
|
||||||
|
UserId: types.StrInt64(8672173294),
|
||||||
|
Symbol: "APEUSDT",
|
||||||
|
OrderId: types.StrInt64(1104337778433757184),
|
||||||
|
TradeId: types.StrInt64(1104337778504044545),
|
||||||
|
OrderType: v2.OrderTypeLimit,
|
||||||
|
Side: v2.SideTypeSell,
|
||||||
|
PriceAvg: fixedpoint.NewFromFloat(1.4001),
|
||||||
|
Size: fixedpoint.NewFromFloat(5),
|
||||||
|
Amount: fixedpoint.NewFromFloat(7.0005),
|
||||||
|
FeeDetail: v2.TradeFee{
|
||||||
|
Deduction: "no",
|
||||||
|
FeeCoin: "USDT",
|
||||||
|
TotalDeductionFee: fixedpoint.Zero,
|
||||||
|
TotalFee: fixedpoint.NewFromFloat(-0.0070005),
|
||||||
|
},
|
||||||
|
TradeScope: v2.TradeTaker,
|
||||||
|
CTime: types.NewMillisecondTimestampFromInt(1699020564676),
|
||||||
|
UTime: types.NewMillisecondTimestampFromInt(1699020564687),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := toGlobalTrade(trade)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, &types.Trade{
|
||||||
|
ID: uint64(1104337778504044545),
|
||||||
|
OrderID: uint64(1104337778433757184),
|
||||||
|
Exchange: types.ExchangeBitget,
|
||||||
|
Price: fixedpoint.NewFromFloat(1.4001),
|
||||||
|
Quantity: fixedpoint.NewFromFloat(5),
|
||||||
|
QuoteQuantity: fixedpoint.NewFromFloat(7.0005),
|
||||||
|
Symbol: "APEUSDT",
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
IsBuyer: false,
|
||||||
|
IsMaker: false,
|
||||||
|
Time: types.Time(types.NewMillisecondTimestampFromInt(1699020564676)),
|
||||||
|
Fee: fixedpoint.NewFromFloat(0.0070005),
|
||||||
|
FeeCurrency: "USDT",
|
||||||
|
FeeDiscounted: false,
|
||||||
|
}, res)
|
||||||
|
}
|
||||||
|
|
|
@ -2,19 +2,29 @@ package bitget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"go.uber.org/multierr"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
"github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi"
|
||||||
|
v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ID = "bitget"
|
const (
|
||||||
|
ID = "bitget"
|
||||||
|
|
||||||
const PlatformToken = "BGB"
|
PlatformToken = "BGB"
|
||||||
|
|
||||||
|
queryLimit = 100
|
||||||
|
maxOrderIdLen = 36
|
||||||
|
queryMaxDuration = 90 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
var log = logrus.WithFields(logrus.Fields{
|
var log = logrus.WithFields(logrus.Fields{
|
||||||
"exchange": ID,
|
"exchange": ID,
|
||||||
|
@ -29,12 +39,23 @@ var (
|
||||||
queryTickerRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
queryTickerRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
||||||
// queryTickersRateLimiter has its own rate limit. https://bitgetlimited.github.io/apidoc/en/spot/#get-all-tickers
|
// queryTickersRateLimiter has its own rate limit. https://bitgetlimited.github.io/apidoc/en/spot/#get-all-tickers
|
||||||
queryTickersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
queryTickersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
||||||
|
// queryOpenOrdersRateLimiter has its own rate limit. https://www.bitget.com/zh-CN/api-doc/spot/trade/Get-Unfilled-Orders
|
||||||
|
queryOpenOrdersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5)
|
||||||
|
// closedQueryOrdersRateLimiter has its own rate limit. https://www.bitget.com/api-doc/spot/trade/Get-History-Orders
|
||||||
|
closedQueryOrdersRateLimiter = rate.NewLimiter(rate.Every(time.Second/15), 5)
|
||||||
|
// submitOrdersRateLimiter has its own rate limit. https://www.bitget.com/zh-CN/api-doc/spot/trade/Place-Order
|
||||||
|
submitOrdersRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
||||||
|
// queryTradeRateLimiter has its own rate limit. https://www.bitget.com/zh-CN/api-doc/spot/trade/Get-Fills
|
||||||
|
queryTradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
||||||
|
// cancelOrderRateLimiter has its own rate limit. https://www.bitget.com/api-doc/spot/trade/Cancel-Order
|
||||||
|
cancelOrderRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5)
|
||||||
)
|
)
|
||||||
|
|
||||||
type Exchange struct {
|
type Exchange struct {
|
||||||
key, secret, passphrase string
|
key, secret, passphrase string
|
||||||
|
|
||||||
client *bitgetapi.RestClient
|
client *bitgetapi.RestClient
|
||||||
|
v2Client *v2.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(key, secret, passphrase string) *Exchange {
|
func New(key, secret, passphrase string) *Exchange {
|
||||||
|
@ -49,6 +70,7 @@ func New(key, secret, passphrase string) *Exchange {
|
||||||
secret: secret,
|
secret: secret,
|
||||||
passphrase: passphrase,
|
passphrase: passphrase,
|
||||||
client: client,
|
client: client,
|
||||||
|
v2Client: v2.NewClient(client),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,17 +190,321 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
|
||||||
return bals, nil
|
return bals, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SubmitOrder submits an order.
|
||||||
|
//
|
||||||
|
// Remark:
|
||||||
|
// 1. We support only GTC for time-in-force, because the response from queryOrder does not include time-in-force information.
|
||||||
|
// 2. For market buy orders, the size unit is quote currency, whereas the unit for order.Quantity is in base currency.
|
||||||
|
// Therefore, we need to calculate the equivalent quote currency amount based on the ticker data.
|
||||||
|
//
|
||||||
|
// Note that there is a bug in Bitget where you can place a market order with the 'post_only' option successfully,
|
||||||
|
// which should not be possible. The issue has been reported.
|
||||||
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
|
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
|
||||||
// TODO implement me
|
if len(order.Market.Symbol) == 0 {
|
||||||
panic("implement me")
|
return nil, fmt.Errorf("order.Market.Symbol is required: %+v", order)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := e.v2Client.NewPlaceOrderRequest()
|
||||||
|
req.Symbol(order.Market.Symbol)
|
||||||
|
|
||||||
|
// set order type
|
||||||
|
orderType, err := toLocalOrderType(order.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.OrderType(orderType)
|
||||||
|
|
||||||
|
// set side
|
||||||
|
side, err := toLocalSide(order.Side)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Side(side)
|
||||||
|
|
||||||
|
// set quantity
|
||||||
|
qty := order.Quantity
|
||||||
|
// if the order is market buy, the quantity is quote coin, instead of base coin. so we need to convert it.
|
||||||
|
if order.Type == types.OrderTypeMarket && order.Side == types.SideTypeBuy {
|
||||||
|
ticker, err := e.QueryTicker(ctx, order.Market.Symbol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
qty = order.Quantity.Mul(ticker.Buy)
|
||||||
|
}
|
||||||
|
req.Size(order.Market.FormatQuantity(qty))
|
||||||
|
|
||||||
|
// we support only GTC/PostOnly, this is because:
|
||||||
|
// 1. We support only SPOT trading.
|
||||||
|
// 2. The query oepn/closed order does not including the `force` in SPOT.
|
||||||
|
// If we support FOK/IOC, but you can't query them, that would be unreasonable.
|
||||||
|
// The other case to consider is 'PostOnly', which is a trade-off because we want to support 'xmaker'.
|
||||||
|
if order.TimeInForce != types.TimeInForceGTC {
|
||||||
|
return nil, fmt.Errorf("time-in-force %s not supported", order.TimeInForce)
|
||||||
|
}
|
||||||
|
req.Force(v2.OrderForceGTC)
|
||||||
|
// set price
|
||||||
|
if order.Type == types.OrderTypeLimit || order.Type == types.OrderTypeLimitMaker {
|
||||||
|
req.Price(order.Market.FormatPrice(order.Price))
|
||||||
|
|
||||||
|
if order.Type == types.OrderTypeLimitMaker {
|
||||||
|
req.Force(v2.OrderForcePostOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set client order id
|
||||||
|
if len(order.ClientOrderID) > maxOrderIdLen {
|
||||||
|
return nil, fmt.Errorf("unexpected length of order id, got: %d", len(order.ClientOrderID))
|
||||||
|
}
|
||||||
|
if len(order.ClientOrderID) > 0 {
|
||||||
|
req.ClientOrderId(order.ClientOrderID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := submitOrdersRateLimiter.Wait(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("place order rate limiter wait error: %w", err)
|
||||||
|
}
|
||||||
|
res, err := req.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to place order, order: %#v, err: %w", order, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.OrderId) == 0 || (len(order.ClientOrderID) != 0 && res.ClientOrderId != order.ClientOrderID) {
|
||||||
|
return nil, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderId := res.OrderId
|
||||||
|
ordersResp, err := e.v2Client.NewGetUnfilledOrdersRequest().OrderId(orderId).Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query open order by order id: %s, err: %w", orderId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(ordersResp) {
|
||||||
|
case 0:
|
||||||
|
// The market order will be executed immediately, so we cannot retrieve it through the NewGetUnfilledOrdersRequest API.
|
||||||
|
// Try to get the order from the NewGetHistoryOrdersRequest API.
|
||||||
|
ordersResp, err := e.v2Client.NewGetHistoryOrdersRequest().OrderId(orderId).Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query history order by order id: %s, err: %w", orderId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ordersResp) != 1 {
|
||||||
|
return nil, fmt.Errorf("unexpected order length, order id: %s", orderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return toGlobalOrder(ordersResp[0])
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return unfilledOrderToGlobalOrder(ordersResp[0])
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected order length, order id: %s", orderId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
|
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
|
||||||
// TODO implement me
|
var nextCursor types.StrInt64
|
||||||
panic("implement me")
|
for {
|
||||||
|
if err := queryOpenOrdersRateLimiter.Wait(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("open order rate limiter wait error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := e.v2Client.NewGetUnfilledOrdersRequest().
|
||||||
|
Symbol(symbol).
|
||||||
|
Limit(strconv.FormatInt(queryLimit, 10))
|
||||||
|
if nextCursor != 0 {
|
||||||
|
req.IdLessThan(strconv.FormatInt(int64(nextCursor), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
openOrders, err := req.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query open orders: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range openOrders {
|
||||||
|
order, err := unfilledOrderToGlobalOrder(o)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert order, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
orders = append(orders, *order)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderLen := len(openOrders)
|
||||||
|
// a defensive programming to ensure the length of order response is expected.
|
||||||
|
if orderLen > queryLimit {
|
||||||
|
return nil, fmt.Errorf("unexpected open orders length %d", orderLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
if orderLen < queryLimit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nextCursor = openOrders[orderLen-1].OrderId
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
|
// QueryClosedOrders queries closed order by time range(`CTime`) and id. The order of the response is in descending order.
|
||||||
// TODO implement me
|
// If you need to retrieve all data, please utilize the function pkg/exchange/batch.ClosedOrderBatchQuery.
|
||||||
panic("implement me")
|
//
|
||||||
|
// ** Since is inclusive, Until is exclusive. If you use a time range to query, you must provide both a start time and an end time. **
|
||||||
|
// ** Since and Until cannot exceed 90 days. **
|
||||||
|
// ** Since from the last 90 days can be queried. **
|
||||||
|
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
|
||||||
|
if time.Since(since) > queryMaxDuration {
|
||||||
|
return nil, fmt.Errorf("start time from the last 90 days can be queried, got: %s", since)
|
||||||
|
}
|
||||||
|
if until.Before(since) {
|
||||||
|
return nil, fmt.Errorf("end time %s before start %s", until, since)
|
||||||
|
}
|
||||||
|
if until.Sub(since) > queryMaxDuration {
|
||||||
|
return nil, fmt.Errorf("the start time %s and end time %s cannot exceed 90 days", since, until)
|
||||||
|
}
|
||||||
|
if lastOrderID != 0 {
|
||||||
|
log.Warn("!!!BITGET EXCHANGE API NOTICE!!! The order of response is in descending order, so the last order id not supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := closedQueryOrdersRateLimiter.Wait(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("query closed order rate limiter wait error: %w", err)
|
||||||
|
}
|
||||||
|
res, err := e.v2Client.NewGetHistoryOrdersRequest().
|
||||||
|
Symbol(symbol).
|
||||||
|
Limit(strconv.Itoa(queryLimit)).
|
||||||
|
StartTime(since.UnixMilli()).
|
||||||
|
EndTime(until.UnixMilli()).
|
||||||
|
Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to call get order histories error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range res {
|
||||||
|
o, err2 := toGlobalOrder(order)
|
||||||
|
if err2 != nil {
|
||||||
|
err = multierr.Append(err, err2)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.Status.Closed() {
|
||||||
|
orders = append(orders, *o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.SortOrdersAscending(orders), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (errs error) {
|
||||||
|
if len(orders) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, order := range orders {
|
||||||
|
req := e.client.NewCancelOrderRequest()
|
||||||
|
|
||||||
|
reqId := ""
|
||||||
|
switch {
|
||||||
|
// use the OrderID first, then the ClientOrderID
|
||||||
|
case order.OrderID > 0:
|
||||||
|
req.OrderId(strconv.FormatUint(order.OrderID, 10))
|
||||||
|
reqId = strconv.FormatUint(order.OrderID, 10)
|
||||||
|
|
||||||
|
case len(order.ClientOrderID) != 0:
|
||||||
|
req.ClientOrderId(order.ClientOrderID)
|
||||||
|
reqId = order.ClientOrderID
|
||||||
|
|
||||||
|
default:
|
||||||
|
errs = multierr.Append(
|
||||||
|
errs,
|
||||||
|
fmt.Errorf("the order uuid and client order id are empty, order: %#v", order),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Symbol(order.Market.Symbol)
|
||||||
|
|
||||||
|
if err := cancelOrderRateLimiter.Wait(ctx); err != nil {
|
||||||
|
errs = multierr.Append(errs, fmt.Errorf("cancel order rate limiter wait, order id: %s, error: %w", order.ClientOrderID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
res, err := req.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
errs = multierr.Append(errs, fmt.Errorf("failed to cancel order id: %s, err: %w", order.ClientOrderID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
if res.OrderId != reqId && res.ClientOrderId != reqId {
|
||||||
|
errs = multierr.Append(errs, fmt.Errorf("order id mismatch, exp: %s, respOrderId: %s, respClientOrderId: %s", reqId, res.OrderId, res.ClientOrderId))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryTrades queries fill trades. The trade of the response is in descending order. The time-based query are typically
|
||||||
|
// using (`CTime`) as the search criteria.
|
||||||
|
// If you need to retrieve all data, please utilize the function pkg/exchange/batch.TradeBatchQuery.
|
||||||
|
//
|
||||||
|
// ** StartTime is inclusive, EndTime is exclusive. If you use the EndTime, the StartTime is required. **
|
||||||
|
// ** StartTime and EndTime cannot exceed 90 days. **
|
||||||
|
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
|
||||||
|
if options.LastTradeID != 0 {
|
||||||
|
log.Warn("!!!BITGET EXCHANGE API NOTICE!!! The trade of response is in descending order, so the last trade id not supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := e.v2Client.NewGetTradeFillsRequest()
|
||||||
|
req.Symbol(symbol)
|
||||||
|
|
||||||
|
if options.StartTime != nil {
|
||||||
|
if time.Since(*options.StartTime) > queryMaxDuration {
|
||||||
|
return nil, fmt.Errorf("start time from the last 90 days can be queried, got: %s", options.StartTime)
|
||||||
|
}
|
||||||
|
req.StartTime(options.StartTime.UnixMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.EndTime != nil {
|
||||||
|
if options.StartTime == nil {
|
||||||
|
return nil, errors.New("start time is required for query trades if you take end time")
|
||||||
|
}
|
||||||
|
if options.EndTime.Before(*options.StartTime) {
|
||||||
|
return nil, fmt.Errorf("end time %s before start %s", *options.EndTime, *options.StartTime)
|
||||||
|
}
|
||||||
|
if options.EndTime.Sub(*options.StartTime) > queryMaxDuration {
|
||||||
|
return nil, fmt.Errorf("start time %s and end time %s cannot greater than 90 days", options.StartTime, options.EndTime)
|
||||||
|
}
|
||||||
|
req.EndTime(options.EndTime.UnixMilli())
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := options.Limit
|
||||||
|
if limit > queryLimit || limit <= 0 {
|
||||||
|
log.Debugf("limtit is exceeded or zero, update to %d, got: %d", queryLimit, options.Limit)
|
||||||
|
limit = queryLimit
|
||||||
|
}
|
||||||
|
req.Limit(strconv.FormatInt(limit, 10))
|
||||||
|
|
||||||
|
if err := queryTradeRateLimiter.Wait(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("trade rate limiter wait error: %w", err)
|
||||||
|
}
|
||||||
|
response, err := req.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query trades, err: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs error
|
||||||
|
for _, trade := range response {
|
||||||
|
res, err := toGlobalTrade(trade)
|
||||||
|
if err != nil {
|
||||||
|
errs = multierr.Append(errs, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
trades = append(trades, *res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
return nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return trades, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,6 +187,12 @@ func (p *CancelOrderRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (p *CancelOrderRequest) GetPath() string {
|
||||||
|
return "/v5/order/cancel"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (p *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) {
|
func (p *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) {
|
||||||
|
|
||||||
params, err := p.GetParameters()
|
params, err := p.GetParameters()
|
||||||
|
@ -195,7 +201,9 @@ func (p *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, erro
|
||||||
}
|
}
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
|
|
||||||
apiURL := "/v5/order/cancel"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = p.GetPath()
|
||||||
|
|
||||||
req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
|
req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -211,6 +219,16 @@ func (p *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, erro
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data CancelOrderResponse
|
var data CancelOrderResponse
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -162,10 +162,25 @@ sample:
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type APIResponse struct {
|
type APIResponse struct {
|
||||||
RetCode uint `json:"retCode"`
|
// Success/Error code
|
||||||
RetMsg string `json:"retMsg"`
|
RetCode uint `json:"retCode"`
|
||||||
Result json.RawMessage `json:"result"`
|
// Success/Error msg. OK, success, SUCCESS indicate a successful response
|
||||||
|
RetMsg string `json:"retMsg"`
|
||||||
|
// Business data result
|
||||||
|
Result json.RawMessage `json:"result"`
|
||||||
|
// Extend info. Most of the time, it is {}
|
||||||
RetExtInfo json.RawMessage `json:"retExtInfo"`
|
RetExtInfo json.RawMessage `json:"retExtInfo"`
|
||||||
// Time is current timestamp (ms)
|
// Time is current timestamp (ms)
|
||||||
Time types.MillisecondTimestamp `json:"time"`
|
Time types.MillisecondTimestamp `json:"time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a APIResponse) Validate() error {
|
||||||
|
if a.RetCode != 0 {
|
||||||
|
return a.Error()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a APIResponse) Error() error {
|
||||||
|
return fmt.Errorf("retCode: %d, retMsg: %s, retExtInfo: %q, time: %s", a.RetCode, a.RetMsg, a.RetExtInfo, a.Time)
|
||||||
|
}
|
||||||
|
|
|
@ -109,13 +109,21 @@ func (g *GetAccountInfoRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetAccountInfoRequest) GetPath() string {
|
||||||
|
return "/v5/account/info"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetAccountInfoRequest) Do(ctx context.Context) (*AccountInfo, error) {
|
func (g *GetAccountInfoRequest) Do(ctx context.Context) (*AccountInfo, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
var params interface{}
|
var params interface{}
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
|
|
||||||
apiURL := "/v5/account/info"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -131,6 +139,16 @@ func (g *GetAccountInfoRequest) Do(ctx context.Context) (*AccountInfo, error) {
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data AccountInfo
|
var data AccountInfo
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -156,6 +156,12 @@ func (g *GetFeeRatesRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetFeeRatesRequest) GetPath() string {
|
||||||
|
return "/v5/account/fee-rate"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetFeeRatesRequest) Do(ctx context.Context) (*FeeRates, error) {
|
func (g *GetFeeRatesRequest) Do(ctx context.Context) (*FeeRates, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -165,7 +171,9 @@ func (g *GetFeeRatesRequest) Do(ctx context.Context) (*FeeRates, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/v5/account/fee-rate"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -181,6 +189,16 @@ func (g *GetFeeRatesRequest) Do(ctx context.Context) (*FeeRates, error) {
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data FeeRates
|
var data FeeRates
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -169,6 +169,12 @@ func (g *GetInstrumentsInfoRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetInstrumentsInfoRequest) GetPath() string {
|
||||||
|
return "/v5/market/instruments-info"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetInstrumentsInfoRequest) Do(ctx context.Context) (*InstrumentsInfo, error) {
|
func (g *GetInstrumentsInfoRequest) Do(ctx context.Context) (*InstrumentsInfo, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -178,7 +184,9 @@ func (g *GetInstrumentsInfoRequest) Do(ctx context.Context) (*InstrumentsInfo, e
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/v5/market/instruments-info"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -194,6 +202,16 @@ func (g *GetInstrumentsInfoRequest) Do(ctx context.Context) (*InstrumentsInfo, e
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data InstrumentsInfo
|
var data InstrumentsInfo
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -204,6 +204,12 @@ func (g *GetKLinesRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetKLinesRequest) GetPath() string {
|
||||||
|
return "/v5/market/kline"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetKLinesRequest) Do(ctx context.Context) (*KLinesResponse, error) {
|
func (g *GetKLinesRequest) Do(ctx context.Context) (*KLinesResponse, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -213,7 +219,9 @@ func (g *GetKLinesRequest) Do(ctx context.Context) (*KLinesResponse, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/v5/market/kline"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -229,6 +237,16 @@ func (g *GetKLinesRequest) Do(ctx context.Context) (*KLinesResponse, error) {
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data KLinesResponse
|
var data KLinesResponse
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -258,6 +258,12 @@ func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetOpenOrdersRequest) GetPath() string {
|
||||||
|
return "/v5/order/realtime"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OrdersResponse, error) {
|
func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OrdersResponse, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -267,7 +273,9 @@ func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OrdersResponse, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/v5/order/realtime"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -283,6 +291,16 @@ func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OrdersResponse, error)
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data OrdersResponse
|
var data OrdersResponse
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -262,6 +262,12 @@ func (g *GetOrderHistoriesRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetOrderHistoriesRequest) GetPath() string {
|
||||||
|
return "/v5/order/history"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetOrderHistoriesRequest) Do(ctx context.Context) (*OrdersResponse, error) {
|
func (g *GetOrderHistoriesRequest) Do(ctx context.Context) (*OrdersResponse, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -271,7 +277,9 @@ func (g *GetOrderHistoriesRequest) Do(ctx context.Context) (*OrdersResponse, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/v5/order/history"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -287,6 +295,16 @@ func (g *GetOrderHistoriesRequest) Do(ctx context.Context) (*OrdersResponse, err
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data OrdersResponse
|
var data OrdersResponse
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -143,6 +143,12 @@ func (g *GetTickersRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetTickersRequest) GetPath() string {
|
||||||
|
return "/v5/market/tickers"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetTickersRequest) Do(ctx context.Context) (*APIResponse, error) {
|
func (g *GetTickersRequest) Do(ctx context.Context) (*APIResponse, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -152,7 +158,9 @@ func (g *GetTickersRequest) Do(ctx context.Context) (*APIResponse, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/v5/market/tickers"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -168,5 +176,15 @@ func (g *GetTickersRequest) Do(ctx context.Context) (*APIResponse, error) {
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return &apiResponse, nil
|
return &apiResponse, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,6 +143,12 @@ func (g *GetWalletBalancesRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetWalletBalancesRequest) GetPath() string {
|
||||||
|
return "/v5/account/wallet-balance"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetWalletBalancesRequest) Do(ctx context.Context) (*WalletBalancesResponse, error) {
|
func (g *GetWalletBalancesRequest) Do(ctx context.Context) (*WalletBalancesResponse, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -152,7 +158,9 @@ func (g *GetWalletBalancesRequest) Do(ctx context.Context) (*WalletBalancesRespo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/v5/account/wallet-balance"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -168,6 +176,16 @@ func (g *GetWalletBalancesRequest) Do(ctx context.Context) (*WalletBalancesRespo
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data WalletBalancesResponse
|
var data WalletBalancesResponse
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -496,6 +496,12 @@ func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (p *PlaceOrderRequest) GetPath() string {
|
||||||
|
return "/v5/order/create"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (p *PlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error) {
|
func (p *PlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error) {
|
||||||
|
|
||||||
params, err := p.GetParameters()
|
params, err := p.GetParameters()
|
||||||
|
@ -504,7 +510,9 @@ func (p *PlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error)
|
||||||
}
|
}
|
||||||
query := url.Values{}
|
query := url.Values{}
|
||||||
|
|
||||||
apiURL := "/v5/order/create"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = p.GetPath()
|
||||||
|
|
||||||
req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
|
req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -520,6 +528,16 @@ func (p *PlaceOrderRequest) Do(ctx context.Context) (*PlaceOrderResponse, error)
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data PlaceOrderResponse
|
var data PlaceOrderResponse
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -205,6 +205,12 @@ func (g *GetTradesRequest) GetSlugsMap() (map[string]string, error) {
|
||||||
return slugs, nil
|
return slugs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPath returns the request path of the API
|
||||||
|
func (g *GetTradesRequest) GetPath() string {
|
||||||
|
return "/spot/v3/private/my-trades"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do generates the request object and send the request object to the API endpoint
|
||||||
func (g *GetTradesRequest) Do(ctx context.Context) (*TradesResponse, error) {
|
func (g *GetTradesRequest) Do(ctx context.Context) (*TradesResponse, error) {
|
||||||
|
|
||||||
// no body params
|
// no body params
|
||||||
|
@ -214,7 +220,9 @@ func (g *GetTradesRequest) Do(ctx context.Context) (*TradesResponse, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
apiURL := "/spot/v3/private/my-trades"
|
var apiURL string
|
||||||
|
|
||||||
|
apiURL = g.GetPath()
|
||||||
|
|
||||||
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -230,6 +238,16 @@ func (g *GetTradesRequest) Do(ctx context.Context) (*TradesResponse, error) {
|
||||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type responseValidator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
validator, ok := interface{}(apiResponse).(responseValidator)
|
||||||
|
if ok {
|
||||||
|
if err := validator.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
var data TradesResponse
|
var data TradesResponse
|
||||||
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
132
pkg/exchange/bybit/market_info_poller.go
Normal file
132
pkg/exchange/bybit/market_info_poller.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package bybit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// To maintain aligned fee rates, it's important to update fees frequently.
|
||||||
|
feeRatePollingPeriod = time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type symbolFeeDetail struct {
|
||||||
|
bybitapi.FeeRate
|
||||||
|
|
||||||
|
BaseCoin string
|
||||||
|
QuoteCoin string
|
||||||
|
}
|
||||||
|
|
||||||
|
// feeRatePoller pulls the specified market data from bbgo QueryMarkets.
|
||||||
|
type feeRatePoller struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
once sync.Once
|
||||||
|
client MarketInfoProvider
|
||||||
|
|
||||||
|
symbolFeeDetail map[string]symbolFeeDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFeeRatePoller(marketInfoProvider MarketInfoProvider) *feeRatePoller {
|
||||||
|
return &feeRatePoller{
|
||||||
|
client: marketInfoProvider,
|
||||||
|
symbolFeeDetail: map[string]symbolFeeDetail{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *feeRatePoller) Start(ctx context.Context) {
|
||||||
|
p.once.Do(func() {
|
||||||
|
p.startLoop(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *feeRatePoller) startLoop(ctx context.Context) {
|
||||||
|
err := p.poll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warn("failed to initialize the fee rate, the ticker is scheduled to update it subsequently")
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(feeRatePollingPeriod)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if err := ctx.Err(); !errors.Is(err, context.Canceled) {
|
||||||
|
log.WithError(err).Error("context done with error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := p.poll(ctx); err != nil {
|
||||||
|
log.WithError(err).Warn("failed to update fee rate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *feeRatePoller) poll(ctx context.Context) error {
|
||||||
|
symbolFeeRate, err := p.getAllFeeRates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
p.symbolFeeDetail = symbolFeeRate
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *feeRatePoller) Get(symbol string) (symbolFeeDetail, bool) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
fee, found := p.symbolFeeDetail[symbol]
|
||||||
|
return fee, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *feeRatePoller) getAllFeeRates(ctx context.Context) (map[string]symbolFeeDetail, error) {
|
||||||
|
feeRates, err := e.client.GetAllFeeRates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to call get fee rates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
symbolMap := map[string]symbolFeeDetail{}
|
||||||
|
for _, f := range feeRates.List {
|
||||||
|
if _, found := symbolMap[f.Symbol]; !found {
|
||||||
|
symbolMap[f.Symbol] = symbolFeeDetail{FeeRate: f}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mkts, err := e.client.QueryMarkets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get markets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update base coin, quote coin into symbolFeeDetail
|
||||||
|
for _, mkt := range mkts {
|
||||||
|
feeRate, found := symbolMap[mkt.Symbol]
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
feeRate.BaseCoin = mkt.BaseCurrency
|
||||||
|
feeRate.QuoteCoin = mkt.QuoteCurrency
|
||||||
|
|
||||||
|
symbolMap[mkt.Symbol] = feeRate
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove trading pairs that are not present in spot market.
|
||||||
|
for k, v := range symbolMap {
|
||||||
|
if len(v.BaseCoin) == 0 || len(v.QuoteCoin) == 0 {
|
||||||
|
log.Debugf("related market not found: %s, skipping the associated trade", k)
|
||||||
|
delete(symbolMap, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbolMap, nil
|
||||||
|
}
|
173
pkg/exchange/bybit/market_info_poller_test.go
Normal file
173
pkg/exchange/bybit/market_info_poller_test.go
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
package bybit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/bybit/mocks"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFeeRatePoller_getAllFeeRates(t *testing.T) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
|
unknownErr := errors.New("unknown err")
|
||||||
|
|
||||||
|
t.Run("succeeds", func(t *testing.T) {
|
||||||
|
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
||||||
|
s := &feeRatePoller{
|
||||||
|
client: mockMarketProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
feeRates := bybitapi.FeeRates{
|
||||||
|
List: []bybitapi.FeeRate{
|
||||||
|
{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Symbol: "ETHUSDT",
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Symbol: "OPTIONCOIN",
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mkts := types.MarketMap{
|
||||||
|
"BTCUSDT": types.Market{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
BaseCurrency: "BTC",
|
||||||
|
},
|
||||||
|
"ETHUSDT": types.Market{
|
||||||
|
Symbol: "ETHUSDT",
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
BaseCurrency: "ETH",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
|
||||||
|
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(mkts, nil).Times(1)
|
||||||
|
|
||||||
|
expFeeRates := map[string]symbolFeeDetail{
|
||||||
|
"BTCUSDT": {
|
||||||
|
FeeRate: feeRates.List[0],
|
||||||
|
BaseCoin: "BTC",
|
||||||
|
QuoteCoin: "USDT",
|
||||||
|
},
|
||||||
|
"ETHUSDT": {
|
||||||
|
FeeRate: feeRates.List[1],
|
||||||
|
BaseCoin: "ETH",
|
||||||
|
QuoteCoin: "USDT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
symbolFeeDetails, err := s.getAllFeeRates(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expFeeRates, symbolFeeDetails)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to query markets", func(t *testing.T) {
|
||||||
|
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
||||||
|
s := &feeRatePoller{
|
||||||
|
client: mockMarketProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
feeRates := bybitapi.FeeRates{
|
||||||
|
List: []bybitapi.FeeRate{
|
||||||
|
{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Symbol: "ETHUSDT",
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Symbol: "OPTIONCOIN",
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
|
||||||
|
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(nil, unknownErr).Times(1)
|
||||||
|
|
||||||
|
symbolFeeDetails, err := s.getAllFeeRates(ctx)
|
||||||
|
assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err)
|
||||||
|
assert.Equal(t, map[string]symbolFeeDetail(nil), symbolFeeDetails)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("failed to get fee rates", func(t *testing.T) {
|
||||||
|
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
||||||
|
s := &feeRatePoller{
|
||||||
|
client: mockMarketProvider,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1)
|
||||||
|
|
||||||
|
symbolFeeDetails, err := s.getAllFeeRates(ctx)
|
||||||
|
assert.Equal(t, fmt.Errorf("failed to call get fee rates: %w", unknownErr), err)
|
||||||
|
assert.Equal(t, map[string]symbolFeeDetail(nil), symbolFeeDetails)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_feeRatePoller_Get(t *testing.T) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
|
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
||||||
|
t.Run("found", func(t *testing.T) {
|
||||||
|
symbol := "BTCUSDT"
|
||||||
|
expFeeDetail := symbolFeeDetail{
|
||||||
|
FeeRate: bybitapi.FeeRate{
|
||||||
|
Symbol: symbol,
|
||||||
|
TakerFeeRate: fixedpoint.NewFromFloat(0.1),
|
||||||
|
MakerFeeRate: fixedpoint.NewFromFloat(0.2),
|
||||||
|
},
|
||||||
|
BaseCoin: "BTC",
|
||||||
|
QuoteCoin: "USDT",
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &feeRatePoller{
|
||||||
|
client: mockMarketProvider,
|
||||||
|
symbolFeeDetail: map[string]symbolFeeDetail{
|
||||||
|
symbol: expFeeDetail,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, found := s.Get(symbol)
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, expFeeDetail, res)
|
||||||
|
})
|
||||||
|
t.Run("not found", func(t *testing.T) {
|
||||||
|
symbol := "BTCUSDT"
|
||||||
|
s := &feeRatePoller{
|
||||||
|
client: mockMarketProvider,
|
||||||
|
symbolFeeDetail: map[string]symbolFeeDetail{},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, found := s.Get(symbol)
|
||||||
|
assert.False(t, found)
|
||||||
|
})
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
"github.com/c9s/bbgo/pkg/util"
|
"github.com/c9s/bbgo/pkg/util"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +23,11 @@ const (
|
||||||
var (
|
var (
|
||||||
// wsAuthRequest specifies the duration for which a websocket request's authentication is valid.
|
// wsAuthRequest specifies the duration for which a websocket request's authentication is valid.
|
||||||
wsAuthRequest = 10 * time.Second
|
wsAuthRequest = 10 * time.Second
|
||||||
|
// The default taker/maker fees can help us in estimating trading fees in the SPOT market, because trade fees are not
|
||||||
|
// provided for traditional accounts on Bybit.
|
||||||
|
// https://www.bybit.com/en-US/help-center/article/Trading-Fee-Structure
|
||||||
|
defaultTakerFee = fixedpoint.NewFromFloat(0.001)
|
||||||
|
defaultMakerFee = fixedpoint.NewFromFloat(0.001)
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarketInfoProvider calculates trade fees since trading fees are not supported by streaming.
|
// MarketInfoProvider calculates trade fees since trading fees are not supported by streaming.
|
||||||
|
@ -47,8 +53,8 @@ type Stream struct {
|
||||||
|
|
||||||
key, secret string
|
key, secret string
|
||||||
streamDataProvider StreamDataProvider
|
streamDataProvider StreamDataProvider
|
||||||
// TODO: update the fee rate at 7:00 am UTC; rotation required.
|
feeRateProvider *feeRatePoller
|
||||||
symbolFeeDetails map[string]*symbolFeeDetail
|
marketsInfo types.MarketMap
|
||||||
|
|
||||||
bookEventCallbacks []func(e BookEvent)
|
bookEventCallbacks []func(e BookEvent)
|
||||||
marketTradeEventCallbacks []func(e []MarketTradeEvent)
|
marketTradeEventCallbacks []func(e []MarketTradeEvent)
|
||||||
|
@ -65,13 +71,23 @@ func NewStream(key, secret string, userDataProvider StreamDataProvider) *Stream
|
||||||
key: key,
|
key: key,
|
||||||
secret: secret,
|
secret: secret,
|
||||||
streamDataProvider: userDataProvider,
|
streamDataProvider: userDataProvider,
|
||||||
|
feeRateProvider: newFeeRatePoller(userDataProvider),
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.SetEndpointCreator(stream.createEndpoint)
|
stream.SetEndpointCreator(stream.createEndpoint)
|
||||||
stream.SetParser(stream.parseWebSocketEvent)
|
stream.SetParser(stream.parseWebSocketEvent)
|
||||||
stream.SetDispatcher(stream.dispatchEvent)
|
stream.SetDispatcher(stream.dispatchEvent)
|
||||||
stream.SetHeartBeat(stream.ping)
|
stream.SetHeartBeat(stream.ping)
|
||||||
stream.SetBeforeConnect(stream.getAllFeeRates)
|
stream.SetBeforeConnect(func(ctx context.Context) (err error) {
|
||||||
|
go stream.feeRateProvider.Start(ctx)
|
||||||
|
|
||||||
|
stream.marketsInfo, err = stream.streamDataProvider.QueryMarkets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("failed to query market info before to connect stream")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
stream.OnConnect(stream.handlerConnect)
|
stream.OnConnect(stream.handlerConnect)
|
||||||
stream.OnAuth(stream.handleAuthEvent)
|
stream.OnAuth(stream.handleAuthEvent)
|
||||||
|
|
||||||
|
@ -403,13 +419,34 @@ func (s *Stream) handleKLineEvent(klineEvent KLineEvent) {
|
||||||
|
|
||||||
func (s *Stream) handleTradeEvent(events []TradeEvent) {
|
func (s *Stream) handleTradeEvent(events []TradeEvent) {
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
feeRate, found := s.symbolFeeDetails[event.Symbol]
|
feeRate, found := s.feeRateProvider.Get(event.Symbol)
|
||||||
if !found {
|
if !found {
|
||||||
log.Warnf("unexpected symbol found, fee rate not supported, symbol: %s", event.Symbol)
|
feeRate = symbolFeeDetail{
|
||||||
continue
|
FeeRate: bybitapi.FeeRate{
|
||||||
|
Symbol: event.Symbol,
|
||||||
|
TakerFeeRate: defaultTakerFee,
|
||||||
|
MakerFeeRate: defaultMakerFee,
|
||||||
|
},
|
||||||
|
BaseCoin: "",
|
||||||
|
QuoteCoin: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
if market, ok := s.marketsInfo[event.Symbol]; ok {
|
||||||
|
feeRate.BaseCoin = market.BaseCurrency
|
||||||
|
feeRate.QuoteCoin = market.QuoteCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
// The error log level was utilized due to a detected discrepancy in the fee calculations.
|
||||||
|
log.Errorf("failed to get %s fee rate, use default taker fee %f, maker fee %f, base coin: %s, quote coin: %s",
|
||||||
|
event.Symbol,
|
||||||
|
feeRate.TakerFeeRate.Float64(),
|
||||||
|
feeRate.MakerFeeRate.Float64(),
|
||||||
|
feeRate.BaseCoin,
|
||||||
|
feeRate.QuoteCoin,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
gTrade, err := event.toGlobalTrade(*feeRate)
|
gTrade, err := event.toGlobalTrade(feeRate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Errorf("unable to convert: %+v", event)
|
log.WithError(err).Errorf("unable to convert: %+v", event)
|
||||||
continue
|
continue
|
||||||
|
@ -417,53 +454,3 @@ func (s *Stream) handleTradeEvent(events []TradeEvent) {
|
||||||
s.StandardStream.EmitTradeUpdate(*gTrade)
|
s.StandardStream.EmitTradeUpdate(*gTrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type symbolFeeDetail struct {
|
|
||||||
bybitapi.FeeRate
|
|
||||||
|
|
||||||
BaseCoin string
|
|
||||||
QuoteCoin string
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAllFeeRates retrieves all fee rates from the Bybit API and then fetches markets to ensure the base coin and quote coin
|
|
||||||
// are correct.
|
|
||||||
func (e *Stream) getAllFeeRates(ctx context.Context) error {
|
|
||||||
feeRates, err := e.streamDataProvider.GetAllFeeRates(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to call get fee rates: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
symbolMap := map[string]*symbolFeeDetail{}
|
|
||||||
for _, f := range feeRates.List {
|
|
||||||
if _, found := symbolMap[f.Symbol]; !found {
|
|
||||||
symbolMap[f.Symbol] = &symbolFeeDetail{FeeRate: f}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mkts, err := e.streamDataProvider.QueryMarkets(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get markets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update base coin, quote coin into symbolFeeDetail
|
|
||||||
for _, mkt := range mkts {
|
|
||||||
feeRate, found := symbolMap[mkt.Symbol]
|
|
||||||
if !found {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
feeRate.BaseCoin = mkt.BaseCurrency
|
|
||||||
feeRate.QuoteCoin = mkt.QuoteCurrency
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove trading pairs that are not present in spot market.
|
|
||||||
for k, v := range symbolMap {
|
|
||||||
if len(v.BaseCoin) == 0 || len(v.QuoteCoin) == 0 {
|
|
||||||
log.Debugf("related market not found: %s, skipping the associated trade", k)
|
|
||||||
delete(symbolMap, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
e.symbolFeeDetails = symbolMap
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,11 +9,9 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bybit/mocks"
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/testutil"
|
"github.com/c9s/bbgo/pkg/testutil"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
@ -36,7 +34,7 @@ func getTestClientOrSkip(t *testing.T) *Stream {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStream(t *testing.T) {
|
func TestStream(t *testing.T) {
|
||||||
t.Skip()
|
//t.Skip()
|
||||||
s := getTestClientOrSkip(t)
|
s := getTestClientOrSkip(t)
|
||||||
|
|
||||||
symbols := []string{
|
symbols := []string{
|
||||||
|
@ -70,12 +68,12 @@ func TestStream(t *testing.T) {
|
||||||
err := s.Connect(context.Background())
|
err := s.Connect(context.Background())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
s.OnBookSnapshot(func(book types.SliceOrderBook) {
|
//s.OnBookSnapshot(func(book types.SliceOrderBook) {
|
||||||
t.Log("got snapshot", book)
|
// t.Log("got snapshot", book)
|
||||||
})
|
//})
|
||||||
s.OnBookUpdate(func(book types.SliceOrderBook) {
|
//s.OnBookUpdate(func(book types.SliceOrderBook) {
|
||||||
t.Log("got update", book)
|
// t.Log("got update", book)
|
||||||
})
|
//})
|
||||||
c := make(chan struct{})
|
c := make(chan struct{})
|
||||||
<-c
|
<-c
|
||||||
})
|
})
|
||||||
|
@ -175,7 +173,7 @@ func TestStream(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
s.OnTradeUpdate(func(trade types.Trade) {
|
s.OnTradeUpdate(func(trade types.Trade) {
|
||||||
t.Log("got update", trade)
|
t.Log("got update", trade.Fee, trade.FeeCurrency, trade)
|
||||||
})
|
})
|
||||||
c := make(chan struct{})
|
c := make(chan struct{})
|
||||||
<-c
|
<-c
|
||||||
|
@ -467,120 +465,3 @@ func Test_convertSubscription(t *testing.T) {
|
||||||
assert.Equal(t, genTopic(TopicTypeMarketTrade, "BTCUSDT"), res)
|
assert.Equal(t, genTopic(TopicTypeMarketTrade, "BTCUSDT"), res)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStream_getFeeRate(t *testing.T) {
|
|
||||||
mockCtrl := gomock.NewController(t)
|
|
||||||
defer mockCtrl.Finish()
|
|
||||||
|
|
||||||
unknownErr := errors.New("unknown err")
|
|
||||||
|
|
||||||
t.Run("succeeds", func(t *testing.T) {
|
|
||||||
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
|
||||||
s := &Stream{
|
|
||||||
streamDataProvider: mockMarketProvider,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
feeRates := bybitapi.FeeRates{
|
|
||||||
List: []bybitapi.FeeRate{
|
|
||||||
{
|
|
||||||
Symbol: "BTCUSDT",
|
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Symbol: "ETHUSDT",
|
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Symbol: "OPTIONCOIN",
|
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mkts := types.MarketMap{
|
|
||||||
"BTCUSDT": types.Market{
|
|
||||||
Symbol: "BTCUSDT",
|
|
||||||
QuoteCurrency: "USDT",
|
|
||||||
BaseCurrency: "BTC",
|
|
||||||
},
|
|
||||||
"ETHUSDT": types.Market{
|
|
||||||
Symbol: "ETHUSDT",
|
|
||||||
QuoteCurrency: "USDT",
|
|
||||||
BaseCurrency: "ETH",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
|
|
||||||
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(mkts, nil).Times(1)
|
|
||||||
|
|
||||||
expFeeRates := map[string]*symbolFeeDetail{
|
|
||||||
"BTCUSDT": {
|
|
||||||
FeeRate: feeRates.List[0],
|
|
||||||
BaseCoin: "BTC",
|
|
||||||
QuoteCoin: "USDT",
|
|
||||||
},
|
|
||||||
"ETHUSDT": {
|
|
||||||
FeeRate: feeRates.List[1],
|
|
||||||
BaseCoin: "ETH",
|
|
||||||
QuoteCoin: "USDT",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := s.getAllFeeRates(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expFeeRates, s.symbolFeeDetails)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("failed to query markets", func(t *testing.T) {
|
|
||||||
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
|
||||||
s := &Stream{
|
|
||||||
streamDataProvider: mockMarketProvider,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
feeRates := bybitapi.FeeRates{
|
|
||||||
List: []bybitapi.FeeRate{
|
|
||||||
{
|
|
||||||
Symbol: "BTCUSDT",
|
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Symbol: "ETHUSDT",
|
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Symbol: "OPTIONCOIN",
|
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
MakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
|
|
||||||
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(nil, unknownErr).Times(1)
|
|
||||||
|
|
||||||
err := s.getAllFeeRates(ctx)
|
|
||||||
assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err)
|
|
||||||
assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("failed to get fee rates", func(t *testing.T) {
|
|
||||||
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
|
||||||
s := &Stream{
|
|
||||||
streamDataProvider: mockMarketProvider,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1)
|
|
||||||
|
|
||||||
err := s.getAllFeeRates(ctx)
|
|
||||||
assert.Equal(t, fmt.Errorf("failed to call get fee rates: %w", unknownErr), err)
|
|
||||||
assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -66,22 +66,25 @@ func (s *Strategy) recoverActiveOrdersPeriodically(ctx context.Context) {
|
||||||
exchange: s.session.Exchange,
|
exchange: s.session.Exchange,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastRecoverTime time.Time
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if err := syncActiveOrders(ctx, opts); err != nil {
|
s.recoverC <- struct{}{}
|
||||||
log.WithError(err).Errorf("unable to sync active orders")
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-s.recoverC:
|
case <-s.recoverC:
|
||||||
if err := syncActiveOrders(ctx, opts); err != nil {
|
if !time.Now().After(lastRecoverTime.Add(10 * time.Minute)) {
|
||||||
log.WithError(err).Errorf("unable to sync active orders")
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := syncActiveOrders(ctx, opts); err != nil {
|
||||||
|
log.WithError(err).Errorf("unable to sync active orders")
|
||||||
|
} else {
|
||||||
|
lastRecoverTime = time.Now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,20 +119,18 @@ func syncActiveOrders(ctx context.Context, opts SyncActiveOrdersOpts) error {
|
||||||
// no need to sync active order already in active orderbook, because we only need to know if it filled or not.
|
// no need to sync active order already in active orderbook, because we only need to know if it filled or not.
|
||||||
delete(openOrdersMap, activeOrder.OrderID)
|
delete(openOrdersMap, activeOrder.OrderID)
|
||||||
} else {
|
} else {
|
||||||
opts.logger.Infof("found active order #%d is not in the open orders, updating...", activeOrder.OrderID)
|
opts.logger.Infof("[ActiveOrderRecover] found active order #%d is not in the open orders, updating...", activeOrder.OrderID)
|
||||||
if activeOrder.UpdateTime.After(syncBefore) {
|
|
||||||
opts.logger.Infof("active order #%d is updated in 3 min, skip updating...", activeOrder.OrderID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// sleep 100ms to avoid DDOS
|
isActiveOrderBookUpdated, err := syncActiveOrder(ctx, opts.activeOrderBook, opts.orderQueryService, activeOrder.OrderID, syncBefore)
|
||||||
time.Sleep(100 * time.Millisecond)
|
if err != nil {
|
||||||
|
|
||||||
if err := syncActiveOrder(ctx, opts.activeOrderBook, opts.orderQueryService, activeOrder.OrderID); err != nil {
|
|
||||||
opts.logger.WithError(err).Errorf("[ActiveOrderRecover] unable to query order #%d", activeOrder.OrderID)
|
opts.logger.WithError(err).Errorf("[ActiveOrderRecover] unable to query order #%d", activeOrder.OrderID)
|
||||||
errs = multierr.Append(errs, err)
|
errs = multierr.Append(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isActiveOrderBookUpdated {
|
||||||
|
opts.logger.Infof("[ActiveOrderRecover] active order #%d is updated in 3 min, skip updating...", activeOrder.OrderID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,9 @@ type GridProfitStats struct {
|
||||||
Market types.Market `json:"market,omitempty"`
|
Market types.Market `json:"market,omitempty"`
|
||||||
Since *time.Time `json:"since,omitempty"`
|
Since *time.Time `json:"since,omitempty"`
|
||||||
InitialOrderID uint64 `json:"initialOrderID"`
|
InitialOrderID uint64 `json:"initialOrderID"`
|
||||||
|
|
||||||
|
// ttl is the ttl to keep in persistence
|
||||||
|
ttl time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGridProfitStats(market types.Market) *GridProfitStats {
|
func newGridProfitStats(market types.Market) *GridProfitStats {
|
||||||
|
@ -40,6 +43,17 @@ func newGridProfitStats(market types.Market) *GridProfitStats {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *GridProfitStats) SetTTL(ttl time.Duration) {
|
||||||
|
if ttl.Nanoseconds() <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ttl = ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GridProfitStats) Expiration() time.Duration {
|
||||||
|
return s.ttl
|
||||||
|
}
|
||||||
|
|
||||||
func (s *GridProfitStats) AddTrade(trade types.Trade) {
|
func (s *GridProfitStats) AddTrade(trade types.Trade) {
|
||||||
if s.TotalFee == nil {
|
if s.TotalFee == nil {
|
||||||
s.TotalFee = make(map[string]fixedpoint.Value)
|
s.TotalFee = make(map[string]fixedpoint.Value)
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var syncWindow = -3 * time.Minute
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Background knowledge
|
Background knowledge
|
||||||
1. active orderbook add orders only when receive new order event or call Add/Update method manually
|
1. active orderbook add orders only when receive new order event or call Add/Update method manually
|
||||||
|
@ -91,11 +93,13 @@ func (s *Strategy) recover(ctx context.Context) error {
|
||||||
|
|
||||||
pins := s.getGrid().Pins
|
pins := s.getGrid().Pins
|
||||||
|
|
||||||
|
syncBefore := time.Now().Add(syncWindow)
|
||||||
|
|
||||||
activeOrdersInTwinOrderBook, err := buildTwinOrderBook(pins, activeOrders)
|
activeOrdersInTwinOrderBook, err := buildTwinOrderBook(pins, activeOrders)
|
||||||
openOrdersInTwinOrderBook, err := buildTwinOrderBook(pins, openOrders)
|
openOrdersInTwinOrderBook, err := buildTwinOrderBook(pins, openOrders)
|
||||||
|
|
||||||
s.logger.Infof("active orders' twin orderbook\n%s", activeOrdersInTwinOrderBook.String())
|
s.logger.Infof("[Recover] active orders' twin orderbook\n%s", activeOrdersInTwinOrderBook.String())
|
||||||
s.logger.Infof("open orders in twin orderbook\n%s", openOrdersInTwinOrderBook.String())
|
s.logger.Infof("[Recover] open orders in twin orderbook\n%s", openOrdersInTwinOrderBook.String())
|
||||||
|
|
||||||
// remove index 0, because twin orderbook's price is from the second one
|
// remove index 0, because twin orderbook's price is from the second one
|
||||||
pins = pins[1:]
|
pins = pins[1:]
|
||||||
|
@ -127,7 +131,9 @@ func (s *Strategy) recover(ctx context.Context) error {
|
||||||
|
|
||||||
// case 1
|
// case 1
|
||||||
if activeOrderID == 0 {
|
if activeOrderID == 0 {
|
||||||
activeOrderBook.Add(openOrder.GetOrder())
|
order := openOrder.GetOrder()
|
||||||
|
s.logger.Infof("[Recover] found open order #%d is not in the active orderbook, adding...", order.OrderID)
|
||||||
|
activeOrderBook.Add(order)
|
||||||
// also add open orders into active order's twin orderbook, we will use this active orderbook to recover empty price grid
|
// also add open orders into active order's twin orderbook, we will use this active orderbook to recover empty price grid
|
||||||
activeOrdersInTwinOrderBook.AddTwinOrder(v, openOrder)
|
activeOrdersInTwinOrderBook.AddTwinOrder(v, openOrder)
|
||||||
continue
|
continue
|
||||||
|
@ -135,7 +141,18 @@ func (s *Strategy) recover(ctx context.Context) error {
|
||||||
|
|
||||||
// case 2
|
// case 2
|
||||||
if openOrderID == 0 {
|
if openOrderID == 0 {
|
||||||
syncActiveOrder(ctx, activeOrderBook, s.orderQueryService, activeOrder.GetOrder().OrderID)
|
order := activeOrder.GetOrder()
|
||||||
|
s.logger.Infof("[Recover] found active order #%d is not in the open orders, updating...", order.OrderID)
|
||||||
|
isActiveOrderBookUpdated, err := syncActiveOrder(ctx, activeOrderBook, s.orderQueryService, order.OrderID, syncBefore)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Errorf("[Recover] unable to query order #%d", order.OrderID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isActiveOrderBookUpdated {
|
||||||
|
s.logger.Infof("[Recover] active order #%d is updated in 3 min, skip updating...", order.OrderID)
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,19 +267,24 @@ func buildTwinOrderBook(pins []Pin, orders []types.Order) (*TwinOrderBook, error
|
||||||
return book, nil
|
return book, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncActiveOrder(ctx context.Context, activeOrderBook *bbgo.ActiveOrderBook, orderQueryService types.ExchangeOrderQueryService, orderID uint64) error {
|
func syncActiveOrder(ctx context.Context, activeOrderBook *bbgo.ActiveOrderBook, orderQueryService types.ExchangeOrderQueryService, orderID uint64, syncBefore time.Time) (bool, error) {
|
||||||
updatedOrder, err := retry.QueryOrderUntilSuccessful(ctx, orderQueryService, types.OrderQuery{
|
updatedOrder, err := retry.QueryOrderUntilSuccessful(ctx, orderQueryService, types.OrderQuery{
|
||||||
Symbol: activeOrderBook.Symbol,
|
Symbol: activeOrderBook.Symbol,
|
||||||
OrderID: strconv.FormatUint(orderID, 10),
|
OrderID: strconv.FormatUint(orderID, 10),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isActiveOrderBookUpdated := false
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return isActiveOrderBookUpdated, err
|
||||||
}
|
}
|
||||||
|
|
||||||
activeOrderBook.Update(*updatedOrder)
|
isActiveOrderBookUpdated = updatedOrder.UpdateTime.Before(syncBefore)
|
||||||
|
if isActiveOrderBookUpdated {
|
||||||
|
activeOrderBook.Update(*updatedOrder)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return isActiveOrderBookUpdated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryTradesToUpdateTwinOrderBook(
|
func queryTradesToUpdateTwinOrderBook(
|
||||||
|
|
|
@ -114,7 +114,8 @@ func TestSyncActiveOrder(t *testing.T) {
|
||||||
OrderID: strconv.FormatUint(order.OrderID, 10),
|
OrderID: strconv.FormatUint(order.OrderID, 10),
|
||||||
}).Return(&updatedOrder, nil)
|
}).Return(&updatedOrder, nil)
|
||||||
|
|
||||||
if !assert.NoError(syncActiveOrder(ctx, activeOrderbook, mockOrderQueryService, order.OrderID)) {
|
_, err := syncActiveOrder(ctx, activeOrderbook, mockOrderQueryService, order.OrderID, time.Now())
|
||||||
|
if !assert.NoError(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +145,8 @@ func TestSyncActiveOrder(t *testing.T) {
|
||||||
OrderID: strconv.FormatUint(order.OrderID, 10),
|
OrderID: strconv.FormatUint(order.OrderID, 10),
|
||||||
}).Return(&updatedOrder, nil)
|
}).Return(&updatedOrder, nil)
|
||||||
|
|
||||||
if !assert.NoError(syncActiveOrder(ctx, activeOrderbook, mockOrderQueryService, order.OrderID)) {
|
_, err := syncActiveOrder(ctx, activeOrderbook, mockOrderQueryService, order.OrderID, time.Now())
|
||||||
|
if !assert.NoError(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -177,6 +177,7 @@ type Strategy struct {
|
||||||
|
|
||||||
GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"`
|
GridProfitStats *GridProfitStats `persistence:"grid_profit_stats"`
|
||||||
Position *types.Position `persistence:"position"`
|
Position *types.Position `persistence:"position"`
|
||||||
|
PersistenceTTL types.Duration `json:"persistenceTTL"`
|
||||||
|
|
||||||
// ExchangeSession is an injection field
|
// ExchangeSession is an injection field
|
||||||
ExchangeSession *bbgo.ExchangeSession
|
ExchangeSession *bbgo.ExchangeSession
|
||||||
|
@ -796,6 +797,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
|
||||||
if numberOfSellOrders > 0 {
|
if numberOfSellOrders > 0 {
|
||||||
numberOfSellOrders--
|
numberOfSellOrders--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.logger.Infof("calculated number of sell orders: %d", numberOfSellOrders)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders
|
// if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders
|
||||||
|
@ -810,8 +813,12 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
|
||||||
s.Market.MinQuantity)
|
s.Market.MinQuantity)
|
||||||
|
|
||||||
if baseQuantity.Compare(minBaseQuantity) <= 0 {
|
if baseQuantity.Compare(minBaseQuantity) <= 0 {
|
||||||
|
s.logger.Infof("base quantity %s is less than min base quantity: %s, adjusting...", baseQuantity.String(), minBaseQuantity.String())
|
||||||
|
|
||||||
baseQuantity = s.Market.RoundUpQuantityByPrecision(minBaseQuantity)
|
baseQuantity = s.Market.RoundUpQuantityByPrecision(minBaseQuantity)
|
||||||
numberOfSellOrders = int(math.Floor(baseInvestment.Div(baseQuantity).Float64()))
|
numberOfSellOrders = int(math.Floor(baseInvestment.Div(baseQuantity).Float64()))
|
||||||
|
|
||||||
|
s.logger.Infof("adjusted base quantity to %s", baseQuantity.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Infof("grid base investment sell orders: %d", numberOfSellOrders)
|
s.logger.Infof("grid base investment sell orders: %d", numberOfSellOrders)
|
||||||
|
@ -824,7 +831,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
|
||||||
// quoteInvestment = (p1 + p2 + p3) * q
|
// quoteInvestment = (p1 + p2 + p3) * q
|
||||||
// maxBuyQuantity = quoteInvestment / (p1 + p2 + p3)
|
// maxBuyQuantity = quoteInvestment / (p1 + p2 + p3)
|
||||||
si := -1
|
si := -1
|
||||||
for i := len(pins) - 1 - numberOfSellOrders; i >= 0; i-- {
|
end := len(pins) - 1
|
||||||
|
for i := end - numberOfSellOrders - 1; i >= 0; i-- {
|
||||||
pin := pins[i]
|
pin := pins[i]
|
||||||
price := fixedpoint.Value(pin)
|
price := fixedpoint.Value(pin)
|
||||||
|
|
||||||
|
@ -844,6 +852,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
|
||||||
// requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice))
|
// requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice))
|
||||||
totalQuotePrice = totalQuotePrice.Add(nextLowerPrice)
|
totalQuotePrice = totalQuotePrice.Add(nextLowerPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// for orders that buy
|
// for orders that buy
|
||||||
if s.ProfitSpread.IsZero() && i+1 == si {
|
if s.ProfitSpread.IsZero() && i+1 == si {
|
||||||
|
@ -851,7 +860,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
|
||||||
}
|
}
|
||||||
|
|
||||||
// should never place a buy order at the upper price
|
// should never place a buy order at the upper price
|
||||||
if i == len(pins)-1 {
|
if i == end {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -859,8 +868,11 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.logger.Infof("total quote price: %f", totalQuotePrice.Float64())
|
||||||
if totalQuotePrice.Sign() > 0 && quoteInvestment.Sign() > 0 {
|
if totalQuotePrice.Sign() > 0 && quoteInvestment.Sign() > 0 {
|
||||||
quoteSideQuantity := quoteInvestment.Div(totalQuotePrice)
|
quoteSideQuantity := quoteInvestment.Div(totalQuotePrice)
|
||||||
|
|
||||||
|
s.logger.Infof("quote side quantity: %f = %f / %f", quoteSideQuantity.Float64(), quoteInvestment.Float64(), totalQuotePrice.Float64())
|
||||||
if numberOfSellOrders > 0 {
|
if numberOfSellOrders > 0 {
|
||||||
return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil
|
return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil
|
||||||
}
|
}
|
||||||
|
@ -1058,6 +1070,11 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession)
|
||||||
return err2
|
return err2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.BaseGridNum > 0 {
|
||||||
|
sell1 := fixedpoint.Value(s.grid.Pins[len(s.grid.Pins)-1-s.BaseGridNum])
|
||||||
|
lastPrice = sell1.Sub(s.Market.TickSize)
|
||||||
|
}
|
||||||
|
|
||||||
// check if base and quote are enough
|
// check if base and quote are enough
|
||||||
var totalBase = fixedpoint.Zero
|
var totalBase = fixedpoint.Zero
|
||||||
var totalQuote = fixedpoint.Zero
|
var totalQuote = fixedpoint.Zero
|
||||||
|
@ -1432,6 +1449,8 @@ func calculateMinimalQuoteInvestment(market types.Market, grid *Grid) fixedpoint
|
||||||
for i := len(pins) - 2; i >= 0; i-- {
|
for i := len(pins) - 2; i >= 0; i-- {
|
||||||
pin := pins[i]
|
pin := pins[i]
|
||||||
price := fixedpoint.Value(pin)
|
price := fixedpoint.Value(pin)
|
||||||
|
|
||||||
|
// TODO: should we round the quote here before adding?
|
||||||
totalQuote = totalQuote.Add(price.Mul(minQuantity))
|
totalQuote = totalQuote.Add(price.Mul(minQuantity))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1817,13 +1836,17 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
||||||
s.ProfitSpread = s.Market.TruncatePrice(s.ProfitSpread)
|
s.ProfitSpread = s.Market.TruncatePrice(s.ProfitSpread)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.logger.Infof("persistence ttl: %s", s.PersistenceTTL.Duration())
|
||||||
|
|
||||||
if s.GridProfitStats == nil {
|
if s.GridProfitStats == nil {
|
||||||
s.GridProfitStats = newGridProfitStats(s.Market)
|
s.GridProfitStats = newGridProfitStats(s.Market)
|
||||||
}
|
}
|
||||||
|
s.GridProfitStats.SetTTL(s.PersistenceTTL.Duration())
|
||||||
|
|
||||||
if s.Position == nil {
|
if s.Position == nil {
|
||||||
s.Position = types.NewPositionFromMarket(s.Market)
|
s.Position = types.NewPositionFromMarket(s.Market)
|
||||||
}
|
}
|
||||||
|
s.Position.SetTTL(s.PersistenceTTL.Duration())
|
||||||
|
|
||||||
// initialize and register prometheus metrics
|
// initialize and register prometheus metrics
|
||||||
if s.PrometheusLabels != nil {
|
if s.PrometheusLabels != nil {
|
||||||
|
|
|
@ -204,6 +204,123 @@ func TestStrategy_generateGridOrders(t *testing.T) {
|
||||||
}, orders)
|
}, orders)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("base and quote with predefined base grid num", func(t *testing.T) {
|
||||||
|
gridNum := int64(22)
|
||||||
|
upperPrice := number(35500.000000)
|
||||||
|
lowerPrice := number(34450.000000)
|
||||||
|
quoteInvestment := number(18.47)
|
||||||
|
baseInvestment := number(0.010700)
|
||||||
|
lastPrice := number(34522.930000)
|
||||||
|
baseGridNum := int(20)
|
||||||
|
|
||||||
|
s := newTestStrategy()
|
||||||
|
s.GridNum = gridNum
|
||||||
|
s.BaseGridNum = baseGridNum
|
||||||
|
s.LowerPrice = lowerPrice
|
||||||
|
s.UpperPrice = upperPrice
|
||||||
|
s.grid = NewGrid(lowerPrice, upperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
|
||||||
|
s.grid.CalculateArithmeticPins()
|
||||||
|
assert.Equal(t, 22, len(s.grid.Pins))
|
||||||
|
|
||||||
|
quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, s.grid.Pins)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "0.000535", quantity.String())
|
||||||
|
|
||||||
|
s.QuantityOrAmount.Quantity = quantity
|
||||||
|
|
||||||
|
orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if !assert.Equal(t, 21, len(orders)) {
|
||||||
|
for _, o := range orders {
|
||||||
|
t.Logf("- %s %s", o.Price.String(), o.Side)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertPriceSide(t, []PriceSideAssert{
|
||||||
|
{number(35500.0), types.SideTypeSell},
|
||||||
|
{number(35450.0), types.SideTypeSell},
|
||||||
|
{number(35400.0), types.SideTypeSell},
|
||||||
|
{number(35350.0), types.SideTypeSell},
|
||||||
|
{number(35300.0), types.SideTypeSell},
|
||||||
|
{number(35250.0), types.SideTypeSell},
|
||||||
|
{number(35200.0), types.SideTypeSell},
|
||||||
|
{number(35150.0), types.SideTypeSell},
|
||||||
|
{number(35100.0), types.SideTypeSell},
|
||||||
|
{number(35050.0), types.SideTypeSell},
|
||||||
|
{number(35000.0), types.SideTypeSell},
|
||||||
|
{number(34950.0), types.SideTypeSell},
|
||||||
|
{number(34900.0), types.SideTypeSell},
|
||||||
|
{number(34850.0), types.SideTypeSell},
|
||||||
|
{number(34800.0), types.SideTypeSell},
|
||||||
|
{number(34750.0), types.SideTypeSell},
|
||||||
|
{number(34700.0), types.SideTypeSell},
|
||||||
|
{number(34650.0), types.SideTypeSell},
|
||||||
|
{number(34600.0), types.SideTypeSell},
|
||||||
|
{number(34550.0), types.SideTypeSell},
|
||||||
|
// -- fake trade price at 34549.9
|
||||||
|
// -- 34500 should be empty
|
||||||
|
{number(34450.0), types.SideTypeBuy},
|
||||||
|
}, orders)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("base and quote", func(t *testing.T) {
|
||||||
|
gridNum := int64(22)
|
||||||
|
upperPrice := number(35500.000000)
|
||||||
|
lowerPrice := number(34450.000000)
|
||||||
|
quoteInvestment := number(20.0)
|
||||||
|
baseInvestment := number(0.010700)
|
||||||
|
lastPrice := number(34522.930000)
|
||||||
|
baseGridNum := int(0)
|
||||||
|
|
||||||
|
s := newTestStrategy()
|
||||||
|
s.GridNum = gridNum
|
||||||
|
s.BaseGridNum = baseGridNum
|
||||||
|
s.LowerPrice = lowerPrice
|
||||||
|
s.UpperPrice = upperPrice
|
||||||
|
s.grid = NewGrid(lowerPrice, upperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
|
||||||
|
s.grid.CalculateArithmeticPins()
|
||||||
|
assert.Equal(t, 22, len(s.grid.Pins))
|
||||||
|
|
||||||
|
quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, s.grid.Pins)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "0.00029006", quantity.String())
|
||||||
|
|
||||||
|
s.QuantityOrAmount.Quantity = quantity
|
||||||
|
|
||||||
|
orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if !assert.Equal(t, 21, len(orders)) {
|
||||||
|
for _, o := range orders {
|
||||||
|
t.Logf("- %s %s", o.Price.String(), o.Side)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertPriceSide(t, []PriceSideAssert{
|
||||||
|
{number(35500.0), types.SideTypeSell},
|
||||||
|
{number(35450.0), types.SideTypeSell},
|
||||||
|
{number(35400.0), types.SideTypeSell},
|
||||||
|
{number(35350.0), types.SideTypeSell},
|
||||||
|
{number(35300.0), types.SideTypeSell},
|
||||||
|
{number(35250.0), types.SideTypeSell},
|
||||||
|
{number(35200.0), types.SideTypeSell},
|
||||||
|
{number(35150.0), types.SideTypeSell},
|
||||||
|
{number(35100.0), types.SideTypeSell},
|
||||||
|
{number(35050.0), types.SideTypeSell},
|
||||||
|
{number(35000.0), types.SideTypeSell},
|
||||||
|
{number(34950.0), types.SideTypeSell},
|
||||||
|
{number(34900.0), types.SideTypeSell},
|
||||||
|
{number(34850.0), types.SideTypeSell},
|
||||||
|
{number(34800.0), types.SideTypeSell},
|
||||||
|
{number(34750.0), types.SideTypeSell},
|
||||||
|
{number(34700.0), types.SideTypeSell},
|
||||||
|
{number(34650.0), types.SideTypeSell},
|
||||||
|
{number(34600.0), types.SideTypeSell},
|
||||||
|
{number(34550.0), types.SideTypeSell},
|
||||||
|
// -- 34500 should be empty
|
||||||
|
{number(34450.0), types.SideTypeBuy},
|
||||||
|
}, orders)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("base and quote with pre-calculated baseGridNumber", func(t *testing.T) {
|
t.Run("base and quote with pre-calculated baseGridNumber", func(t *testing.T) {
|
||||||
s := newTestStrategy()
|
s := newTestStrategy()
|
||||||
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
|
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
|
||||||
|
@ -519,11 +636,11 @@ func newTestMarket(symbol string) types.Market {
|
||||||
BaseCurrency: "BTC",
|
BaseCurrency: "BTC",
|
||||||
QuoteCurrency: "USDT",
|
QuoteCurrency: "USDT",
|
||||||
TickSize: number(0.01),
|
TickSize: number(0.01),
|
||||||
StepSize: number(0.00001),
|
StepSize: number(0.000001),
|
||||||
PricePrecision: 2,
|
PricePrecision: 2,
|
||||||
VolumePrecision: 8,
|
VolumePrecision: 8,
|
||||||
MinNotional: number(10.0),
|
MinNotional: number(8.0),
|
||||||
MinQuantity: number(0.001),
|
MinQuantity: number(0.0003),
|
||||||
}
|
}
|
||||||
case "ETHUSDT":
|
case "ETHUSDT":
|
||||||
return types.Market{
|
return types.Market{
|
||||||
|
@ -534,7 +651,7 @@ func newTestMarket(symbol string) types.Market {
|
||||||
PricePrecision: 2,
|
PricePrecision: 2,
|
||||||
VolumePrecision: 6,
|
VolumePrecision: 6,
|
||||||
MinNotional: number(8.000),
|
MinNotional: number(8.000),
|
||||||
MinQuantity: number(0.00030),
|
MinQuantity: number(0.0046),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -577,12 +694,17 @@ func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.O
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestStrategy() *Strategy {
|
func newTestStrategy(va ...string) *Strategy {
|
||||||
market := newTestMarket("BTCUSDT")
|
symbol := "BTCUSDT"
|
||||||
|
|
||||||
|
if len(va) > 0 {
|
||||||
|
symbol = va[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
market := newTestMarket(symbol)
|
||||||
s := &Strategy{
|
s := &Strategy{
|
||||||
logger: logrus.NewEntry(logrus.New()),
|
logger: logrus.NewEntry(logrus.New()),
|
||||||
Symbol: "BTCUSDT",
|
Symbol: symbol,
|
||||||
Market: market,
|
Market: market,
|
||||||
GridProfitStats: newGridProfitStats(market),
|
GridProfitStats: newGridProfitStats(market),
|
||||||
UpperPrice: number(20_000),
|
UpperPrice: number(20_000),
|
||||||
|
@ -790,7 +912,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
|
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
|
||||||
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) {
|
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(
|
||||||
|
ctx context.Context, order types.SubmitOrder,
|
||||||
|
) (types.OrderSlice, error) {
|
||||||
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
||||||
return []types.Order{
|
return []types.Order{
|
||||||
{SubmitOrder: expectedSubmitOrder},
|
{SubmitOrder: expectedSubmitOrder},
|
||||||
|
@ -858,7 +982,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
|
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
|
||||||
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) {
|
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(
|
||||||
|
ctx context.Context, order types.SubmitOrder,
|
||||||
|
) (types.OrderSlice, error) {
|
||||||
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
||||||
return []types.Order{
|
return []types.Order{
|
||||||
{SubmitOrder: expectedSubmitOrder},
|
{SubmitOrder: expectedSubmitOrder},
|
||||||
|
@ -946,7 +1072,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
Market: s.Market,
|
Market: s.Market,
|
||||||
Tag: orderTag,
|
Tag: orderTag,
|
||||||
}
|
}
|
||||||
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) {
|
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(
|
||||||
|
ctx context.Context, order types.SubmitOrder,
|
||||||
|
) (types.OrderSlice, error) {
|
||||||
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
||||||
return []types.Order{
|
return []types.Order{
|
||||||
{SubmitOrder: expectedSubmitOrder},
|
{SubmitOrder: expectedSubmitOrder},
|
||||||
|
@ -963,7 +1091,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
Market: s.Market,
|
Market: s.Market,
|
||||||
Tag: orderTag,
|
Tag: orderTag,
|
||||||
}
|
}
|
||||||
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) {
|
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(
|
||||||
|
ctx context.Context, order types.SubmitOrder,
|
||||||
|
) (types.OrderSlice, error) {
|
||||||
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2)
|
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2)
|
||||||
return []types.Order{
|
return []types.Order{
|
||||||
{SubmitOrder: expectedSubmitOrder2},
|
{SubmitOrder: expectedSubmitOrder2},
|
||||||
|
@ -1060,7 +1190,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
|
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
|
||||||
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) {
|
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(
|
||||||
|
ctx context.Context, order types.SubmitOrder,
|
||||||
|
) (types.OrderSlice, error) {
|
||||||
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
|
||||||
return []types.Order{
|
return []types.Order{
|
||||||
{SubmitOrder: expectedSubmitOrder},
|
{SubmitOrder: expectedSubmitOrder},
|
||||||
|
@ -1078,7 +1210,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
Tag: orderTag,
|
Tag: orderTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) {
|
orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(
|
||||||
|
ctx context.Context, order types.SubmitOrder,
|
||||||
|
) (types.OrderSlice, error) {
|
||||||
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2)
|
assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2)
|
||||||
return []types.Order{
|
return []types.Order{
|
||||||
{SubmitOrder: expectedSubmitOrder2},
|
{SubmitOrder: expectedSubmitOrder2},
|
||||||
|
@ -1190,14 +1324,14 @@ func TestStrategy_aggregateOrderQuoteAmountAndFeeRetry(t *testing.T) {
|
||||||
func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
|
func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
|
||||||
|
|
||||||
t.Run("7 grids", func(t *testing.T) {
|
t.Run("7 grids", func(t *testing.T) {
|
||||||
s := newTestStrategy()
|
s := newTestStrategy("ETHUSDT")
|
||||||
s.UpperPrice = number(1660)
|
s.UpperPrice = number(1660)
|
||||||
s.LowerPrice = number(1630)
|
s.LowerPrice = number(1630)
|
||||||
s.QuoteInvestment = number(61)
|
s.QuoteInvestment = number(61)
|
||||||
s.GridNum = 7
|
s.GridNum = 7
|
||||||
grid := s.newGrid()
|
grid := s.newGrid()
|
||||||
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid)
|
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid)
|
||||||
assert.InDelta(t, 60.46, minQuoteInvestment.Float64(), 0.01)
|
assert.InDelta(t, 48.36, minQuoteInvestment.Float64(), 0.01)
|
||||||
|
|
||||||
err := s.checkMinimalQuoteInvestment(grid)
|
err := s.checkMinimalQuoteInvestment(grid)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1207,12 +1341,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
|
||||||
s := newTestStrategy()
|
s := newTestStrategy()
|
||||||
// 10_000 * 0.001 = 10USDT
|
// 10_000 * 0.001 = 10USDT
|
||||||
// 20_000 * 0.001 = 20USDT
|
// 20_000 * 0.001 = 20USDT
|
||||||
// hence we should have at least: 20USDT * 10 grids
|
|
||||||
s.QuoteInvestment = number(10_000)
|
s.QuoteInvestment = number(10_000)
|
||||||
s.GridNum = 10
|
s.GridNum = 10
|
||||||
grid := s.newGrid()
|
grid := s.newGrid()
|
||||||
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid)
|
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid)
|
||||||
assert.InDelta(t, 129.9999, minQuoteInvestment.Float64(), 0.01)
|
assert.InDelta(t, 103.999, minQuoteInvestment.Float64(), 0.01)
|
||||||
|
|
||||||
err := s.checkMinimalQuoteInvestment(grid)
|
err := s.checkMinimalQuoteInvestment(grid)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -1225,11 +1358,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
|
||||||
|
|
||||||
grid := s.newGrid()
|
grid := s.newGrid()
|
||||||
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid)
|
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid)
|
||||||
assert.InDelta(t, 14979.995499, minQuoteInvestment.Float64(), 0.001)
|
assert.InDelta(t, 11983.996400, minQuoteInvestment.Float64(), 0.001)
|
||||||
|
|
||||||
err := s.checkMinimalQuoteInvestment(grid)
|
err := s.checkMinimalQuoteInvestment(grid)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.EqualError(t, err, "need at least 14979.995500 USDT for quote investment, 10000.000000 USDT given")
|
assert.EqualError(t, err, "need at least 11983.996400 USDT for quote investment, 10000.000000 USDT given")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
96
pkg/strategy/liquiditymaker/generator.go
Normal file
96
pkg/strategy/liquiditymaker/generator.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package liquiditymaker
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// input: liquidityOrderGenerator(
|
||||||
|
//
|
||||||
|
// totalLiquidityAmount,
|
||||||
|
// startPrice,
|
||||||
|
// endPrice,
|
||||||
|
// numLayers,
|
||||||
|
// quantityScale)
|
||||||
|
//
|
||||||
|
// when side == sell
|
||||||
|
//
|
||||||
|
// priceAsk1 * scale(1) * f = amount1
|
||||||
|
// priceAsk2 * scale(2) * f = amount2
|
||||||
|
// priceAsk3 * scale(3) * f = amount3
|
||||||
|
//
|
||||||
|
// totalLiquidityAmount = priceAsk1 * scale(1) * f + priceAsk2 * scale(2) * f + priceAsk3 * scale(3) * f + ....
|
||||||
|
// totalLiquidityAmount = f * (priceAsk1 * scale(1) + priceAsk2 * scale(2) + priceAsk3 * scale(3) + ....)
|
||||||
|
// f = totalLiquidityAmount / (priceAsk1 * scale(1) + priceAsk2 * scale(2) + priceAsk3 * scale(3) + ....)
|
||||||
|
//
|
||||||
|
// when side == buy
|
||||||
|
//
|
||||||
|
// priceBid1 * scale(1) * f = amount1
|
||||||
|
type LiquidityOrderGenerator struct {
|
||||||
|
Symbol string
|
||||||
|
Market types.Market
|
||||||
|
|
||||||
|
logger log.FieldLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *LiquidityOrderGenerator) Generate(
|
||||||
|
side types.SideType, totalAmount, startPrice, endPrice fixedpoint.Value, numLayers int, scale bbgo.Scale,
|
||||||
|
) (orders []types.SubmitOrder) {
|
||||||
|
|
||||||
|
if g.logger == nil {
|
||||||
|
logger := log.New()
|
||||||
|
logger.SetLevel(log.ErrorLevel)
|
||||||
|
g.logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
layerSpread := endPrice.Sub(startPrice).Div(fixedpoint.NewFromInt(int64(numLayers - 1)))
|
||||||
|
switch side {
|
||||||
|
case types.SideTypeSell:
|
||||||
|
if layerSpread.Compare(g.Market.TickSize) < 0 {
|
||||||
|
layerSpread = g.Market.TickSize
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
if layerSpread.Compare(g.Market.TickSize.Neg()) > 0 {
|
||||||
|
layerSpread = g.Market.TickSize.Neg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quantityBase := 0.0
|
||||||
|
var layerPrices []fixedpoint.Value
|
||||||
|
var layerScales []float64
|
||||||
|
for i := 0; i < numLayers; i++ {
|
||||||
|
fi := fixedpoint.NewFromInt(int64(i))
|
||||||
|
layerPrice := g.Market.TruncatePrice(startPrice.Add(layerSpread.Mul(fi)))
|
||||||
|
layerPrices = append(layerPrices, layerPrice)
|
||||||
|
|
||||||
|
layerScale := scale.Call(float64(i + 1))
|
||||||
|
layerScales = append(layerScales, layerScale)
|
||||||
|
|
||||||
|
quantityBase += layerPrice.Float64() * layerScale
|
||||||
|
}
|
||||||
|
|
||||||
|
factor := totalAmount.Float64() / quantityBase
|
||||||
|
|
||||||
|
g.logger.Infof("liquidity amount base: %f, factor: %f", quantityBase, factor)
|
||||||
|
|
||||||
|
for i := 0; i < numLayers; i++ {
|
||||||
|
price := layerPrices[i]
|
||||||
|
s := layerScales[i]
|
||||||
|
|
||||||
|
quantity := factor * s
|
||||||
|
orders = append(orders, types.SubmitOrder{
|
||||||
|
Symbol: g.Symbol,
|
||||||
|
Price: price,
|
||||||
|
Type: types.OrderTypeLimitMaker,
|
||||||
|
Quantity: g.Market.TruncateQuantity(fixedpoint.NewFromFloat(quantity)),
|
||||||
|
Side: side,
|
||||||
|
Market: g.Market,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return orders
|
||||||
|
}
|
114
pkg/strategy/liquiditymaker/generator_test.go
Normal file
114
pkg/strategy/liquiditymaker/generator_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
//go:build !dnum
|
||||||
|
|
||||||
|
package liquiditymaker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
. "github.com/c9s/bbgo/pkg/testing/testhelper"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestMarket() types.Market {
|
||||||
|
return types.Market{
|
||||||
|
BaseCurrency: "XML",
|
||||||
|
QuoteCurrency: "USDT",
|
||||||
|
TickSize: Number(0.0001),
|
||||||
|
StepSize: Number(0.01),
|
||||||
|
PricePrecision: 4,
|
||||||
|
VolumePrecision: 8,
|
||||||
|
MinNotional: Number(8.0),
|
||||||
|
MinQuantity: Number(40.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiquidityOrderGenerator(t *testing.T) {
|
||||||
|
g := &LiquidityOrderGenerator{
|
||||||
|
Symbol: "XMLUSDT",
|
||||||
|
Market: newTestMarket(),
|
||||||
|
}
|
||||||
|
|
||||||
|
scale := &bbgo.ExponentialScale{
|
||||||
|
Domain: [2]float64{1.0, 30.0},
|
||||||
|
Range: [2]float64{1.0, 4.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scale.Solve()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.InDelta(t, 1.0, scale.Call(1.0), 0.00001)
|
||||||
|
assert.InDelta(t, 4.0, scale.Call(30.0), 0.00001)
|
||||||
|
|
||||||
|
totalAmount := Number(20_000.0)
|
||||||
|
|
||||||
|
t.Run("ask orders", func(t *testing.T) {
|
||||||
|
orders := g.Generate(types.SideTypeSell, totalAmount, Number(2.0), Number(2.04), 30, scale)
|
||||||
|
assert.Len(t, orders, 30)
|
||||||
|
|
||||||
|
totalQuoteQuantity := fixedpoint.NewFromInt(0)
|
||||||
|
for _, o := range orders {
|
||||||
|
totalQuoteQuantity = totalQuoteQuantity.Add(o.Quantity.Mul(o.Price))
|
||||||
|
}
|
||||||
|
assert.InDelta(t, totalAmount.Float64(), totalQuoteQuantity.Float64(), 1.0)
|
||||||
|
|
||||||
|
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0000"), Quantity: Number("151.34")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0013"), Quantity: Number("158.75")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0027"), Quantity: Number("166.52")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0041"), Quantity: Number("174.67")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0055"), Quantity: Number("183.23")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0068"), Quantity: Number("192.20")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0082"), Quantity: Number("201.61")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0096"), Quantity: Number("211.48")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0110"), Quantity: Number("221.84")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0124"), Quantity: Number("232.70")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0137"), Quantity: Number("244.09")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0151"), Quantity: Number("256.04")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0165"), Quantity: Number("268.58")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0179"), Quantity: Number("281.73")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0193"), Quantity: Number("295.53")},
|
||||||
|
}, orders[0:15])
|
||||||
|
|
||||||
|
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0386"), Quantity: Number("577.10")},
|
||||||
|
{Side: types.SideTypeSell, Price: Number("2.0399"), Quantity: Number("605.36")},
|
||||||
|
}, orders[28:30])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bid orders", func(t *testing.T) {
|
||||||
|
orders := g.Generate(types.SideTypeBuy, totalAmount, Number(2.0), Number(1.96), 30, scale)
|
||||||
|
assert.Len(t, orders, 30)
|
||||||
|
|
||||||
|
totalQuoteQuantity := fixedpoint.NewFromInt(0)
|
||||||
|
for _, o := range orders {
|
||||||
|
totalQuoteQuantity = totalQuoteQuantity.Add(o.Quantity.Mul(o.Price))
|
||||||
|
}
|
||||||
|
assert.InDelta(t, totalAmount.Float64(), totalQuoteQuantity.Float64(), 1.0)
|
||||||
|
|
||||||
|
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("2.0000"), Quantity: Number("155.13")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9986"), Quantity: Number("162.73")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9972"), Quantity: Number("170.70")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9958"), Quantity: Number("179.05")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9944"), Quantity: Number("187.82")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9931"), Quantity: Number("197.02")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9917"), Quantity: Number("206.67")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9903"), Quantity: Number("216.79")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9889"), Quantity: Number("227.40")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9875"), Quantity: Number("238.54")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9862"), Quantity: Number("250.22")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9848"), Quantity: Number("262.47")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9834"), Quantity: Number("275.32")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9820"), Quantity: Number("288.80")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9806"), Quantity: Number("302.94")},
|
||||||
|
}, orders[0:15])
|
||||||
|
|
||||||
|
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9613"), Quantity: Number("591.58")},
|
||||||
|
{Side: types.SideTypeBuy, Price: Number("1.9600"), Quantity: Number("620.54")},
|
||||||
|
}, orders[28:30])
|
||||||
|
})
|
||||||
|
}
|
378
pkg/strategy/liquiditymaker/strategy.go
Normal file
378
pkg/strategy/liquiditymaker/strategy.go
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
package liquiditymaker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
. "github.com/c9s/bbgo/pkg/indicator/v2"
|
||||||
|
"github.com/c9s/bbgo/pkg/strategy/common"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ID = "liquiditymaker"
|
||||||
|
|
||||||
|
type advancedOrderCancelApi interface {
|
||||||
|
CancelAllOrders(ctx context.Context) ([]types.Order, error)
|
||||||
|
CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy is the strategy struct of LiquidityMaker
|
||||||
|
// liquidity maker does not care about the current price, it tries to place liquidity orders (limit maker orders)
|
||||||
|
// around the current mid price
|
||||||
|
// liquidity maker's target:
|
||||||
|
// - place enough total liquidity amount on the order book, for example, 20k USDT value liquidity on both sell and buy
|
||||||
|
// - ensure the spread by placing the orders from the mid price (or the last trade price)
|
||||||
|
type Strategy struct {
|
||||||
|
*common.Strategy
|
||||||
|
|
||||||
|
Environment *bbgo.Environment
|
||||||
|
Market types.Market
|
||||||
|
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
|
||||||
|
LiquidityUpdateInterval types.Interval `json:"liquidityUpdateInterval"`
|
||||||
|
|
||||||
|
AdjustmentUpdateInterval types.Interval `json:"adjustmentUpdateInterval"`
|
||||||
|
|
||||||
|
NumOfLiquidityLayers int `json:"numOfLiquidityLayers"`
|
||||||
|
LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"`
|
||||||
|
LiquidityPriceRange fixedpoint.Value `json:"liquidityPriceRange"`
|
||||||
|
AskLiquidityAmount fixedpoint.Value `json:"askLiquidityAmount"`
|
||||||
|
BidLiquidityAmount fixedpoint.Value `json:"bidLiquidityAmount"`
|
||||||
|
|
||||||
|
UseLastTradePrice bool `json:"useLastTradePrice"`
|
||||||
|
Spread fixedpoint.Value `json:"spread"`
|
||||||
|
MaxPrice fixedpoint.Value `json:"maxPrice"`
|
||||||
|
MinPrice fixedpoint.Value `json:"minPrice"`
|
||||||
|
|
||||||
|
MaxExposure fixedpoint.Value `json:"maxExposure"`
|
||||||
|
|
||||||
|
MinProfit fixedpoint.Value `json:"minProfit"`
|
||||||
|
|
||||||
|
liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook
|
||||||
|
book *types.StreamOrderBook
|
||||||
|
|
||||||
|
liquidityScale bbgo.Scale
|
||||||
|
|
||||||
|
orderGenerator *LiquidityOrderGenerator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) ID() string {
|
||||||
|
return ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) InstanceID() string {
|
||||||
|
return fmt.Sprintf("%s:%s", ID, s.Symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
|
session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.AdjustmentUpdateInterval})
|
||||||
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LiquidityUpdateInterval})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
|
s.Strategy = &common.Strategy{}
|
||||||
|
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
|
||||||
|
|
||||||
|
s.orderGenerator = &LiquidityOrderGenerator{
|
||||||
|
Symbol: s.Symbol,
|
||||||
|
Market: s.Market,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.book = types.NewStreamBook(s.Symbol)
|
||||||
|
s.book.BindStream(session.MarketDataStream)
|
||||||
|
|
||||||
|
s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
|
s.liquidityOrderBook.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
|
s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
|
s.adjustmentOrderBook.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
|
scale, err := s.LiquiditySlideRule.Scale()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scale.Solve(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cancelApi, ok := session.Exchange.(advancedOrderCancelApi); ok {
|
||||||
|
_, _ = cancelApi.CancelOrdersBySymbol(ctx, s.Symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.liquidityScale = scale
|
||||||
|
|
||||||
|
session.UserDataStream.OnStart(func() {
|
||||||
|
s.placeLiquidityOrders(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
session.MarketDataStream.OnKLineClosed(func(k types.KLine) {
|
||||||
|
if k.Interval == s.AdjustmentUpdateInterval {
|
||||||
|
s.placeAdjustmentOrders(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if k.Interval == s.LiquidityUpdateInterval {
|
||||||
|
s.placeLiquidityOrders(ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
if err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange); err != nil {
|
||||||
|
logErr(err, "unable to cancel liquidity orders")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.adjustmentOrderBook.GracefulCancel(ctx, s.Session.Exchange); err != nil {
|
||||||
|
logErr(err, "unable to cancel adjustment orders")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
|
||||||
|
_ = s.adjustmentOrderBook.GracefulCancel(ctx, s.Session.Exchange)
|
||||||
|
|
||||||
|
if s.Position.IsDust() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol)
|
||||||
|
if logErr(err, "unable to query ticker") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Session.UpdateAccount(ctx); err != nil {
|
||||||
|
logErr(err, "unable to update account")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
baseBal, _ := s.Session.Account.Balance(s.Market.BaseCurrency)
|
||||||
|
quoteBal, _ := s.Session.Account.Balance(s.Market.QuoteCurrency)
|
||||||
|
|
||||||
|
var adjOrders []types.SubmitOrder
|
||||||
|
|
||||||
|
posSize := s.Position.Base.Abs()
|
||||||
|
tickSize := s.Market.TickSize
|
||||||
|
|
||||||
|
if s.Position.IsShort() {
|
||||||
|
price := profitProtectedPrice(types.SideTypeBuy, s.Position.AverageCost, ticker.Sell.Add(tickSize.Neg()), s.Session.MakerFeeRate, s.MinProfit)
|
||||||
|
quoteQuantity := fixedpoint.Min(price.Mul(posSize), quoteBal.Available)
|
||||||
|
bidQuantity := quoteQuantity.Div(price)
|
||||||
|
|
||||||
|
if s.Market.IsDustQuantity(bidQuantity, price) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adjOrders = append(adjOrders, types.SubmitOrder{
|
||||||
|
Symbol: s.Symbol,
|
||||||
|
Type: types.OrderTypeLimitMaker,
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
Price: price,
|
||||||
|
Quantity: bidQuantity,
|
||||||
|
Market: s.Market,
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
})
|
||||||
|
} else if s.Position.IsLong() {
|
||||||
|
price := profitProtectedPrice(types.SideTypeSell, s.Position.AverageCost, ticker.Buy.Add(tickSize), s.Session.MakerFeeRate, s.MinProfit)
|
||||||
|
askQuantity := fixedpoint.Min(posSize, baseBal.Available)
|
||||||
|
|
||||||
|
if s.Market.IsDustQuantity(askQuantity, price) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adjOrders = append(adjOrders, types.SubmitOrder{
|
||||||
|
Symbol: s.Symbol,
|
||||||
|
Type: types.OrderTypeLimitMaker,
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
Price: price,
|
||||||
|
Quantity: askQuantity,
|
||||||
|
Market: s.Market,
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, adjOrders...)
|
||||||
|
if logErr(err, "unable to place liquidity orders") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.adjustmentOrderBook.Add(createdOrders...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
|
err := s.liquidityOrderBook.GracefulCancel(ctx, s.Session.Exchange)
|
||||||
|
if logErr(err, "unable to cancel orders") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker, err := s.Session.Exchange.QueryTicker(ctx, s.Symbol)
|
||||||
|
if logErr(err, "unable to query ticker") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.IsHalted(ticker.Time) {
|
||||||
|
log.Warn("circuitBreakRiskControl: trading halted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Session.UpdateAccount(ctx); err != nil {
|
||||||
|
logErr(err, "unable to update account")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
baseBal, _ := s.Session.Account.Balance(s.Market.BaseCurrency)
|
||||||
|
quoteBal, _ := s.Session.Account.Balance(s.Market.QuoteCurrency)
|
||||||
|
|
||||||
|
if ticker.Buy.IsZero() && ticker.Sell.IsZero() {
|
||||||
|
ticker.Sell = ticker.Last.Add(s.Market.TickSize)
|
||||||
|
ticker.Buy = ticker.Last.Sub(s.Market.TickSize)
|
||||||
|
} else if ticker.Buy.IsZero() {
|
||||||
|
ticker.Buy = ticker.Sell.Sub(s.Market.TickSize)
|
||||||
|
} else if ticker.Sell.IsZero() {
|
||||||
|
ticker.Sell = ticker.Buy.Add(s.Market.TickSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("ticker: %+v", ticker)
|
||||||
|
|
||||||
|
lastTradedPrice := ticker.Last
|
||||||
|
midPrice := ticker.Sell.Add(ticker.Buy).Div(fixedpoint.Two)
|
||||||
|
currentSpread := ticker.Sell.Sub(ticker.Buy)
|
||||||
|
sideSpread := s.Spread.Div(fixedpoint.Two)
|
||||||
|
|
||||||
|
if s.UseLastTradePrice {
|
||||||
|
midPrice = lastTradedPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("current spread: %f lastTradedPrice: %f midPrice: %f", currentSpread.Float64(), lastTradedPrice.Float64(), midPrice.Float64())
|
||||||
|
|
||||||
|
ask1Price := midPrice.Mul(fixedpoint.One.Add(sideSpread))
|
||||||
|
bid1Price := midPrice.Mul(fixedpoint.One.Sub(sideSpread))
|
||||||
|
|
||||||
|
askLastPrice := midPrice.Mul(fixedpoint.One.Add(s.LiquidityPriceRange))
|
||||||
|
bidLastPrice := midPrice.Mul(fixedpoint.One.Sub(s.LiquidityPriceRange))
|
||||||
|
log.Infof("wanted side spread: %f askRange: %f ~ %f bidRange: %f ~ %f",
|
||||||
|
sideSpread.Float64(),
|
||||||
|
ask1Price.Float64(), askLastPrice.Float64(),
|
||||||
|
bid1Price.Float64(), bidLastPrice.Float64())
|
||||||
|
|
||||||
|
availableBase := baseBal.Available
|
||||||
|
availableQuote := quoteBal.Available
|
||||||
|
|
||||||
|
log.Infof("balances before liq orders: %s, %s",
|
||||||
|
baseBal.String(),
|
||||||
|
quoteBal.String())
|
||||||
|
|
||||||
|
if !s.Position.IsDust() {
|
||||||
|
if s.Position.IsLong() {
|
||||||
|
availableBase = availableBase.Sub(s.Position.Base)
|
||||||
|
availableBase = s.Market.RoundDownQuantityByPrecision(availableBase)
|
||||||
|
} else if s.Position.IsShort() {
|
||||||
|
posSizeInQuote := s.Position.Base.Mul(ticker.Sell)
|
||||||
|
availableQuote = availableQuote.Sub(posSizeInQuote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bidOrders := s.orderGenerator.Generate(types.SideTypeBuy,
|
||||||
|
fixedpoint.Min(s.BidLiquidityAmount, quoteBal.Available),
|
||||||
|
bid1Price,
|
||||||
|
bidLastPrice,
|
||||||
|
s.NumOfLiquidityLayers,
|
||||||
|
s.liquidityScale)
|
||||||
|
|
||||||
|
askOrders := s.orderGenerator.Generate(types.SideTypeSell,
|
||||||
|
s.AskLiquidityAmount,
|
||||||
|
ask1Price,
|
||||||
|
askLastPrice,
|
||||||
|
s.NumOfLiquidityLayers,
|
||||||
|
s.liquidityScale)
|
||||||
|
|
||||||
|
askOrders = filterAskOrders(askOrders, baseBal.Available)
|
||||||
|
|
||||||
|
orderForms := append(bidOrders, askOrders...)
|
||||||
|
|
||||||
|
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orderForms...)
|
||||||
|
if logErr(err, "unable to place liquidity orders") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.liquidityOrderBook.Add(createdOrders...)
|
||||||
|
log.Infof("%d liq orders are placed successfully", len(orderForms))
|
||||||
|
for _, o := range createdOrders {
|
||||||
|
log.Infof("liq order: %+v", o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func profitProtectedPrice(
|
||||||
|
side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value,
|
||||||
|
) fixedpoint.Value {
|
||||||
|
switch side {
|
||||||
|
case types.SideTypeSell:
|
||||||
|
minProfitPrice := averageCost.Add(
|
||||||
|
averageCost.Mul(feeRate.Add(minProfit)))
|
||||||
|
return fixedpoint.Max(minProfitPrice, price)
|
||||||
|
|
||||||
|
case types.SideTypeBuy:
|
||||||
|
minProfitPrice := averageCost.Sub(
|
||||||
|
averageCost.Mul(feeRate.Add(minProfit)))
|
||||||
|
return fixedpoint.Min(minProfitPrice, price)
|
||||||
|
|
||||||
|
}
|
||||||
|
return price
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterAskOrders(askOrders []types.SubmitOrder, available fixedpoint.Value) (out []types.SubmitOrder) {
|
||||||
|
usedBase := fixedpoint.Zero
|
||||||
|
for _, askOrder := range askOrders {
|
||||||
|
if usedBase.Add(askOrder.Quantity).Compare(available) > 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
usedBase = usedBase.Add(askOrder.Quantity)
|
||||||
|
out = append(out, askOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func logErr(err error, msgAndArgs ...interface{}) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msgAndArgs) == 0 {
|
||||||
|
log.WithError(err).Error(err.Error())
|
||||||
|
} else if len(msgAndArgs) == 1 {
|
||||||
|
msg := msgAndArgs[0].(string)
|
||||||
|
log.WithError(err).Error(msg)
|
||||||
|
} else if len(msgAndArgs) > 1 {
|
||||||
|
msg := msgAndArgs[0].(string)
|
||||||
|
log.WithError(err).Errorf(msg, msgAndArgs[1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func preloadKLines(
|
||||||
|
inc *KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval,
|
||||||
|
) {
|
||||||
|
if store, ok := session.MarketDataStore(symbol); ok {
|
||||||
|
if kLinesData, ok := store.KLinesOfInterval(interval); ok {
|
||||||
|
for _, k := range *kLinesData {
|
||||||
|
inc.EmitUpdate(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,7 +40,6 @@ type Strategy struct {
|
||||||
DryRun bool `json:"dryRun"`
|
DryRun bool `json:"dryRun"`
|
||||||
OnStart bool `json:"onStart"` // rebalance on start
|
OnStart bool `json:"onStart"` // rebalance on start
|
||||||
|
|
||||||
session *bbgo.ExchangeSession
|
|
||||||
symbols []string
|
symbols []string
|
||||||
markets map[string]types.Market
|
markets map[string]types.Market
|
||||||
activeOrderBook *bbgo.ActiveOrderBook
|
activeOrderBook *bbgo.ActiveOrderBook
|
||||||
|
@ -97,11 +96,9 @@ func (s *Strategy) Validate() error {
|
||||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {}
|
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {}
|
||||||
|
|
||||||
func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
s.session = session
|
|
||||||
|
|
||||||
s.markets = make(map[string]types.Market)
|
s.markets = make(map[string]types.Market)
|
||||||
for _, symbol := range s.symbols {
|
for _, symbol := range s.symbols {
|
||||||
market, ok := s.session.Market(symbol)
|
market, ok := session.Market(symbol)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("market %s not found", symbol)
|
return fmt.Errorf("market %s not found", symbol)
|
||||||
}
|
}
|
||||||
|
@ -112,7 +109,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
||||||
s.MultiMarketStrategy.Initialize(ctx, s.Environment, session, s.markets, ID)
|
s.MultiMarketStrategy.Initialize(ctx, s.Environment, session, s.markets, ID)
|
||||||
|
|
||||||
s.activeOrderBook = bbgo.NewActiveOrderBook("")
|
s.activeOrderBook = bbgo.NewActiveOrderBook("")
|
||||||
s.activeOrderBook.BindStream(s.session.UserDataStream)
|
s.activeOrderBook.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
session.UserDataStream.OnStart(func() {
|
session.UserDataStream.OnStart(func() {
|
||||||
if s.OnStart {
|
if s.OnStart {
|
||||||
|
@ -137,7 +134,7 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
|
||||||
|
|
||||||
func (s *Strategy) rebalance(ctx context.Context) {
|
func (s *Strategy) rebalance(ctx context.Context) {
|
||||||
// cancel active orders before rebalance
|
// cancel active orders before rebalance
|
||||||
if err := s.session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil {
|
if err := s.Session.Exchange.CancelOrders(ctx, s.activeOrderBook.Orders()...); err != nil {
|
||||||
log.WithError(err).Errorf("failed to cancel orders")
|
log.WithError(err).Errorf("failed to cancel orders")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +171,7 @@ func (s *Strategy) queryMidPrices(ctx context.Context) (types.ValueMap, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker, err := s.session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency)
|
ticker, err := s.Session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -186,7 +183,7 @@ func (s *Strategy) queryMidPrices(ctx context.Context) (types.ValueMap, error) {
|
||||||
|
|
||||||
func (s *Strategy) selectBalances() (types.BalanceMap, error) {
|
func (s *Strategy) selectBalances() (types.BalanceMap, error) {
|
||||||
m := make(types.BalanceMap)
|
m := make(types.BalanceMap)
|
||||||
balances := s.session.GetAccount().Balances()
|
balances := s.Session.GetAccount().Balances()
|
||||||
for currency := range s.TargetWeights {
|
for currency := range s.TargetWeights {
|
||||||
balance, ok := balances[currency]
|
balance, ok := balances[currency]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -235,28 +232,36 @@ func (s *Strategy) generateOrder(ctx context.Context) (*types.SubmitOrder, error
|
||||||
quantity = quantity.Abs()
|
quantity = quantity.Abs()
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.MaxAmount.Float64() > 0 {
|
ticker, err := s.Session.Exchange.QueryTicker(ctx, symbol)
|
||||||
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, midPrice, s.MaxAmount)
|
if err != nil {
|
||||||
log.Infof("adjust quantity %s (%s %s @ %s) by max amount %s",
|
return nil, err
|
||||||
quantity.String(),
|
|
||||||
symbol,
|
|
||||||
side.String(),
|
|
||||||
midPrice.String(),
|
|
||||||
s.MaxAmount.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var price fixedpoint.Value
|
||||||
if side == types.SideTypeBuy {
|
if side == types.SideTypeBuy {
|
||||||
quantity = fixedpoint.Min(quantity, balances[s.QuoteCurrency].Available.Div(midPrice))
|
price = ticker.Buy
|
||||||
|
quantity = fixedpoint.Min(quantity, balances[s.QuoteCurrency].Available.Div(ticker.Sell))
|
||||||
} else if side == types.SideTypeSell {
|
} else if side == types.SideTypeSell {
|
||||||
|
price = ticker.Sell
|
||||||
quantity = fixedpoint.Min(quantity, balances[market.BaseCurrency].Available)
|
quantity = fixedpoint.Min(quantity, balances[market.BaseCurrency].Available)
|
||||||
}
|
}
|
||||||
|
|
||||||
if market.IsDustQuantity(quantity, midPrice) {
|
if s.MaxAmount.Float64() > 0 {
|
||||||
|
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, price, s.MaxAmount)
|
||||||
|
log.Infof("adjusted quantity %s (%s %s @ %s) by max amount %s",
|
||||||
|
quantity.String(),
|
||||||
|
symbol,
|
||||||
|
side.String(),
|
||||||
|
price.String(),
|
||||||
|
s.MaxAmount.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if market.IsDustQuantity(quantity, price) {
|
||||||
log.Infof("quantity %s (%s %s @ %s) is dust quantity, skip",
|
log.Infof("quantity %s (%s %s @ %s) is dust quantity, skip",
|
||||||
quantity.String(),
|
quantity.String(),
|
||||||
symbol,
|
symbol,
|
||||||
side.String(),
|
side.String(),
|
||||||
midPrice.String())
|
price.String())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +270,7 @@ func (s *Strategy) generateOrder(ctx context.Context) (*types.SubmitOrder, error
|
||||||
Side: side,
|
Side: side,
|
||||||
Type: s.OrderType,
|
Type: s.OrderType,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
Price: midPrice,
|
Price: price,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -5,22 +5,18 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
"github.com/c9s/bbgo/pkg/bbgo"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
. "github.com/c9s/bbgo/pkg/indicator/v2"
|
. "github.com/c9s/bbgo/pkg/indicator/v2"
|
||||||
"github.com/c9s/bbgo/pkg/risk/riskcontrol"
|
|
||||||
"github.com/c9s/bbgo/pkg/strategy/common"
|
"github.com/c9s/bbgo/pkg/strategy/common"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ID = "scmaker"
|
const ID = "scmaker"
|
||||||
|
|
||||||
var ten = fixedpoint.NewFromInt(10)
|
|
||||||
|
|
||||||
type advancedOrderCancelApi interface {
|
type advancedOrderCancelApi interface {
|
||||||
CancelAllOrders(ctx context.Context) ([]types.Order, error)
|
CancelAllOrders(ctx context.Context) ([]types.Order, error)
|
||||||
CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error)
|
CancelOrdersBySymbol(ctx context.Context, symbol string) ([]types.Order, error)
|
||||||
|
@ -62,12 +58,6 @@ type Strategy struct {
|
||||||
|
|
||||||
MinProfit fixedpoint.Value `json:"minProfit"`
|
MinProfit fixedpoint.Value `json:"minProfit"`
|
||||||
|
|
||||||
// risk related parameters
|
|
||||||
PositionHardLimit fixedpoint.Value `json:"positionHardLimit"`
|
|
||||||
MaxPositionQuantity fixedpoint.Value `json:"maxPositionQuantity"`
|
|
||||||
CircuitBreakLossThreshold fixedpoint.Value `json:"circuitBreakLossThreshold"`
|
|
||||||
CircuitBreakEMA types.IntervalWindow `json:"circuitBreakEMA"`
|
|
||||||
|
|
||||||
liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook
|
liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook
|
||||||
book *types.StreamOrderBook
|
book *types.StreamOrderBook
|
||||||
|
|
||||||
|
@ -77,9 +67,6 @@ type Strategy struct {
|
||||||
ewma *EWMAStream
|
ewma *EWMAStream
|
||||||
boll *BOLLStream
|
boll *BOLLStream
|
||||||
intensity *IntensityStream
|
intensity *IntensityStream
|
||||||
|
|
||||||
positionRiskControl *riskcontrol.PositionRiskControl
|
|
||||||
circuitBreakRiskControl *riskcontrol.CircuitBreakRiskControl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) ID() string {
|
func (s *Strategy) ID() string {
|
||||||
|
@ -100,12 +87,12 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
s.Strategy = &common.Strategy{}
|
s.Strategy = &common.Strategy{}
|
||||||
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
|
s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID())
|
||||||
|
|
||||||
s.book = types.NewStreamBook(s.Symbol)
|
s.book = types.NewStreamBook(s.Symbol)
|
||||||
s.book.BindStream(session.UserDataStream)
|
s.book.BindStream(session.MarketDataStream)
|
||||||
|
|
||||||
s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
s.liquidityOrderBook.BindStream(session.UserDataStream)
|
s.liquidityOrderBook.BindStream(session.UserDataStream)
|
||||||
|
@ -113,21 +100,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
|
||||||
s.adjustmentOrderBook.BindStream(session.UserDataStream)
|
s.adjustmentOrderBook.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() {
|
|
||||||
log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...")
|
|
||||||
s.positionRiskControl = riskcontrol.NewPositionRiskControl(s.OrderExecutor, s.PositionHardLimit, s.MaxPositionQuantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.CircuitBreakLossThreshold.IsZero() {
|
|
||||||
log.Infof("circuitBreakLossThreshold is configured, setting up CircuitBreakRiskControl...")
|
|
||||||
s.circuitBreakRiskControl = riskcontrol.NewCircuitBreakRiskControl(
|
|
||||||
s.Position,
|
|
||||||
session.Indicators(s.Symbol).EWMA(s.CircuitBreakEMA),
|
|
||||||
s.CircuitBreakLossThreshold,
|
|
||||||
s.ProfitStats,
|
|
||||||
24*time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
scale, err := s.LiquiditySlideRule.Scale()
|
scale, err := s.LiquiditySlideRule.Scale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -174,7 +146,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) preloadKLines(inc *KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval) {
|
func (s *Strategy) preloadKLines(
|
||||||
|
inc *KLineStream, session *bbgo.ExchangeSession, symbol string, interval types.Interval,
|
||||||
|
) {
|
||||||
if store, ok := session.MarketDataStore(symbol); ok {
|
if store, ok := session.MarketDataStore(symbol); ok {
|
||||||
if kLinesData, ok := store.KLinesOfInterval(interval); ok {
|
if kLinesData, ok := store.KLinesOfInterval(interval); ok {
|
||||||
for _, k := range *kLinesData {
|
for _, k := range *kLinesData {
|
||||||
|
@ -282,7 +256,7 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.circuitBreakRiskControl != nil && s.circuitBreakRiskControl.IsHalted(ticker.Time) {
|
if s.IsHalted(ticker.Time) {
|
||||||
log.Warn("circuitBreakRiskControl: trading halted")
|
log.Warn("circuitBreakRiskControl: trading halted")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -476,7 +450,9 @@ func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
|
||||||
log.Infof("%d liq orders are placed successfully", len(liqOrders))
|
log.Infof("%d liq orders are placed successfully", len(liqOrders))
|
||||||
}
|
}
|
||||||
|
|
||||||
func profitProtectedPrice(side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value) fixedpoint.Value {
|
func profitProtectedPrice(
|
||||||
|
side types.SideType, averageCost, price, feeRate, minProfit fixedpoint.Value,
|
||||||
|
) fixedpoint.Value {
|
||||||
switch side {
|
switch side {
|
||||||
case types.SideTypeSell:
|
case types.SideTypeSell:
|
||||||
minProfitPrice := averageCost.Add(
|
minProfitPrice := averageCost.Add(
|
||||||
|
|
58
pkg/testing/testhelper/assert_priceside.go
Normal file
58
pkg/testing/testhelper/assert_priceside.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package testhelper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PriceSideAssert struct {
|
||||||
|
Price fixedpoint.Value
|
||||||
|
Side types.SideType
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertOrdersPriceSide asserts the orders with the given price and side (slice)
|
||||||
|
func AssertOrdersPriceSide(t *testing.T, asserts []PriceSideAssert, orders []types.SubmitOrder) {
|
||||||
|
for i, a := range asserts {
|
||||||
|
assert.Equalf(t, a.Price, orders[i].Price, "order #%d price should be %f", i+1, a.Price.Float64())
|
||||||
|
assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriceSideQuantityAssert struct {
|
||||||
|
Price fixedpoint.Value
|
||||||
|
Side types.SideType
|
||||||
|
Quantity fixedpoint.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssertOrdersPriceSide asserts the orders with the given price and side (slice)
|
||||||
|
func AssertOrdersPriceSideQuantity(
|
||||||
|
t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder,
|
||||||
|
) {
|
||||||
|
assert.Equalf(t, len(orders), len(asserts), "expecting %d orders", len(asserts))
|
||||||
|
|
||||||
|
var assertPrices, orderPrices fixedpoint.Slice
|
||||||
|
var assertPricesFloat, orderPricesFloat []float64
|
||||||
|
for _, a := range asserts {
|
||||||
|
assertPrices = append(assertPrices, a.Price)
|
||||||
|
assertPricesFloat = append(assertPricesFloat, a.Price.Float64())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range orders {
|
||||||
|
orderPrices = append(orderPrices, o.Price)
|
||||||
|
orderPricesFloat = append(orderPricesFloat, o.Price.Float64())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assert.Equalf(t, assertPricesFloat, orderPricesFloat, "assert prices") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, a := range asserts {
|
||||||
|
assert.Equalf(t, a.Price.Float64(), orders[i].Price.Float64(), "order #%d price should be %f", i+1, a.Price.Float64())
|
||||||
|
assert.Equalf(t, a.Quantity.Float64(), orders[i].Quantity.Float64(), "order #%d quantity should be %f", i+1, a.Quantity.Float64())
|
||||||
|
assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side)
|
||||||
|
}
|
||||||
|
}
|
18
pkg/testing/testhelper/number.go
Normal file
18
pkg/testing/testhelper/number.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package testhelper
|
||||||
|
|
||||||
|
import "github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
|
||||||
|
func Number(a interface{}) fixedpoint.Value {
|
||||||
|
switch v := a.(type) {
|
||||||
|
case string:
|
||||||
|
return fixedpoint.MustNewFromString(v)
|
||||||
|
case int:
|
||||||
|
return fixedpoint.NewFromInt(int64(v))
|
||||||
|
case int64:
|
||||||
|
return fixedpoint.NewFromInt(int64(v))
|
||||||
|
case float64:
|
||||||
|
return fixedpoint.NewFromFloat(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedpoint.Zero
|
||||||
|
}
|
|
@ -65,6 +65,20 @@ type Position struct {
|
||||||
|
|
||||||
// Modify position callbacks
|
// Modify position callbacks
|
||||||
modifyCallbacks []func(baseQty fixedpoint.Value, quoteQty fixedpoint.Value, price fixedpoint.Value)
|
modifyCallbacks []func(baseQty fixedpoint.Value, quoteQty fixedpoint.Value, price fixedpoint.Value)
|
||||||
|
|
||||||
|
// ttl is the ttl to keep in persistence
|
||||||
|
ttl time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Position) SetTTL(ttl time.Duration) {
|
||||||
|
if ttl.Nanoseconds() <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ttl = ttl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Position) Expiration() time.Duration {
|
||||||
|
return s.ttl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Position) CsvHeader() []string {
|
func (p *Position) CsvHeader() []string {
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
const Version = "v1.52.0-2058ce80-dev"
|
const Version = "v1.53.0-4c701676-dev"
|
||||||
|
|
||||||
const VersionGitRef = "2058ce80"
|
const VersionGitRef = "4c701676"
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
const Version = "v1.52.0-2058ce80"
|
const Version = "v1.53.0-4c701676"
|
||||||
|
|
||||||
const VersionGitRef = "2058ce80"
|
const VersionGitRef = "4c701676"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user