diff --git a/common/common.go b/common/common.go index 382d914090a..65c05172fbe 100644 --- a/common/common.go +++ b/common/common.go @@ -68,6 +68,7 @@ var ( ErrCannotCalculateOffline = errors.New("cannot calculate offline, unsupported") ErrNoResponse = errors.New("no response") ErrTypeAssertFailure = errors.New("type assert failure") + ErrNoResults = errors.New("no results found") ErrUnknownError = errors.New("unknown error") ErrGettingField = errors.New("error getting field") ErrSettingField = errors.New("error setting field") diff --git a/config_example.json b/config_example.json index 97053af4445..42c52c09363 100644 --- a/config_example.json +++ b/config_example.json @@ -2135,7 +2135,8 @@ "margin", "option", "perpetualswap", - "spot" + "spot", + "spread" ], "pairs": { "futures": { @@ -2162,6 +2163,11 @@ "assetEnabled": true, "enabled": "BTC-USDT,ETH-USDT,OKB-USDT", "available": "OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,BTC-USDT,ETH-USDT,OKB-USDT,OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,AKITA-USDT,ALCX-USDT,ALGO-USDT,ALPHA-USDT,ANC-USDT,ANT-USDT,ANW-USDT,APE-USDT,APIX-USDT,API3-USDT,APM-USDT,AR-USDT,ARK-USDT,AST-USDT,ASTR-USDT,ATOM-USDT,AUCTION-USDT,AVAX-USDT,AXS-USDT,AZY-USDT,BABYDOGE-USDT,BADGER-USDT,BAL-USDT,BAND-USDT,BAT-USDT,BCD-USDT,BCH-USDT,BETH-USDT,BHP-USDT,BICO-USDT,BLOK-USDT,BNT-USDT,BORING-USDT,BORA-USDT,BRWL-USDT,BSV-USDT,BTG-USDT,BTM-USDT,BTT-USDT,BZZ-USDT,CELO-USDT,CEL-USDT,CELR-USDT,CELT-USDT,CFG-USDT,CFX-USDT,CGS-USDT,CHAT-USDT,CHE-USDT,CHZ-USDT,CLV-USDT,CMT-USDT,CNTM-USDT,COMP-USDT,CONV-USDT,COVER-USDT,CQT-USDT,CRO-USDT,CRV-USDT,CSPR-USDT,CTC-USDT,CTXC-USDT,CVC-USDT,CVP-USDT,CVT-USDT,CVX-USDT,DAI-USDT,DAO-USDT,DASH-USDT,DCR-USDT,DEP-USDT,DEVT-USDT,DGB-USDT,DHT-USDT,DIA-USDT,DMD-USDT,DNA-USDT,DOME-USDT,DORA-USDT,DOSE-USDT,DYDX-USDT,EC-USDT,EDEN-USDT,EFI-USDT,EGLD-USDT,EGT-USDT,ELF-USDT,ELON-USDT,ELT-USDT,EM-USDT,ENJ-USDT,ENS-USDT,EOS-USDT,ERN-USDT,ETC-USDT,EURT-USDT,FAIR-USDT,FAME-USDT,FITFI-USDT,FLM-USDT,FLOW-USDT,FODL-USDT,FORTH-USDT,FRONT-USDT,FSN-USDT,FTM-USDT,GALA-USDT,GALFT-USDT,GARI-USDT,GAS-USDT,GF-USDT,GHST-USDT,GLM-USDT,GLMR-USDT,GM-USDT,GMT-USDT,GODS-USDT,GOG-USDT,GRT-USDT,GTO-USDT,GUSD-USDT,HBAR-USDT,HC-USDT,HDAO-USDT,HEGIC-USDT,HYC-USDT,ICP-USDT,ICX-USDT,ILV-USDT,IMX-USDT,INT-USDT,INX-USDT,IOST-USDT,IOTA-USDT,IQ-USDT,JFI-USDT,JOE-USDT,JST-USDT,KAN-USDT,KAR-USDT,KCASH-USDT,KDA-USDT,KINE-USDT,KISHU-USDT,KLAY-USDT,KNC-USDT,KOL-USDT,KONO-USDT,KP3R-USDT,KSM-USDT,LAMB-USDT,LAT-USDT,LBA-USDT,LDN-USDT,LDO-USDT,LEASH-USDT,LEO-USDT,LET-USDT,LINK-USDT,LING-USDT,LITH-USDT,LON-USDT,LOON-USDT,LOOKS-USDT,LPT-USDT,LRC-USDT,LSK-USDT,MAGIC-USDT,MANA-USDT,MASK-USDT,MATIC-USDT,MCO-USDT,MDA-USDT,MDT-USDT,MEME-USDT,METIS-USDT,MILO-USDT,MINA-USDT,MIR-USDT,MITH-USDT,MKR-USDT,MLN-USDT,MOF-USDT,MON-USDT,MOVR-USDT,MOVEZ-USDT,MXC-USDT,MXT-USDT,NAS-USDT,NEAR-USDT,NEO-USDT,NFT-USDT,NMR-USDT,NULS-USDT,NYM-USDT,OM-USDT,OMG-USDT,OMI-USDT,ONE-USDT,ONT-USDT,OP-USDT,ORBS-USDT,ORB-USDT,ORS-USDT,OXT-USDT,PAY-USDT,PCI-USDT,PEOPLE-USDT,PERP-USDT,PHA-USDT,PICKLE-USDT,PIT-USDT,PLG-USDT,PNK-USDT,POLS-USDT,POLYDOGE-USDT,PPT-USDT,PRQ-USDT,PST-USDT,PSTAKE-USDT,QOM-USDT,QTUM-USDT,RACA-USDT,RAY-USDT,REN-USDT,REP-USDT,REVV-USDT,RFUEL-USDT,RIO-USDT,RNT-USDT,ROAD-USDT,RON-USDT,RSR-USDT,RSS3-USDT,RVN-USDT,SAITAMA-USDT,SAMO-USDT,SAND-USDT,SC-USDT,SD-USDT,SFG-USDT,SHIB-USDT,SIS-USDT,SKEB-USDT,SKL-USDT,SLP-USDT,SNT-USDT,SNX-USDT,SOC-USDT,SOL-USDT,SOS-USDT,SPELL-USDT,SRM-USDT,STARL-USDT,STC-USDT,STORJ-USDT,STRK-USDT,STX-USDT,SUN-USDT,SUSHI-USDT,SWEAT-USDT,SWFTC-USDT,SWRV-USDT,T-USDT,TAI-USDT,TAKI-USDT,TCT-USDT,THETA-USDT,THG-USDT,TON-USDT,TOPC-USDT,TORN-USDT,TOWN-USDT,TRADE-USDT,TRA-USDT,TRB-USDT,TRUE-USDT,TRX-USDT,TUP-USDT,TUSD-USDT,UMA-USDT,UMEE-USDT,UNI-USDT,USDC-USDT,USDP-USDT,USTC-USDT,UTK-USDT,VALUE-USDT,VELO-USDT,VRA-USDT,VSYS-USDT,WAVES-USDT,WAXP-USDT,WBTC-USDT,WEMIX-USDT,WGRT-USDT,WING-USDT,WIN-USDT,WNCG-USDT,WNXM-USDT,WOO-USDT,WSB-USDT,WXT-USDT,XAUT-USDT,XCH-USDT,XEC-USDT,XEM-USDT,XETA-USDT,XLM-USDT,XMR-USDT,XNO-USDT,XPR-USDT,XTZ-USDT,YEE-USDT,YFI-USDT,YFII-USDT,YGG-USDT,YOU-USDT,YOYO-USDT,ZBC-USDT,ZEC-USDT,ZEN-USDT,ZIL-USDT,ZKS-USDT,ZRX-USDT,ZYRO-USDT,BTC-USDC,ETH-USDC,ETH-BTC,OKB-USDC,OKT-USDC,LTC-USDC,DOT-USDC,DOGE-USDC,LUNC-USDC,ETHW-USDC,LUNA-USDC,FIL-USDC,XRP-USDC,1INCH-USDC,AAVE-USDC,ADA-USDC,AGLD-USDC,ALGO-USDC,ANC-USDC,ANT-USDC,APE-USDC,API3-USDC,AR-USDC,ASTR-USDC,ATOM-USDC,AVAX-USDC,AXS-USDC,AZY-USDC,BABYDOGE-USDC,BAT-USDC,BCH-USDC,BICO-USDC,BSV-USDC,CEL-USDC,CELO-USDC,CELT-USDC,CHZ-USDC,COMP-USDC,CRO-USDC,CRV-USDC,CSPR-USDC,DASH-USDC,DEP-USDC,DOME-USDC,DYDX-USDC,EGLD-USDC,ELT-USDC,ENS-USDC,EOS-USDC,ETC-USDC,FITFI-USDC,FLM-USDC,FLOW-USDC,FTM-USDC,GALA-USDC,GALFT-USDC,GARI-USDC,GLMR-USDC,GMT-USDC,GODS-USDC,GRT-USDC,HBAR-USDC,ICP-USDC,IMX-USDC,IOST-USDC,JST-USDC,KISHU-USDC,KLAY-USDC,KNC-USDC,KSM-USDC,LINK-USDC,LOOKS-USDC,LRC-USDC,MANA-USDC,MASK-USDC,MATIC-USDC,MINA-USDC,MKR-USDC,MOF-USDC,MOVEZ-USDC,MXC-USDC,NEAR-USDC,NFT-USDC,NMR-USDC,NYM-USDC,OMG-USDC,OP-USDC,PEOPLE-USDC,PERP-USDC,RACA-USDC,RSR-USDC,SAITAMA-USDC,SAND-USDC,SHIB-USDC,SLP-USDC,SNX-USDC,SOC-USDC,SOL-USDC,SOS-USDC,SRM-USDC,STARL-USDC,STC-USDC,STORJ-USDC,STX-USDC,SUSHI-USDC,SWFTC-USDC,THETA-USDC,TON-USDC,TORN-USDC,TRB-USDC,TRX-USDC,UNI-USDC,USDP-USDC,USTC-USDC,VRA-USDC,WAVES-USDC,XCH-USDC,XEM-USDC,XLM-USDC,XMR-USDC,XNO-USDC,XTZ-USDC,YFI-USDC,YFII-USDC,YGG-USDC,ZEC-USDC,ZIL-USDC,BTC-DAI,ETH-DAI,BTC-USDK,ETH-USDK,USDT-USDK,OKB-BTC,OKT-BTC,LTC-BTC,DOT-BTC,DOGE-BTC,FIL-BTC,XRP-BTC,AAVE-BTC,ADA-BTC,ALGO-BTC,ALPHA-BTC,ANT-BTC,ATOM-BTC,AVAX-BTC,BADGER-BTC,BAT-BTC,BCD-BTC,BCH-BTC,BNT-BTC,BSV-BTC,BTG-BTC,BTM-BTC,CELO-BTC,CELT-BTC,CHZ-BTC,COMP-BTC,CQT-BTC,CRO-BTC,CRV-BTC,CTC-BTC,CVC-BTC,DASH-BTC,DCR-BTC,DGB-BTC,EGLD-BTC,ELF-BTC,ENJ-BTC,EOS-BTC,ETC-BTC,FLOW-BTC,GAS-BTC,GRT-BTC,GTO-BTC,HBAR-BTC,HC-BTC,ICP-BTC,ICX-BTC,INT-BTC,IOST-BTC,IOTA-BTC,KLAY-BTC,KNC-BTC,KSM-BTC,LINK-BTC,LRC-BTC,LSK-BTC,MANA-BTC,MITH-BTC,MKR-BTC,NAS-BTC,NEAR-BTC,NEO-BTC,NULS-BTC,OMG-BTC,ONT-BTC,PST-BTC,QTUM-BTC,REN-BTC,RSR-BTC,RVN-BTC,SC-BTC,SNT-BTC,SOL-BTC,SRM-BTC,STX-BTC,SWFTC-BTC,THETA-BTC,TRUE-BTC,TRX-BTC,UNI-BTC,VSYS-BTC,WAVES-BTC,WBTC-BTC,WXT-BTC,XCH-BTC,XEM-BTC,XLM-BTC,XMR-BTC,XTZ-BTC,YFI-BTC,YOU-BTC,ZEC-BTC,ZEN-BTC,ZIL-BTC,ZRX-BTC,OKB-ETH,OKT-ETH,LTC-ETH,DOT-ETH,DOGE-ETH,FIL-ETH,XRP-ETH,AAVE-ETH,ADA-ETH,API3-ETH,ATOM-ETH,AVAX-ETH,BETH-ETH,CRV-ETH,DASH-ETH,EOS-ETH,ETC-ETH,FLOW-ETH,GAS-ETH,GHST-ETH,HEGIC-ETH,INT-ETH,IOST-ETH,KSM-ETH,LINK-ETH,MANA-ETH,MKR-ETH,NEAR-ETH,NEO-ETH,NULS-ETH,OM-ETH,QTUM-ETH,SNX-ETH,SOL-ETH,SUSHI-ETH,SWFTC-ETH,TRX-ETH,UNI-ETH,WBTC-ETH,XLM-ETH,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,OKDOT1-DOT,OKDOT2-DOT,BTC-EURT,ETH-EURT" + }, + "spread": { + "assetEnabled": true, + "enabled": "BTC-USDT-SWAP_BTC-USDT-250328,ETH-USDT-SWAP_ETH-USD-SWAP,ETH-USDT_ETH-USD-250627", + "available": "BTC-USDT-SWAP_BTC-USDT-250328,ETH-USDT-SWAP_ETH-USD-SWAP,ETH-USDT_ETH-USD-250627,BTC-USD-241220_BTC-USD-250328,BTC-USDT-SWAP_BTC-USDT-250228,BTC-USDT_BTC-USDT-250131,BTC-USD-SWAP_BTC-USD-250328,BTC-USDT-241220_BTC-USDT-250627,ETH-USDT-241220_ETH-USDT-250328,BTC-USD-250228_BTC-USD-250627,ETH-USDT-SWAP_ETH-USDT-250328,BTC-USD-241220_BTC-USD-250228,ETH-USDT-250328_ETH-USDT-250627,DOGE-USDT_DOGE-USDT-SWAP,ETH-USD-SWAP_ETH-USD-241227,BTC-USDT_BTC-USD-241227,BTC-USDT-SWAP_BTC-USDT-250131,ETH-USDT-241220_ETH-USDT-250627,ETH-USD-SWAP_ETH-USD-241220,BTC-USD-SWAP_BTC-USD-250228,ETH-USD-241227_ETH-USD-250228,BTC-USDT_BTC-USDT-250328,LTC-USDT_LTC-USDT-SWAP,ETH-USDT_ETH-USD-250328,ETH-USD-250328_ETH-USD-250627,ETH-USD-241227_ETH-USD-250328,BTC-USD-SWAP_BTC-USD-250131,BTC-USDT_BTC-USDT-250228,ETH-USD-250228_ETH-USD-250328,SOL-USDT_SOL-USDT-SWAP,ETH-USDT_ETH-USD-SWAP,BTC-USD-241220_BTC-USD-241227,ETH-USD-250131_ETH-USD-250328,ETH-USD-SWAP_ETH-USD-250131,BTC-USDT-241227_BTC-USDT-250131,ETH-USDT_ETH-USDT-250627,ETH-USDT-SWAP_ETH-USDT-250627,BTC-USDT_BTC-USDT-SWAP,BTC-USDT-SWAP_BTC-USDT-250627,BTC-USDT_BTC-USD-SWAP,BTC-USDT-SWAP_BTC-USD-SWAP,ETH-USDT-241220_ETH-USDT-241227,ETH-USD-241220_ETH-USD-250131,ETH-USD-SWAP_ETH-USD-250228,ETH-USD-241227_ETH-USD-250131,BTC-USDT_BTC-USD-250627,BTC-USD-241220_BTC-USD-250131,ETH-USD-241220_ETH-USD-241227,BTC-USD-SWAP_BTC-USD-250627,BTC-USD-241227_BTC-USD-250131,ETH-USD-SWAP_ETH-USD-250328,ETH-USD-250228_ETH-USD-250627,BTC-USDT-241227_BTC-USDT-250228,BTC-USDT_BTC-USD-250328,ETH-USD-250131_ETH-USD-250627,BTC-USDT_BTC-USDT-250627,BTC-USDT-241227_BTC-USDT-250328,BTC-USD-241227_BTC-USD-250328,BTC-USDT-250131_BTC-USDT-250328,BCH-USDT_BCH-USDT-SWAP,BTC-USDT-250228_BTC-USDT-250328,BTC-USD-250328_BTC-USD-250627,BTC-USDT-241220_BTC-USDT-241227,BTC-USD-241227_BTC-USD-250228,ETH-USDT_ETH-USDT-250328,BTC-USDT-250131_BTC-USDT-250228,BTC-USD-250131_BTC-USD-250627,ETH-USD-SWAP_ETH-USD-250627,ETH-USD-241220_ETH-USD-250228,BTC-USD-SWAP_BTC-USD-241227,BTC-USD-SWAP_BTC-USD-241220,BTC-USD-250131_BTC-USD-250328,BTC-USDT-241227_BTC-USDT-250627,ETH-USD-250131_ETH-USD-250228,ETH-USD-241220_ETH-USD-250328,ETH-USDT_ETH-USDT-241227,ETH-USDT-SWAP_ETH-USDT-241220,BTC-USDT-241220_BTC-USDT-250328,BTC-USD-250228_BTC-USD-250328,ETH-USDT-SWAP_ETH-USDT-241227,BTC-USDT-SWAP_BTC-USDT-241220,ETH-USDT_ETH-USDT-241220,ETH-USDT-241227_ETH-USDT-250627,BTC-USD-241227_BTC-USD-250627,BTC-USDT-241220_BTC-USDT-250228,BTC-USDT-250131_BTC-USDT-250627,BTC-USD-241220_BTC-USD-250627,BTC-USDT-SWAP_BTC-USDT-241227,BTC-USD-250131_BTC-USD-250228,BTC-USDT-250328_BTC-USDT-250627,ETH-USD-241220_ETH-USD-250627,XRP-USDT_XRP-USDT-SWAP,ETH-USD-241227_ETH-USD-250627,BTC-USDT_BTC-USDT-241227,BTC-USDT-250228_BTC-USDT-250627,BTC-USDT_BTC-USDT-241220,ETH-USDT_ETH-USD-241227,BTC-USDT-241220_BTC-USDT-250131,ETH-USDT_ETH-USDT-SWAP,ETH-USDT-241227_ETH-USDT-25032" } } }, diff --git a/database/repository/withdraw/withdraw.go b/database/repository/withdraw/withdraw.go index 2c92f6b6ff5..1d065517584 100644 --- a/database/repository/withdraw/withdraw.go +++ b/database/repository/withdraw/withdraw.go @@ -3,10 +3,10 @@ package withdraw import ( "context" "database/sql" - "errors" "time" "github.com/gofrs/uuid" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" modelPSQL "github.com/thrasher-corp/gocryptotrader/database/models/postgres" @@ -19,11 +19,6 @@ import ( "github.com/thrasher-corp/sqlboiler/queries/qm" ) -var ( - // ErrNoResults is the error returned if no results are found - ErrNoResults = errors.New("no results found") -) - // Event stores Withdrawal Response details in database func Event(res *withdraw.Response) { if database.DB.SQL == nil { @@ -424,7 +419,7 @@ func getByColumns(q []qm.QueryMod) ([]*withdraw.Response, error) { } } if len(resp) == 0 { - return nil, ErrNoResults + return nil, common.ErrNoResults } return resp, nil } diff --git a/database/repository/withdraw/withdraw_test.go b/database/repository/withdraw/withdraw_test.go index a57e419d997..1caa4ff3726 100644 --- a/database/repository/withdraw/withdraw_test.go +++ b/database/repository/withdraw/withdraw_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/database/drivers" @@ -159,7 +160,7 @@ func withdrawHelper(t *testing.T) { _, err := GetEventByUUID(withdraw.DryRunID.String()) if err != nil { - if !errors.Is(err, ErrNoResults) { + if !errors.Is(err, common.ErrNoResults) { t.Fatal(err) } } @@ -181,7 +182,7 @@ func withdrawHelper(t *testing.T) { if len(v) > 0 { _, err = GetEventByUUID(v[0].ID.String()) if err != nil { - if !errors.Is(err, ErrNoResults) { + if !errors.Is(err, common.ErrNoResults) { t.Error(err) } } diff --git a/exchanges/asset/asset.go b/exchanges/asset/asset.go index 8e20e59e6dd..b987e6ddbe2 100644 --- a/exchanges/asset/asset.go +++ b/exchanges/asset/asset.go @@ -41,6 +41,7 @@ const ( USDCMarginedFutures FutureCombo LinearContract + Spread // Options asset consts must come below this comment for method `IsOptions` Options OptionCombo @@ -58,6 +59,7 @@ const ( perpetualContract = "perpetualcontract" perpetualSwap = "perpetualswap" swap = "swap" + spread = "spread" futures = "futures" deliveryFutures = "delivery" upsideProfitContract = "upsideprofitcontract" @@ -73,7 +75,7 @@ const ( ) var ( - supportedList = Items{Spot, Margin, CrossMargin, MarginFunding, Index, Binary, PerpetualContract, PerpetualSwap, Futures, DeliveryFutures, UpsideProfitContract, DownsideProfitContract, CoinMarginedFutures, USDTMarginedFutures, USDCMarginedFutures, Options, LinearContract, OptionCombo, FutureCombo} + supportedList = Items{Spot, Margin, CrossMargin, MarginFunding, Index, Binary, PerpetualContract, PerpetualSwap, Futures, DeliveryFutures, UpsideProfitContract, DownsideProfitContract, CoinMarginedFutures, USDTMarginedFutures, USDCMarginedFutures, Options, LinearContract, OptionCombo, FutureCombo, Spread} ) // Supported returns a list of supported asset types @@ -100,6 +102,8 @@ func (a Item) String() string { return perpetualContract case PerpetualSwap: return perpetualSwap + case Spread: + return spread case Futures: return futures case DeliveryFutures: @@ -220,6 +224,8 @@ func New(input string) (Item, error) { return PerpetualContract, nil case perpetualSwap, swap: return PerpetualSwap, nil + case spread: + return Spread, nil case futures: return Futures, nil case upsideProfitContract: diff --git a/exchanges/asset/asset_test.go b/exchanges/asset/asset_test.go index cef070415d3..775dda43c36 100644 --- a/exchanges/asset/asset_test.go +++ b/exchanges/asset/asset_test.go @@ -70,7 +70,7 @@ func TestIsValid(t *testing.T) { func TestIsFutures(t *testing.T) { t.Parallel() - valid := []Item{PerpetualContract, PerpetualSwap, Futures, DeliveryFutures, UpsideProfitContract, DownsideProfitContract, CoinMarginedFutures, USDTMarginedFutures, USDCMarginedFutures, FutureCombo, LinearContract} + valid := []Item{PerpetualContract, PerpetualSwap, Futures, DeliveryFutures, UpsideProfitContract, DownsideProfitContract, CoinMarginedFutures, USDTMarginedFutures, USDCMarginedFutures, FutureCombo, LinearContract, Spread} for a := range All { if slices.Contains(valid, a) { require.Truef(t, a.IsFutures(), "IsFutures must return true for %s", a) @@ -117,6 +117,7 @@ func TestNew(t *testing.T) { {Input: "Future", Error: ErrNotSupported}, {Input: "option_combo", Expected: OptionCombo}, {Input: "future_combo", Expected: FutureCombo}, + {Input: "spread", Expected: Spread}, {Input: "linearContract", Expected: LinearContract}, } diff --git a/exchanges/collateral/collateral.go b/exchanges/collateral/collateral.go index 77b045b2a4e..0bdad2df190 100644 --- a/exchanges/collateral/collateral.go +++ b/exchanges/collateral/collateral.go @@ -34,6 +34,8 @@ func (t Mode) String() string { return multiCollateralStr case PortfolioMode: return portfolioCollateralStr + case SpotFuturesMode: + return spotFuturesCollateralStr case UnknownMode: return unknownCollateralStr } diff --git a/exchanges/collateral/collateral_test.go b/exchanges/collateral/collateral_test.go index 60aae825a53..97b35eaf7ac 100644 --- a/exchanges/collateral/collateral_test.go +++ b/exchanges/collateral/collateral_test.go @@ -18,6 +18,9 @@ func TestValidCollateralType(t *testing.T) { if !PortfolioMode.Valid() { t.Fatal("expected 'true', received 'false'") } + if !SpotFuturesMode.Valid() { + t.Fatal("expected 'true', received 'false'") + } if UnsetMode.Valid() { t.Fatal("expected 'false', received 'true'") } diff --git a/exchanges/collateral/collateral_types.go b/exchanges/collateral/collateral_types.go index a0229d46a93..485280f5697 100644 --- a/exchanges/collateral/collateral_types.go +++ b/exchanges/collateral/collateral_types.go @@ -24,20 +24,23 @@ const ( PortfolioMode // UnknownMode has collateral allocated in an unknown manner at present, but is not unset UnknownMode + // SpotFuturesMode has collateral allocated across spot and futures accounts + SpotFuturesMode ) const ( - unsetCollateralStr = "unset" - singleCollateralStr = "single" - multiCollateralStr = "multi" - portfolioCollateralStr = "portfolio" - unknownCollateralStr = "unknown" + unsetCollateralStr = "unset" + singleCollateralStr = "single" + multiCollateralStr = "multi" + portfolioCollateralStr = "portfolio" + spotFuturesCollateralStr = "spot_futures" + unknownCollateralStr = "unknown" ) // ErrInvalidCollateralMode is returned when converting invalid string to collateral mode var ErrInvalidCollateralMode = errors.New("invalid collateral mode") -var supportedCollateralModes = SingleMode | MultiMode | PortfolioMode +var supportedCollateralModes = SingleMode | MultiMode | PortfolioMode | SpotFuturesMode // ByPosition shows how much collateral is used // from positions diff --git a/exchanges/futures/contract.go b/exchanges/futures/contract.go index 83ca8d02c0d..217edc43085 100644 --- a/exchanges/futures/contract.go +++ b/exchanges/futures/contract.go @@ -1,6 +1,8 @@ package futures import ( + "errors" + "strings" "time" "github.com/shopspring/decimal" @@ -9,6 +11,11 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" ) +// var error definitions +var ( + ErrInvalidContractSettlementType = errors.New("invalid contract settlement type") +) + // Contract holds details on futures contracts type Contract struct { Exchange string @@ -41,6 +48,7 @@ const ( Inverse Quanto LinearOrInverse + Hybrid ) // String returns the string representation of a contract settlement type @@ -56,11 +64,34 @@ func (d ContractSettlementType) String() string { return "quanto" case LinearOrInverse: return "linearOrInverse" + case Hybrid: + return "hybrid" default: return "unknown" } } +// StringToContractSettlementType for converting case insensitive contract settlement type +func StringToContractSettlementType(cstype string) (ContractSettlementType, error) { + cstype = strings.ToLower(cstype) + switch cstype { + case UnsetSettlementType.String(), "": + return UnsetSettlementType, nil + case Linear.String(): + return Linear, nil + case Inverse.String(): + return Inverse, nil + case Quanto.String(): + return Quanto, nil + case "linearorinverse": + return LinearOrInverse, nil + case Hybrid.String(): + return Hybrid, nil + default: + return UnsetSettlementType, ErrInvalidContractSettlementType + } +} + // ContractType holds the various style of contracts offered by futures exchanges type ContractType uint8 diff --git a/exchanges/futures/contract_test.go b/exchanges/futures/contract_test.go new file mode 100644 index 00000000000..9a9049bd7db --- /dev/null +++ b/exchanges/futures/contract_test.go @@ -0,0 +1,71 @@ +package futures + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStringToContractSettlementType(t *testing.T) { + t.Parallel() + contractSettlementTypesMap := map[string]struct { + CT ContractSettlementType + Error error + }{ + "lInear": {Linear, nil}, + "LINEAR": {Linear, nil}, + "Inverse": {Inverse, nil}, + "unset": {UnsetSettlementType, nil}, + "hybRiD": {Hybrid, nil}, + "LinearOrInverse": {LinearOrInverse, nil}, + "": {UnsetSettlementType, nil}, + "Quanto": {Quanto, nil}, + "QUANTO": {Quanto, nil}, + "Unknown": {UnsetSettlementType, ErrInvalidContractSettlementType}, + } + for x, v := range contractSettlementTypesMap { + val, err := StringToContractSettlementType(x) + assert.Equal(t, v.CT, val) + assert.ErrorIs(t, err, v.Error) + } +} + +func TestContractSettlementTypeString(t *testing.T) { + t.Parallel() + contractSettlementTypeToStringMap := map[ContractSettlementType]string{ + UnsetSettlementType: "unset", + Linear: "linear", + Inverse: "inverse", + Quanto: "quanto", + LinearOrInverse: "linearOrInverse", + Hybrid: "hybrid", + ContractSettlementType(200): "unknown", + } + for k, v := range contractSettlementTypeToStringMap { + assert.Equal(t, v, k.String()) + } +} + +func TestContractTypeToString(t *testing.T) { + t.Parallel() + contractTypeToStringMap := map[ContractType]string{ + Daily: "day", + Perpetual: "perpetual", + LongDated: "long_dated", + Weekly: "weekly", + Fortnightly: "fortnightly", + ThreeWeekly: "three-weekly", + Monthly: "monthly", + Quarterly: "quarterly", + SemiAnnually: "semi-annually", + HalfYearly: "half-yearly", + NineMonthly: "nine-monthly", + Yearly: "yearly", + Unknown: "unknown", + UnsetContractType: "unset", + ContractType(200): "unset", + } + for k, v := range contractTypeToStringMap { + assert.Equal(t, v, k.String()) + } +} diff --git a/exchanges/margin/margin.go b/exchanges/margin/margin.go index bba28157506..91ccb5b7732 100644 --- a/exchanges/margin/margin.go +++ b/exchanges/margin/margin.go @@ -32,6 +32,10 @@ func (t Type) String() string { return isolatedStr case Multi: return multiStr + case SpotIsolated: + return spotIsolatedStr + case NoMargin: + return cashStr case Unknown: return unknownStr } @@ -46,7 +50,7 @@ func (t Type) Upper() string { // IsValidString checks to see if the supplied string is a valid margin type func IsValidString(m string) bool { switch strings.ToLower(m) { - case isolatedStr, multiStr, unsetStr, crossedStr, crossStr: + case isolatedStr, multiStr, unsetStr, crossedStr, crossStr, spotIsolatedStr, cashStr: return true } return false @@ -60,6 +64,10 @@ func StringToMarginType(m string) (Type, error) { return Isolated, nil case multiStr, crossedStr, crossStr: return Multi, nil + case spotIsolatedStr: + return SpotIsolated, nil + case cashStr: + return NoMargin, nil case "": return Unset, nil } diff --git a/exchanges/margin/margin_test.go b/exchanges/margin/margin_test.go index 2c14b6ffeaa..b5dc245bc89 100644 --- a/exchanges/margin/margin_test.go +++ b/exchanges/margin/margin_test.go @@ -2,161 +2,99 @@ package margin import ( "encoding/json" - "errors" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestValid(t *testing.T) { t.Parallel() - if !Isolated.Valid() { - t.Fatal("expected 'true', received 'false'") - } - if !Multi.Valid() { - t.Fatal("expected 'true', received 'false'") - } - if Unset.Valid() { - t.Fatal("expected 'false', received 'true'") - } - if Unknown.Valid() { - t.Fatal("expected 'false', received 'true'") - } - if Type(137).Valid() { - t.Fatal("expected 'false', received 'true'") - } + require.True(t, Isolated.Valid()) + require.True(t, Multi.Valid()) + require.True(t, NoMargin.Valid()) + require.True(t, SpotIsolated.Valid()) + require.False(t, Unset.Valid()) + require.False(t, Unknown.Valid()) + require.False(t, Type(137).Valid()) } func TestUnmarshalJSON(t *testing.T) { t.Parallel() - type martian struct { - M Type `json:"margin"` - } - - var alien martian - jason := []byte(`{"margin":"isolated"}`) - err := json.Unmarshal(jason, &alien) - if err != nil { - t.Error(err) - } - if alien.M != Isolated { - t.Errorf("received '%v' expected 'isolated'", alien.M) - } - - jason = []byte(`{"margin":"cross"}`) - err = json.Unmarshal(jason, &alien) - if err != nil { - t.Error(err) - } - if alien.M != Multi { - t.Errorf("received '%v' expected 'Multi'", alien.M) - } - - jason = []byte(`{"margin":"hello moto"}`) - err = json.Unmarshal(jason, &alien) - if !errors.Is(err, ErrInvalidMarginType) { - t.Error(err) - } - if alien.M != Unknown { - t.Errorf("received '%v' expected 'isolated'", alien.M) + for name, tc := range map[string]struct { + in string + want Type + err error + }{ + "isolated": {`{"margin":"isolated"}`, Isolated, nil}, + "cross": {`{"margin":"cross"}`, Multi, nil}, + "cash": {`{"margin":"cash"}`, NoMargin, nil}, + "spotIsolated": {`{"margin":"spot_isolated"}`, SpotIsolated, nil}, + "invalid": {`{"margin":"hello moto"}`, Unknown, ErrInvalidMarginType}, + "unset": {`{"margin":""}`, Unset, nil}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + var alien struct { + M Type `json:"margin"` + } + err := json.Unmarshal([]byte(tc.in), &alien) + assert.ErrorIs(t, err, tc.err) + assert.Equal(t, tc.want, alien.M) + }) } } func TestString(t *testing.T) { t.Parallel() - if Unknown.String() != unknownStr { - t.Errorf("received '%v' expected '%v'", Unknown.String(), unknownStr) - } - if Isolated.String() != isolatedStr { - t.Errorf("received '%v' expected '%v'", Isolated.String(), isolatedStr) - } - if Multi.String() != multiStr { - t.Errorf("received '%v' expected '%v'", Multi.String(), multiStr) - } - if Unset.String() != unsetStr { - t.Errorf("received '%v' expected '%v'", Unset.String(), unsetStr) - } + assert.Equal(t, unknownStr, Unknown.String()) + assert.Equal(t, isolatedStr, Isolated.String()) + assert.Equal(t, multiStr, Multi.String()) + assert.Equal(t, unsetStr, Unset.String()) + assert.Equal(t, spotIsolatedStr, SpotIsolated.String()) + assert.Equal(t, cashStr, NoMargin.String()) + assert.Equal(t, "", Type(30).String()) } func TestUpper(t *testing.T) { t.Parallel() - if Unknown.Upper() != strings.ToUpper(unknownStr) { - t.Errorf("received '%v' expected '%v'", Unknown.String(), strings.ToUpper(unknownStr)) - } - if Isolated.Upper() != strings.ToUpper(isolatedStr) { - t.Errorf("received '%v' expected '%v'", Isolated.String(), strings.ToUpper(isolatedStr)) - } - if Multi.Upper() != strings.ToUpper(multiStr) { - t.Errorf("received '%v' expected '%v'", Multi.String(), strings.ToUpper(multiStr)) - } - if Unset.Upper() != strings.ToUpper(unsetStr) { - t.Errorf("received '%v' expected '%v'", Unset.String(), strings.ToUpper(unsetStr)) - } + assert.Equal(t, strings.ToUpper(unknownStr), Unknown.Upper()) + assert.Equal(t, strings.ToUpper(isolatedStr), Isolated.Upper()) + assert.Equal(t, strings.ToUpper(multiStr), Multi.Upper()) + assert.Equal(t, strings.ToUpper(spotIsolatedStr), SpotIsolated.Upper()) + assert.Equal(t, strings.ToUpper(cashStr), NoMargin.Upper()) + assert.Equal(t, strings.ToUpper(unsetStr), Unset.Upper()) } func TestIsValidString(t *testing.T) { t.Parallel() - if IsValidString("lol") { - t.Fatal("expected 'false', received 'true'") - } - if !IsValidString("isolated") { - t.Fatal("expected 'true', received 'false'") - } - if !IsValidString("cross") { - t.Fatal("expected 'true', received 'false'") - } - if !IsValidString("multi") { - t.Fatal("expected 'true', received 'false'") - } - if !IsValidString("unset") { - t.Fatal("expected 'true', received 'false'") - } - if IsValidString("") { - t.Fatal("expected 'false', received 'true'") - } - if IsValidString("unknown") { - t.Fatal("expected 'false', received 'true'") - } + assert.False(t, IsValidString("lol")) + assert.True(t, IsValidString("spot_isolated")) + assert.True(t, IsValidString("cash")) + assert.True(t, IsValidString("isolated")) + assert.True(t, IsValidString("cross")) + assert.True(t, IsValidString("multi")) + assert.True(t, IsValidString("")) + assert.False(t, IsValidString("unknown")) } func TestStringToMarginType(t *testing.T) { t.Parallel() - resp, err := StringToMarginType("lol") - if !errors.Is(err, ErrInvalidMarginType) { - t.Error(err) - } - if resp != Unknown { - t.Errorf("received '%v' expected '%v'", resp, Unknown) - } - - resp, err = StringToMarginType("") - if err != nil { - t.Error(err) - } - if resp != Unset { - t.Errorf("received '%v' expected '%v'", resp, Unset) - } - - resp, err = StringToMarginType("cross") - if err != nil { - t.Error(err) - } - if resp != Multi { - t.Errorf("received '%v' expected '%v'", resp, Multi) - } - - resp, err = StringToMarginType("multi") - if err != nil { - t.Error(err) - } - if resp != Multi { - t.Errorf("received '%v' expected '%v'", resp, Multi) - } - - resp, err = StringToMarginType("isolated") - if err != nil { - t.Error(err) - } - if resp != Isolated { - t.Errorf("received '%v' expected '%v'", resp, Isolated) + for label, v := range map[string]struct { + MarginType Type + Error error + }{ + "lol": {Unknown, ErrInvalidMarginType}, + "": {Unset, nil}, + "cross": {Multi, nil}, + "multi": {Multi, nil}, + "isolated": {Isolated, nil}, + "cash": {NoMargin, nil}, + "spot_isolated": {SpotIsolated, nil}, + } { + resp, err := StringToMarginType(label) + assert.ErrorIs(t, err, v.Error) + assert.Equal(t, v.MarginType.String(), resp.String()) } } diff --git a/exchanges/margin/margin_types.go b/exchanges/margin/margin_types.go index 1d566b11022..dce8ba6a4db 100644 --- a/exchanges/margin/margin_types.go +++ b/exchanges/margin/margin_types.go @@ -78,19 +78,25 @@ const ( // Multi means a margin trade is not isolated from other margin trades // it can sometimes be referred to as "cross" Multi - // Unknown is an unknown margin type but is not unset + // NoMargin indicates a trade that is conducted in non-margin mode, i.e., no borrowing of funds is involved. + NoMargin + // SpotIsolated indicates a margin shared amongst all spot trades but isolated from other assets + SpotIsolated + // Unknown indicates the margin type is set, but unknown Unknown ) -var supported = Isolated | Multi +var supported = Isolated | Multi | NoMargin | SpotIsolated const ( - unsetStr = "unset" - isolatedStr = "isolated" - multiStr = "multi" - crossedStr = "crossed" - crossStr = "cross" - unknownStr = "unknown" + unsetStr = "" + isolatedStr = "isolated" + multiStr = "multi" + cashStr = "cash" + spotIsolatedStr = "spot_isolated" + crossedStr = "crossed" + crossStr = "cross" + unknownStr = "unknown" ) // RateHistoryResponse has the funding rate details diff --git a/exchanges/okx/helpers.go b/exchanges/okx/helpers.go new file mode 100644 index 00000000000..6f79a4ae5f8 --- /dev/null +++ b/exchanges/okx/helpers.go @@ -0,0 +1,193 @@ +package okx + +import ( + "fmt" + "slices" + "strings" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +// orderTypeFromString returns order.Type instance from string +func orderTypeFromString(orderType string) (order.Type, error) { + orderType = strings.ToLower(orderType) + switch orderType { + case orderMarket: + return order.Market, nil + case orderLimit: + return order.Limit, nil + case orderPostOnly: + return order.PostOnly, nil + case orderFOK: + return order.FillOrKill, nil + case orderIOC: + return order.ImmediateOrCancel, nil + case orderOptimalLimitIOC: + return order.OptimalLimitIOC, nil + case "mmp": + return order.MarketMakerProtection, nil + case "mmp_and_post_only": + return order.MarketMakerProtectionAndPostOnly, nil + case "twap": + return order.TWAP, nil + case "move_order_stop": + return order.TrailingStop, nil + case "chase": + return order.Chase, nil + default: + return order.UnknownType, fmt.Errorf("%w %v", order.ErrTypeIsInvalid, orderType) + } +} + +// orderTypeString returns a string representation of order.Type instance +func orderTypeString(orderType order.Type) (string, error) { + switch orderType { + case order.ImmediateOrCancel: + return "ioc", nil + case order.Market, order.Limit, order.Trigger, + order.PostOnly, order.FillOrKill, order.OptimalLimitIOC, + order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly, + order.Chase, order.TWAP, order.OCO: + return orderType.Lower(), nil + case order.ConditionalStop: + return "conditional", nil + case order.TrailingStop: + return "move_order_stop", nil + default: + return "", fmt.Errorf("%w: `%v`", order.ErrUnsupportedOrderType, orderType) + } +} + +// getAssetsFromInstrumentID parses an instrument ID and returns a list of assets types +// that the instrument is associated with +func (ok *Okx) getAssetsFromInstrumentID(instrumentID string) ([]asset.Item, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } + pf, err := ok.CurrencyPairs.GetFormat(asset.Spot, true) + if err != nil { + return nil, err + } + splitSymbol := strings.Split(instrumentID, pf.Delimiter) + if len(splitSymbol) <= 1 { + return nil, fmt.Errorf("%w %v", currency.ErrCurrencyNotSupported, instrumentID) + } + pair, err := currency.NewPairDelimiter(instrumentID, pf.Delimiter) + if err != nil { + return nil, fmt.Errorf("%w: `%s`", err, instrumentID) + } + switch { + case len(splitSymbol) == 2: + resp := make([]asset.Item, 0, 2) + enabled, err := ok.IsPairEnabled(pair, asset.Spot) + if err != nil { + return nil, err + } + if enabled { + resp = append(resp, asset.Spot) + } + enabled, err = ok.IsPairEnabled(pair, asset.Margin) + if err != nil { + return nil, err + } + if enabled { + resp = append(resp, asset.Margin) + } + if len(resp) > 0 { + return resp, nil + } + case len(splitSymbol) > 2: + var aType asset.Item + switch strings.ToLower(splitSymbol[len(splitSymbol)-1]) { + case "swap": + aType = asset.PerpetualSwap + case "c", "p": + aType = asset.Options + default: + aType = asset.Futures + } + enabled, err := ok.IsPairEnabled(pair, aType) + if err != nil { + return nil, err + } else if enabled { + return []asset.Item{aType}, nil + } + } + return nil, fmt.Errorf("%w: no asset enabled with instrument ID `%v`", asset.ErrNotEnabled, instrumentID) +} + +// assetTypeFromInstrumentType returns an asset Item instance given and Instrument Type string +func assetTypeFromInstrumentType(instrumentType string) (asset.Item, error) { + switch strings.ToUpper(instrumentType) { + case instTypeSwap, instTypeContract: + return asset.PerpetualSwap, nil + case instTypeSpot: + return asset.Spot, nil + case instTypeMargin: + return asset.Margin, nil + case instTypeFutures: + return asset.Futures, nil + case instTypeOption: + return asset.Options, nil + case "": + return asset.Empty, nil + default: + return asset.Empty, asset.ErrNotSupported + } +} + +func (ok *Okx) validatePlaceOrderParams(arg *PlaceOrderRequestParam) error { + if arg == nil { + return common.ErrNilPointer + } + if arg.InstrumentID == "" { + return errMissingInstrumentID + } + if arg.AssetType == asset.Spot || arg.AssetType == asset.Margin || arg.AssetType == asset.Empty { + arg.Side = strings.ToLower(arg.Side) + if arg.Side != order.Buy.Lower() && arg.Side != order.Sell.Lower() { + return fmt.Errorf("%w %s", order.ErrSideIsInvalid, arg.Side) + } + } + if !slices.Contains([]string{"", TradeModeCross, TradeModeIsolated, TradeModeCash}, arg.TradeMode) { + return fmt.Errorf("%w %s", errInvalidTradeModeValue, arg.TradeMode) + } + if arg.AssetType == asset.Futures || arg.AssetType == asset.PerpetualSwap { + arg.PositionSide = strings.ToLower(arg.PositionSide) + if !slices.Contains([]string{"long", "short"}, arg.PositionSide) { + return fmt.Errorf("%w: `%s`, 'long' or 'short' supported", order.ErrSideIsInvalid, arg.PositionSide) + } + } + arg.OrderType = strings.ToLower(arg.OrderType) + if !slices.Contains([]string{orderMarket, orderLimit, orderPostOnly, orderFOK, orderIOC, orderOptimalLimitIOC, "mmp", "mmp_and_post_only"}, arg.OrderType) { + return fmt.Errorf("%w: '%v'", order.ErrTypeIsInvalid, arg.OrderType) + } + if arg.Amount <= 0 { + return order.ErrAmountBelowMin + } + if !slices.Contains([]string{"", "base_ccy", "quote_ccy"}, arg.QuantityType) { + return errCurrencyQuantityTypeRequired + } + return nil +} + +// assetTypeString returns a string representation of asset type +func assetTypeString(assetType asset.Item) (string, error) { + switch assetType { + case asset.Spot: + return "SPOT", nil + case asset.Margin: + return "MARGIN", nil + case asset.Futures: + return "FUTURES", nil + case asset.Options: + return "OPTION", nil + case asset.PerpetualSwap: + return "SWAP", nil + default: + return "", asset.ErrNotSupported + } +} diff --git a/exchanges/okx/okx.go b/exchanges/okx/okx.go index a3bbe1a552c..252af3fbc41 100644 --- a/exchanges/okx/okx.go +++ b/exchanges/okx/okx.go @@ -10,20 +10,23 @@ import ( "net/http" "net/url" "reflect" + "slices" "strconv" "strings" + "sync" "time" "github.com/thrasher-corp/gocryptotrader/common" - "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/margin" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/types" ) // Okx is the overarching type across this package @@ -37,430 +40,45 @@ type Okx struct { // ok.WsRequestSemaphore <- 1 // defer func() { <-ok.WsRequestSemaphore }() WsRequestSemaphore chan int + + instrumentsInfoMapLock sync.Mutex + instrumentsInfoMap map[string][]Instrument } const ( - baseURL = "https://www.okx.com/" - okxAPIURL = baseURL + okxAPIPath - okxAPIVersion = "/v5/" - tradeSpot = "trade-spot/" - tradeMargin = "trade-margin/" - tradeFutures = "trade-futures/" - tradePerps = "trade-swap/" - tradeOptions = "trade-option/" - - okxAPIPath = "api" + okxAPIVersion - okxWebsocketURL = "wss://ws.okx.com:8443/ws" + okxAPIVersion - - okxAPIWebsocketPublicURL = okxWebsocketURL + "public" - okxAPIWebsocketPrivateURL = okxWebsocketURL + "private" - - // tradeEndpoints - tradeOrder = "trade/order" - placeMultipleOrderURL = "trade/batch-orders" - cancelTradeOrder = "trade/cancel-order" - cancelBatchTradeOrders = "trade/cancel-batch-orders" - amendOrder = "trade/amend-order" - amendBatchOrders = "trade/amend-batch-orders" - closePositionPath = "trade/close-position" - pendingTradeOrders = "trade/orders-pending" - tradeHistory = "trade/orders-history" - orderHistoryArchive = "trade/orders-history-archive" - tradeFills = "trade/fills" - tradeFillsHistory = "trade/fills-history" - assetBills = "asset/bills" - lightningDeposit = "asset/deposit-lightning" - assetDeposits = "asset/deposit-address" - pathToAssetDepositHistory = "asset/deposit-history" - assetWithdrawal = "asset/withdrawal" - assetLightningWithdrawal = "asset/withdrawal-lightning" - cancelWithdrawal = "asset/cancel-withdrawal" - withdrawalHistory = "asset/withdrawal-history" - smallAssetsConvert = "asset/convert-dust-assets" - - // Algo order routes - cancelAlgoOrder = "trade/cancel-algos" - algoTradeOrder = "trade/order-algo" - cancelAdvancedAlgoOrder = "trade/cancel-advance-algos" - getAlgoOrders = "trade/orders-algo-pending" - algoOrderHistory = "trade/orders-algo-history" - easyConvertCurrencyList = "trade/easy-convert-currency-list" - easyConvertHistoryPath = "trade/easy-convert-history" - easyConvert = "trade/easy-convert" - oneClickRepayCurrencyListPath = "trade/one-click-repay-currency-list" - oneClickRepayHistory = "trade/one-click-repay-history" - oneClickRepay = "trade/one-click-repay" - - // Funding account routes - assetCurrencies = "asset/currencies" - assetBalance = "asset/balances" - assetValuation = "asset/asset-valuation" - assetTransfer = "asset/transfer" - assetTransferState = "asset/transfer-state" - - // Market Data - marketTickers = "market/tickers" - marketTicker = "market/ticker" - indexTickers = "market/index-tickers" - marketBooks = "market/books" - marketCandles = "market/candles" - marketCandlesHistory = "market/history-candles" - marketCandlesIndex = "market/index-candles" - marketPriceCandles = "market/mark-price-candles" - marketTrades = "market/trades" - marketTradesHistory = "market/history-trades" - marketPlatformVolumeIn24Hour = "market/platform-24-volume" - marketOpenOracles = "market/open-oracle" - marketExchangeRate = "market/exchange-rate" - marketIndexComponents = "market/index-components" - marketBlockTickers = "market/block-tickers" - marketBlockTicker = "market/block-ticker" - - // Public endpoints - publicInstruments = "public/instruments" - publicDeliveryExerciseHistory = "public/delivery-exercise-history" - publicOpenInterestValues = "public/open-interest" - publicFundingRate = "public/funding-rate" - publicFundingRateHistory = "public/funding-rate-history" - publicLimitPath = "public/price-limit" - publicOptionalData = "public/opt-summary" - publicEstimatedPrice = "public/estimated-price" - publicDiscountRate = "public/discount-rate-interest-free-quota" - publicTime = "public/time" - publicLiquidationOrders = "public/liquidation-orders" - publicMarkPrice = "public/mark-price" - publicPositionTiers = "public/position-tiers" - publicInterestRateAndLoanQuota = "public/interest-rate-loan-quota" - publicVIPInterestRateAndLoanQuota = "public/vip-interest-rate-loan-quota" - publicUnderlyings = "public/underlying" - publicInsuranceFunds = "public/insurance-fund" - publicCurrencyConvertContract = "public/convert-contract-coin" - publicBlockTrades = "public/block-trades" - - // Trading Endpoints - tradingDataSupportedCoins = "rubik/stat/trading-data/support-coin" - tradingTakerVolume = "rubik/stat/taker-volume" - tradingMarginLoanRatio = "rubik/stat/margin/loan-ratio" - longShortAccountRatio = "rubik/stat/contracts/long-short-account-ratio" - contractOpenInterestVolume = "rubik/stat/contracts/open-interest-volume" - optionOpenInterestVolume = "rubik/stat/option/open-interest-volume" - optionOpenInterestVolumeRatio = "rubik/stat/option/open-interest-volume-ratio" - optionOpenInterestVolumeExpiry = "rubik/stat/option/open-interest-volume-expiry" - optionOpenInterestVolumeStrike = "rubik/stat/option/open-interest-volume-strike" - takerBlockVolume = "rubik/stat/option/taker-block-volume" - - // Convert Currencies end points - assetConvertCurrencies = "asset/convert/currencies" - convertCurrencyPairsPath = "asset/convert/currency-pair" - assetEstimateQuote = "asset/convert/estimate-quote" - assetConvertTrade = "asset/convert/trade" - assetConvertHistory = "asset/convert/history" - - // Account Endpoints - accountBalance = "account/balance" - accountPosition = "account/positions" - accountPositionHistory = "account/positions-history" - accountAndPositionRisk = "account/account-position-risk" - accountBillsDetail = "account/bills" - accountBillsDetailArchive = "account/bills-archive" - accountConfiguration = "account/config" - accountSetPositionMode = "account/set-position-mode" - accountSetLeverage = "account/set-leverage" - accountMaxSize = "account/max-size" - accountMaxAvailSize = "account/max-avail-size" - accountPositionMarginBalance = "account/position/margin-balance" - accountLeverageInfo = "account/leverage-info" - accountMaxLoan = "account/max-loan" - accountTradeFee = "account/trade-fee" - accountInterestAccrued = "account/interest-accrued" - accountInterestRate = "account/interest-rate" - accountSetGreeks = "account/set-greeks" - accountSetIsolatedMode = "account/set-isolated-mode" - accountMaxWithdrawal = "account/max-withdrawal" - accountRiskState = "account/risk-state" - accountBorrowReply = "account/borrow-repay" - accountBorrowRepayHistory = "account/borrow-repay-history" - accountInterestLimits = "account/interest-limits" - accountSimulatedMargin = "account/simulated_margin" - accountGreeks = "account/greeks" - accountPortfolioMarginLimitation = "account/position-tiers" - - // Block Trading - rfqCounterparties = "rfq/counterparties" - rfqCreateRfq = "rfq/create-rfq" - rfqCancelRfq = "rfq/cancel-rfq" - rfqCancelRfqs = "rfq/cancel-batch-rfqs" - rfqCancelAllRfqs = "rfq/cancel-all-rfqs" - rfqExecuteQuote = "rfq/execute-quote" - makerInstrumentSettings = "rfq/maker-instrument-settings" - mmpReset = "rfq/mmp-reset" - rfqCreateQuote = "rfq/create-quote" - rfqCancelQuote = "rfq/cancel-quote" - rfqCancelBatchQuotes = "rfq/cancel-batch-quotes" - rfqCancelAllQuotes = "rfq/cancel-all-quotes" - rfqRfqs = "rfq/rfqs" - rfqQuotes = "rfq/quotes" - rfqTrades = "rfq/trades" - rfqPublicTrades = "rfq/public-trades" - - // Subaccount endpoints - usersSubaccountList = "users/subaccount/list" - subAccountModifyAPIKey = "users/subaccount/modify-apikey" - accountSubaccountBalances = "account/subaccount/balances" - assetSubaccountBalances = "asset/subaccount/balances" - assetSubaccountBills = "asset/subaccount/bills" - assetSubaccountTransfer = "asset/subaccount/transfer" - userSubaccountSetTransferOut = "users/subaccount/set-transfer-out" - usersEntrustSubaccountList = "users/entrust-subaccount-list" - - // Grid Trading Endpoints - gridOrderAlgo = "tradingBot/grid/order-algo" - gridAmendOrderAlgo = "tradingBot/grid/amend-order-algo" - gridAlgoOrderStop = "tradingBot/grid/stop-order-algo" - gridAlgoOrders = "tradingBot/grid/orders-algo-pending" - gridAlgoOrdersHistory = "tradingBot/grid/orders-algo-history" - gridOrdersAlgoDetails = "tradingBot/grid/orders-algo-details" - gridSuborders = "tradingBot/grid/sub-orders" - gridPositions = "tradingBot/grid/positions" - gridWithdrawalIncome = "tradingBot/grid/withdraw-income" - gridComputeMarginBalance = "tradingBot/grid/compute-margin-balance" - gridMarginBalance = "tradingBot/grid/margin-balance" - gridAIParams = "tradingBot/grid/ai-param" - - // Earn - financeOffers = "finance/staking-defi/offers" - financePurchase = "finance/staking-defi/purchase" - financeRedeem = "finance/staking-defi/redeem" - financeCancelPurchase = "finance/staking-defi/cancel" - financeActiveOrders = "finance/staking-defi/orders-active" - financeOrdersHistory = "finance/staking-defi/orders-history" - - // Savings - financeSavingBalance = "finance/savings/balance" - financePurchaseRedempt = "finance/savings/purchase-redempt" - financeSetLendingRate = "finance/savings/set-lending-rate" - financeLendingHistory = "finance/savings/lending-history" - financePublicBorrowInfo = "finance/savings/lending-rate-summary" - financePublicBorrowHistory = "finance/savings/lending-rate-history" - - // Status Endpoints - systemStatus = "system/status" -) + baseURL = "https://www.okx.com/" + apiURL = baseURL + apiPath -var ( - errNo24HrTradeVolumeFound = errors.New("no trade record found in the 24 trade volume ") - errOracleInformationNotFound = errors.New("oracle information not found") - errExchangeInfoNotFound = errors.New("exchange information not found") - errIndexComponentNotFound = errors.New("unable to fetch index components") - errMissingRequiredArgInstType = errors.New("invalid required argument instrument type") - errLimitValueExceedsMaxOf100 = errors.New("limit value exceeds the maximum value 100") - errMissingInstrumentID = errors.New("missing instrument id") - errFundingRateHistoryNotFound = errors.New("funding rate history not found") - errMissingRequiredUnderlying = errors.New("error missing required parameter underlying") - errMissingRequiredParamInstID = errors.New("missing required parameter instrument id") - errLiquidationOrderResponseNotFound = errors.New("liquidation order not found") - errEitherInstIDOrCcyIsRequired = errors.New("either parameter instId or ccy is required") - errIncorrectRequiredParameterTradeMode = errors.New("unacceptable required argument, trade mode") - errInterestRateAndLoanQuotaNotFound = errors.New("interest rate and loan quota not found") - errUnderlyingsForSpecifiedInstTypeNofFound = errors.New("underlyings for the specified instrument id is not found") - errInsuranceFundInformationNotFound = errors.New("insurance fund information not found") - errMissingExpiryTimeParameter = errors.New("missing expiry date parameter") - errInvalidTradeModeValue = errors.New("invalid trade mode value") - errInvalidOrderType = errors.New("invalid order type") - errInvalidAmount = errors.New("unacceptable quantity to buy or sell") - errMissingClientOrderIDOrOrderID = errors.New("client order id or order id is missing") - errInvalidNewSizeOrPriceInformation = errors.New("invalid the new size or price information") - errMissingNewSize = errors.New("missing the order size information") - errMissingMarginMode = errors.New("missing required param margin mode \"mgnMode\"") - errInvalidTriggerPrice = errors.New("invalid trigger price value") - errInvalidPriceLimit = errors.New("invalid price limit value") - errMissingIntervalValue = errors.New("missing interval value") - errMissingTakeProfitTriggerPrice = errors.New("missing take profit trigger price") - errMissingTakeProfitOrderPrice = errors.New("missing take profit order price") - errMissingSizeLimit = errors.New("missing required parameter \"szLimit\"") - errMissingEitherAlgoIDOrState = errors.New("either algo id or order state is required") - errUnacceptableAmount = errors.New("amount must be greater than 0") - errInvalidCurrencyValue = errors.New("invalid currency value") - errInvalidDepositAmount = errors.New("invalid deposit amount") - errMissingResponseBody = errors.New("error missing response body") - errMissingValidWithdrawalID = errors.New("missing valid withdrawal id") - errNoValidResponseFromServer = errors.New("no valid response from server") - errInstrumentTypeRequired = errors.New("instrument type required") - errInvalidInstrumentType = errors.New("invalid instrument type") - errMissingValidGreeksType = errors.New("missing valid greeks type") - errMissingIsolatedMarginTradingSetting = errors.New("missing isolated margin trading setting, isolated margin trading settings automatic:Auto transfers autonomy:Manual transfers") - errInvalidOrderSide = errors.New("invalid order side") - errOrderSideRequired = errors.New("order side required") - errInvalidCounterParties = errors.New("missing counter parties") - errInvalidLegs = errors.New("no legs are provided") - errMissingRfqIDAndClientRfqID = errors.New("missing rfq id or client rfq id") - errMissingRfqIDOrQuoteID = errors.New("either Rfq ID or Quote ID is missing") - errMissingRfqID = errors.New("error missing rfq id") - errMissingLegs = errors.New("missing legs") - errMissingSizeOfQuote = errors.New("missing size of quote leg") - errMossingLegsQuotePrice = errors.New("error missing quote price") - errMissingQuoteIDOrClientQuoteID = errors.New("missing quote id or client quote id") - errMissingEitherQuoteIDAOrClientQuoteIDs = errors.New("missing either quote ids or client quote ids") - errMissingRequiredParameterSubaccountName = errors.New("missing required parameter subaccount name") - errInvalidTransferAmount = errors.New("unacceptable transfer amount") - errInvalidSubaccount = errors.New("invalid account type") - errMissingDestinationSubaccountName = errors.New("missing destination subaccount name") - errMissingInitialSubaccountName = errors.New("missing initial subaccount name") - errMissingAlgoOrderType = errors.New("missing algo order type \"grid\": Spot grid, \"contract_grid\": Contract grid") - errInvalidMaximumPrice = errors.New("invalid maximum price") - errInvalidMinimumPrice = errors.New("invalid minimum price") - errInvalidGridQuantity = errors.New("invalid grid quantity (grid number)") - errMissingSize = errors.New("missing required argument, size") - errMissingRequiredArgumentDirection = errors.New("missing required argument, direction") - errRequiredParameterMissingLeverage = errors.New("missing required parameter, leverage") - errMissingAlgoOrderID = errors.New("missing algo orders id") - errMissingValidStopType = errors.New("invalid grid order stop type, only values are \"1\" and \"2\" ") - errMissingSubOrderType = errors.New("missing sub order type") - errMissingQuantity = errors.New("invalid quantity to buy or sell") - errDepositAddressNotFound = errors.New("deposit address with the specified currency code and chain not found") - errMissingAtLeast1CurrencyPair = errors.New("at least one currency is required to fetch order history") - errNoCandlestickDataFound = errors.New("no candlesticks data found") - errInvalidWebsocketEvent = errors.New("invalid websocket event") - errMissingValidChannelInformation = errors.New("missing channel information") - errNilArgument = errors.New("nil argument is not acceptable") - errNoOrderParameterPassed = errors.New("no order parameter was passed") - errMaxRfqOrdersToCancel = errors.New("no more than 100 Rfq cancel order parameter is allowed") - errMalformedData = errors.New("malformed data") - errInvalidUnderlying = errors.New("invalid underlying") - errMissingRequiredParameter = errors.New("missing required parameter") - errMissingMakerInstrumentSettings = errors.New("missing maker instrument settings") - errInvalidSubAccountName = errors.New("invalid sub-account name") - errInvalidAPIKey = errors.New("invalid api key") - errInvalidAlgoID = errors.New("invalid algo id") - errInvalidMarginTypeAdjust = errors.New("invalid margin type adjust, only 'add' and 'reduce' are allowed") - errInvalidAlgoOrderType = errors.New("invalid algo order type") - errEmptyArgument = errors.New("empty argument") - errInvalidIPAddress = errors.New("invalid ip address") - errInvalidAPIKeyPermission = errors.New("invalid API Key permission") - errInvalidResponseParam = errors.New("invalid response parameter, response must be non-nil pointer") - errEmptyPlaceOrderResponse = errors.New("empty place order response") - errTooManyArgument = errors.New("too many cancel request params") - errIncompleteCurrencyPair = errors.New("incomplete currency pair") - errInvalidDuration = errors.New("invalid grid contract duration, only '7D', '30D', and '180D' are allowed") - errInvalidProtocolType = errors.New("invalid protocol type, only 'staking' and 'defi' allowed") - errExceedLimit = errors.New("limit exceeded") - errOnlyThreeMonthsSupported = errors.New("only three months of trade data retrieval supported") - errOnlyOneResponseExpected = errors.New("one response item expected") - errNoInstrumentFound = errors.New("no instrument found") + apiPath = "api/v5/" + websocketURL = "wss://ws.okx.com:8443/ws/v5/" + + apiWebsocketPublicURL = websocketURL + "public" + apiWebsocketPrivateURL = websocketURL + "private" ) /************************************ MarketData Endpoints *************************************************/ -// OrderTypeFromString returns order.Type instance from string -func (ok *Okx) OrderTypeFromString(orderType string) (order.Type, error) { - switch strings.ToLower(orderType) { - case OkxOrderMarket: - return order.Market, nil - case OkxOrderLimit: - return order.Limit, nil - case OkxOrderPostOnly: - return order.PostOnly, nil - case OkxOrderFOK: - return order.FillOrKill, nil - case OkxOrderIOC: - return order.ImmediateOrCancel, nil - case OkxOrderOptimalLimitIOC: - return order.OptimalLimitIOC, nil - default: - return order.UnknownType, fmt.Errorf("%w %v", errInvalidOrderType, orderType) - } -} - -// OrderTypeString returns a string representation of order.Type instance -func (ok *Okx) OrderTypeString(orderType order.Type) (string, error) { - switch orderType { - case order.Market: - return OkxOrderMarket, nil - case order.Limit: - return OkxOrderLimit, nil - case order.PostOnly: - return OkxOrderPostOnly, nil - case order.FillOrKill: - return OkxOrderFOK, nil - case order.IOS: - return OkxOrderIOC, nil - case order.OptimalLimitIOC: - return OkxOrderOptimalLimitIOC, nil - default: - return "", fmt.Errorf("%w %v", errInvalidOrderType, orderType) - } -} - -// PlaceOrder place an order only if you have sufficient funds. -func (ok *Okx) PlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam, a asset.Item) (*OrderData, error) { - if arg == nil { - return nil, errNilArgument - } - arg.AssetType = a +// PlaceOrder places an order +func (ok *Okx) PlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam) (*OrderData, error) { err := ok.validatePlaceOrderParams(arg) if err != nil { return nil, err } - var resp []OrderData - err = ok.SendHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, tradeOrder, &arg, &resp, true) + var resp *OrderData + err = ok.SendHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, "trade/order", &arg, &resp, request.AuthenticatedRequest) if err != nil { - if len(resp) != 1 { - return nil, err - } - return nil, fmt.Errorf("error code:%s message: %v", resp[0].SCode, resp[0].SMessage) - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer -} - -func (ok *Okx) validatePlaceOrderParams(arg *PlaceOrderRequestParam) error { - if arg.InstrumentID == "" { - return errMissingInstrumentID - } - arg.Side = strings.ToLower(arg.Side) - if arg.Side != order.Buy.Lower() && arg.Side != order.Sell.Lower() { - return fmt.Errorf("%w %s", errInvalidOrderSide, arg.Side) - } - if arg.TradeMode != "" && - arg.TradeMode != TradeModeCross && - arg.TradeMode != TradeModeIsolated && - arg.TradeMode != TradeModeCash { - return fmt.Errorf("%w %s", errInvalidTradeModeValue, arg.TradeMode) - } - if arg.PositionSide != "" { - if (arg.PositionSide == positionSideLong || arg.PositionSide == positionSideShort) && - (arg.AssetType != asset.Futures && arg.AssetType != asset.PerpetualSwap) { - return errors.New("invalid position mode, 'long' or 'short' for Futures/SWAP, otherwise 'net'(default) are allowed") + if resp != nil && resp.StatusMessage != "" { + return nil, fmt.Errorf("%w, error code: %s error message: %s", err, resp.StatusCode, resp.StatusMessage) } + return nil, err } - arg.OrderType = strings.ToLower(arg.OrderType) - if arg.OrderType == order.OptimalLimitIOC.Lower() && - (arg.AssetType != asset.Futures && arg.AssetType != asset.PerpetualSwap) { - return errors.New("\"optimal_limit_ioc\": market order with immediate-or-cancel order (applicable only to Futures and Perpetual swap)") - } - if arg.OrderType != OkxOrderMarket && - arg.OrderType != OkxOrderLimit && - arg.OrderType != OkxOrderPostOnly && - arg.OrderType != OkxOrderFOK && - arg.OrderType != OkxOrderIOC && - arg.OrderType != OkxOrderOptimalLimitIOC { - return fmt.Errorf("%w %v", errInvalidOrderType, arg.OrderType) - } - if arg.Amount <= 0 { - return errInvalidAmount - } - if arg.QuantityType != "" && arg.QuantityType != "base_ccy" && arg.QuantityType != "quote_ccy" { - return errors.New("only base_ccy and quote_ccy quantity types are supported") - } - return nil + return resp, nil } -// PlaceMultipleOrders to place orders in batches. Maximum 20 orders can be placed at a time. Request parameters should be passed in the form of an array. +// PlaceMultipleOrders to place orders in batches. Maximum 20 orders can be placed at a time. Request parameters should be passed in the form of an array func (ok *Okx) PlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequestParam) ([]OrderData, error) { if len(args) == 0 { - return nil, errNoOrderParameterPassed + return nil, order.ErrSubmissionIsNil } var err error for x := range args { @@ -470,65 +88,68 @@ func (ok *Okx) PlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequest } } var resp []OrderData - err = ok.SendHTTPRequest(ctx, exchange.RestSpot, placeMultipleOrdersEPL, http.MethodPost, placeMultipleOrderURL, &args, &resp, true) + err = ok.SendHTTPRequest(ctx, exchange.RestSpot, placeMultipleOrdersEPL, http.MethodPost, "trade/batch-orders", &args, &resp, request.AuthenticatedRequest) if err != nil { if len(resp) == 0 { return nil, err } var errs error for x := range resp { - errs = common.AppendError(errs, fmt.Errorf("error code:%s message: %v", resp[x].SCode, resp[x].SMessage)) + errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %v", resp[x].StatusCode, resp[x].StatusMessage)) } return nil, errs } return resp, nil } -// CancelSingleOrder cancel an incomplete order. -func (ok *Okx) CancelSingleOrder(ctx context.Context, arg CancelOrderRequestParam) (*OrderData, error) { +// CancelSingleOrder cancel an incomplete order +func (ok *Okx) CancelSingleOrder(ctx context.Context, arg *CancelOrderRequestParam) (*OrderData, error) { + if *arg == (CancelOrderRequestParam{}) { + return nil, common.ErrEmptyParams + } if arg.InstrumentID == "" { return nil, errMissingInstrumentID } if arg.OrderID == "" && arg.ClientOrderID == "" { - return nil, errors.New("either order id or client id is required") + return nil, order.ErrOrderIDNotSet } - var resp []OrderData - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelOrderEPL, http.MethodPost, cancelTradeOrder, &arg, &resp, true) + var resp *OrderData + err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelOrderEPL, http.MethodPost, "trade/cancel-order", &arg, &resp, request.AuthenticatedRequest) if err != nil { - if len(resp) != 1 { - return nil, err + if resp != nil && resp.StatusMessage != "" { + return nil, fmt.Errorf("%w, error code: %s and error message: %s", err, resp.StatusCode, resp.StatusMessage) } - return nil, fmt.Errorf("error code:%s message: %v", resp[0].SCode, resp[0].SMessage) - } - if len(resp) == 1 { - return &resp[0], nil + return nil, err } - return nil, errNoValidResponseFromServer + return resp, nil } // CancelMultipleOrders cancel incomplete orders in batches. Maximum 20 orders can be canceled at a time. -// Request parameters should be passed in the form of an array. +// Request parameters should be passed in the form of an array func (ok *Okx) CancelMultipleOrders(ctx context.Context, args []CancelOrderRequestParam) ([]OrderData, error) { + if len(args) == 0 { + return nil, common.ErrEmptyParams + } for x := range args { arg := args[x] if arg.InstrumentID == "" { return nil, errMissingInstrumentID } if arg.OrderID == "" && arg.ClientOrderID == "" { - return nil, errors.New("either order id or client id is required") + return nil, order.ErrOrderIDNotSet } } var resp []OrderData err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleOrdersEPL, - http.MethodPost, cancelBatchTradeOrders, args, &resp, true) + http.MethodPost, "trade/cancel-batch-orders", args, &resp, request.AuthenticatedRequest) if err != nil { if len(resp) == 0 { return nil, err } var errs error for x := range resp { - if resp[x].SCode != "0" { - errs = common.AppendError(errs, fmt.Errorf("error code:%s message: %v", resp[x].SCode, resp[x].SMessage)) + if resp[x].StatusCode != "0" { + errs = common.AppendError(errs, fmt.Errorf("error code:%s message: %v", resp[x].StatusCode, resp[x].StatusMessage)) } } return nil, errs @@ -536,99 +157,87 @@ func (ok *Okx) CancelMultipleOrders(ctx context.Context, args []CancelOrderReque return resp, nil } -// AmendOrder an incomplete order. +// AmendOrder an incomplete order func (ok *Okx) AmendOrder(ctx context.Context, arg *AmendOrderRequestParams) (*OrderData, error) { + if arg == nil { + return nil, common.ErrNilPointer + } if arg.InstrumentID == "" { return nil, errMissingInstrumentID } if arg.ClientOrderID == "" && arg.OrderID == "" { - return nil, errMissingClientOrderIDOrOrderID + return nil, order.ErrOrderIDNotSet } - if arg.NewQuantity < 0 && arg.NewPrice < 0 { + if arg.NewQuantity <= 0 && arg.NewPrice <= 0 { return nil, errInvalidNewSizeOrPriceInformation } - var resp []OrderData - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, amendOrderEPL, http.MethodPost, amendOrder, arg, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *OrderData + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendOrderEPL, http.MethodPost, "trade/amend-order", arg, &resp, request.AuthenticatedRequest) } -// AmendMultipleOrders amend incomplete orders in batches. Maximum 20 orders can be amended at a time. Request parameters should be passed in the form of an array. +// AmendMultipleOrders amend incomplete orders in batches. Maximum 20 orders can be amended at a time. Request parameters should be passed in the form of an array func (ok *Okx) AmendMultipleOrders(ctx context.Context, args []AmendOrderRequestParams) ([]OrderData, error) { + if len(args) == 0 { + return nil, common.ErrEmptyParams + } for x := range args { if args[x].InstrumentID == "" { return nil, errMissingInstrumentID } if args[x].ClientOrderID == "" && args[x].OrderID == "" { - return nil, errMissingClientOrderIDOrOrderID + return nil, order.ErrOrderIDNotSet } - if args[x].NewQuantity < 0 && args[x].NewPrice < 0 { + if args[x].NewQuantity <= 0 && args[x].NewPrice <= 0 { return nil, errInvalidNewSizeOrPriceInformation } } var resp []OrderData - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendMultipleOrdersEPL, http.MethodPost, amendBatchOrders, &args, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendMultipleOrdersEPL, http.MethodPost, "trade/amend-batch-orders", &args, &resp, request.AuthenticatedRequest) } -// ClosePositions Close all positions of an instrument via a market order. +// ClosePositions close all positions of an instrument via a market order func (ok *Okx) ClosePositions(ctx context.Context, arg *ClosePositionsRequestParams) (*ClosePositionResponse, error) { + if *arg == (ClosePositionsRequestParams{}) { + return nil, common.ErrEmptyParams + } if arg.InstrumentID == "" { return nil, errMissingInstrumentID } - if arg.MarginMode != "" && - arg.MarginMode != TradeModeCross && - arg.MarginMode != TradeModeIsolated { - return nil, errMissingMarginMode - } - var resp []ClosePositionResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, closePositionEPL, http.MethodPost, closePositionPath, arg, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil + switch arg.MarginMode { + case "", TradeModeCross, TradeModeIsolated: + default: + return nil, margin.ErrMarginTypeUnsupported } - return nil, errNoValidResponseFromServer + var resp *ClosePositionResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, closePositionEPL, http.MethodPost, "trade/close-position", arg, &resp, request.AuthenticatedRequest) } // GetOrderDetail retrieves order details given instrument id and order identification func (ok *Okx) GetOrderDetail(ctx context.Context, arg *OrderDetailRequestParam) (*OrderDetail, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (OrderDetailRequestParam{}) { + return nil, common.ErrEmptyParams } - params := url.Values{} if arg.InstrumentID == "" { return nil, errMissingInstrumentID } - params.Set("instId", arg.InstrumentID) + params := url.Values{} switch { case arg.OrderID == "" && arg.ClientOrderID == "": - return nil, errMissingClientOrderIDOrOrderID + return nil, order.ErrOrderIDNotSet case arg.ClientOrderID == "": params.Set("ordId", arg.OrderID) default: params.Set("clOrdId", arg.ClientOrderID) } - var resp []OrderDetail - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getOrderDetEPL, http.MethodGet, common.EncodeURLValues(tradeOrder, params), nil, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + params.Set("instId", arg.InstrumentID) + var resp *OrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOrderDetEPL, http.MethodGet, common.EncodeURLValues("trade/order", params), nil, &resp, request.AuthenticatedRequest) } -// GetOrderList retrieves all incomplete orders under the current account. +// GetOrderList retrieves all incomplete orders under the current account func (ok *Okx) GetOrderList(ctx context.Context, arg *OrderListRequestParams) ([]OrderDetail, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (OrderListRequestParams{}) { + return nil, common.ErrEmptyParams } params := url.Values{} if arg.InstrumentType != "" { @@ -656,27 +265,29 @@ func (ok *Okx) GetOrderList(ctx context.Context, arg *OrderListRequestParams) ([ params.Set("limit", strconv.FormatInt(arg.Limit, 10)) } var resp []OrderDetail - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOrderListEPL, http.MethodGet, common.EncodeURLValues(pendingTradeOrders, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOrderListEPL, http.MethodGet, common.EncodeURLValues("trade/orders-pending", params), nil, &resp, request.AuthenticatedRequest) } -// Get7DayOrderHistory retrieves the completed order data for the last 7 days, and the incomplete orders that have been cancelled are only reserved for 2 hours. +// Get7DayOrderHistory retrieves the completed order data for the last 7 days, and the incomplete orders that have been cancelled are only reserved for 2 hours func (ok *Okx) Get7DayOrderHistory(ctx context.Context, arg *OrderHistoryRequestParams) ([]OrderDetail, error) { - return ok.getOrderHistory(ctx, arg, tradeHistory, getOrderHistory7DaysEPL) + return ok.getOrderHistory(ctx, arg, "trade/orders-history", getOrderHistory7DaysEPL) } -// Get3MonthOrderHistory retrieves the completed order data for the last 7 days, and the incomplete orders that have been cancelled are only reserved for 2 hours. +// Get3MonthOrderHistory retrieves the completed order data for the last 7 days, and the incomplete orders that have been cancelled are only reserved for 2 hours func (ok *Okx) Get3MonthOrderHistory(ctx context.Context, arg *OrderHistoryRequestParams) ([]OrderDetail, error) { - return ok.getOrderHistory(ctx, arg, orderHistoryArchive, getOrderHistory3MonthsEPL) + return ok.getOrderHistory(ctx, arg, "trade/orders-history-archive", getOrderHistory3MonthsEPL) } // getOrderHistory retrieves the order history of the past limited times func (ok *Okx) getOrderHistory(ctx context.Context, arg *OrderHistoryRequestParams, route string, rateLimit request.EndpointLimit) ([]OrderDetail, error) { - params := url.Values{} - if arg.InstrumentType != "" { - params.Set("instType", strings.ToUpper(arg.InstrumentType)) - } else { - return nil, errMissingRequiredArgInstType + if *arg == (OrderHistoryRequestParams{}) { + return nil, common.ErrEmptyParams + } + if arg.InstrumentType == "" { + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} + params.Set("instType", strings.ToUpper(arg.InstrumentType)) if arg.InstrumentID != "" { params.Set("instId", arg.InstrumentID) } @@ -708,28 +319,30 @@ func (ok *Okx) getOrderHistory(ctx context.Context, arg *OrderHistoryRequestPara params.Set("category", strings.ToLower(arg.Category)) } var resp []OrderDetail - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest) } -// GetTransactionDetailsLast3Days retrieves recently-filled transaction details in the last 3 day. +// GetTransactionDetailsLast3Days retrieves recently-filled transaction details in the last 3 day func (ok *Okx) GetTransactionDetailsLast3Days(ctx context.Context, arg *TransactionDetailRequestParams) ([]TransactionDetail, error) { - return ok.getTransactionDetails(ctx, arg, tradeFills, getTransactionDetail3DaysEPL) + return ok.getTransactionDetails(ctx, arg, "trade/fills", getTransactionDetail3DaysEPL) } -// GetTransactionDetailsLast3Months Retrieve recently-filled transaction details in the last 3 months. +// GetTransactionDetailsLast3Months retrieve recently-filled transaction details in the last 3 months func (ok *Okx) GetTransactionDetailsLast3Months(ctx context.Context, arg *TransactionDetailRequestParams) ([]TransactionDetail, error) { - return ok.getTransactionDetails(ctx, arg, tradeFillsHistory, getTransactionDetail3MonthsEPL) + return ok.getTransactionDetails(ctx, arg, "trade/fills-history", getTransactionDetail3MonthsEPL) } -// GetTransactionDetails retrieves recently-filled transaction details. +// GetTransactionDetails retrieves recently-filled transaction details func (ok *Okx) getTransactionDetails(ctx context.Context, arg *TransactionDetailRequestParams, route string, rateLimit request.EndpointLimit) ([]TransactionDetail, error) { - params := url.Values{} + if *arg == (TransactionDetailRequestParams{}) { + return nil, common.ErrEmptyParams + } arg.InstrumentType = strings.ToUpper(arg.InstrumentType) - if arg.InstrumentType != "" { - params.Set("instType", arg.InstrumentType) - } else { - return nil, errMissingRequiredArgInstType + if arg.InstrumentType == "" { + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} + params.Set("instType", arg.InstrumentType) if arg.InstrumentID != "" { params.Set("instId", arg.InstrumentID) } @@ -755,83 +368,80 @@ func (ok *Okx) getTransactionDetails(ctx context.Context, arg *TransactionDetail params.Set("before", arg.Before) } var resp []TransactionDetail - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest) } -// PlaceAlgoOrder order includes trigger order, oco order, conditional order,iceberg order, twap order and trailing order. +// PlaceAlgoOrder order includes trigger, oco, chase, conditional, iceberg, twap and trailing orders. +// chase order only applicable to futures and swap orders func (ok *Okx) PlaceAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams + } if arg.InstrumentID == "" { return nil, errMissingInstrumentID } arg.TradeMode = strings.ToLower(arg.TradeMode) - if arg.TradeMode != TradeModeCross && - arg.TradeMode != TradeModeIsolated { + switch arg.TradeMode { + case TradeModeCross, TradeModeIsolated, TradeModeCash: + default: return nil, errInvalidTradeModeValue } - if arg.Side != order.Buy && - arg.Side != order.Sell { - return nil, errInvalidOrderSide + arg.Side = strings.ToLower(arg.Side) + if arg.Side != order.Buy.Lower() && + arg.Side != order.Sell.Lower() { + return nil, order.ErrSideIsInvalid } if arg.OrderType == "" { - return nil, errInvalidOrderType + return nil, order.ErrTypeIsInvalid } if arg.Size <= 0 { - return nil, errMissingNewSize - } - var resp []AlgoOrder - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, placeAlgoOrderEPL, http.MethodGet, algoTradeOrder, arg, &resp, true) - if err != nil { - return nil, err + return nil, order.ErrAmountBelowMin } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *AlgoOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeAlgoOrderEPL, http.MethodPost, "trade/order-algo", arg, &resp, request.AuthenticatedRequest) } -// PlaceStopOrder to place stop order +// PlaceStopOrder places a stop order. +// The order type should be "conditional" because stop orders are used for conditional take-profit or stop-loss scenarios. func (ok *Okx) PlaceStopOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams } - if arg.OrderType != "conditional" { - return nil, errInvalidOrderType - } - if arg.TakeProfitTriggerPrice == 0 { - return nil, errMissingTakeProfitTriggerPrice + if arg.TakeProfitTriggerPrice <= 0 { + return nil, fmt.Errorf("%w, take profit trigger price is required", order.ErrPriceBelowMin) } if arg.TakeProfitTriggerPriceType == "" { - return nil, errMissingTakeProfitOrderPrice + return nil, order.ErrUnknownPriceType } return ok.PlaceAlgoOrder(ctx, arg) } // PlaceTrailingStopOrder to place trailing stop order func (ok *Okx) PlaceTrailingStopOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams } if arg.OrderType != "move_order_stop" { - return nil, errInvalidOrderType + return nil, fmt.Errorf("%w: order type value 'move_order_stop' is only supported for move_order_stop orders", order.ErrTypeIsInvalid) } - if arg.CallbackRatio == 0 && arg.CallbackSpreadVariance == "" { - return nil, errors.New("either \"callbackRatio\" or \"callbackSpread\" is allowed to be passed") + if arg.CallbackRatio == 0 && arg.CallbackSpreadVariance == 0 { + return nil, fmt.Errorf(" %w \"callbackRatio\" or \"callbackSpread\" required", errPriceTrackingNotSet) } return ok.PlaceAlgoOrder(ctx, arg) } -// PlaceIcebergOrder to place iceburg algo order +// PlaceIcebergOrder to place iceberg algo order func (ok *Okx) PlaceIcebergOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams } if arg.OrderType != "iceberg" { - return nil, errInvalidOrderType + return nil, fmt.Errorf("%w: order type value 'iceberg' is only supported for iceberg orders", order.ErrTypeIsInvalid) } if arg.SizeLimit <= 0 { return nil, errMissingSizeLimit } - if arg.PriceLimit <= 0 { + if arg.LimitPrice <= 0 { return nil, errInvalidPriceLimit } return ok.PlaceAlgoOrder(ctx, arg) @@ -839,87 +449,158 @@ func (ok *Okx) PlaceIcebergOrder(ctx context.Context, arg *AlgoOrderParams) (*Al // PlaceTWAPOrder to place TWAP algo orders func (ok *Okx) PlaceTWAPOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams + } if arg.OrderType != "twap" { - return nil, errInvalidOrderType + return nil, fmt.Errorf("%w: order type value 'twap' is only supported for twap orders", order.ErrTypeIsInvalid) } if arg.SizeLimit <= 0 { return nil, errMissingSizeLimit } - if arg.PriceLimit <= 0 { + if arg.LimitPrice <= 0 { return nil, errInvalidPriceLimit } - if ok.GetIntervalEnum(arg.TimeInterval, true) == "" { + if IntervalFromString(arg.TimeInterval, true) == "" { return nil, errMissingIntervalValue } return ok.PlaceAlgoOrder(ctx, arg) } -// TriggerAlgoOrder fetches algo trigger orders for SWAP market types. -func (ok *Okx) TriggerAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { - if arg == nil { - return nil, errNilArgument +// PlaceTakeProfitStopLossOrder places conditional and oco orders +// When placing net TP/SL order (ordType=conditional) and both take-profit and stop-loss parameters are sent, +// only stop-loss logic will be performed and take-profit logic will be ignored +func (ok *Okx) PlaceTakeProfitStopLossOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams + } + if arg.OrderType != "conditional" { + return nil, fmt.Errorf("%w for TPSL: `%s`", order.ErrTypeIsInvalid, arg.OrderType) + } + if arg.StopLossTriggerPrice <= 0 { + return nil, order.ErrPriceBelowMin + } + switch arg.StopLossTriggerPriceType { + case "", "last", "index", "mark": + default: + return nil, fmt.Errorf("%w, only 'last', 'index', and 'mark' are supported", order.ErrUnknownPriceType) + } + return ok.PlaceAlgoOrder(ctx, arg) +} + +// PlaceChaseAlgoOrder places an order that adjusts the price of an open limit order to match the current market price +func (ok *Okx) PlaceChaseAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams + } + if arg.OrderType != "chase" { + return nil, fmt.Errorf("%w: order type value 'chase' is only supported for chase orders", order.ErrTypeIsInvalid) + } + if (arg.MaxChaseType == "" || arg.MaxChaseValue == 0) && + (arg.MaxChaseType != "" || arg.MaxChaseValue != 0) { + return nil, fmt.Errorf("%w, either non or both MaxChaseType and MaxChaseValue has to be provided", errPriceTrackingNotSet) + } + return ok.PlaceAlgoOrder(ctx, arg) +} + +// PlaceTriggerAlgoOrder fetches algo trigger orders for SWAP market types +func (ok *Okx) PlaceTriggerAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) { + if *arg == (AlgoOrderParams{}) { + return nil, common.ErrEmptyParams } if arg.OrderType != "trigger" { - return nil, errInvalidOrderType + return nil, fmt.Errorf("%w for Trigger: `%s`", order.ErrTypeIsInvalid, arg.OrderType) } if arg.TriggerPrice <= 0 { - return nil, errInvalidTriggerPrice + return nil, fmt.Errorf("%w, trigger price must be greater than 0", order.ErrPriceBelowMin) } - if arg.TriggerPriceType != "" && - arg.TriggerPriceType != "last" && - arg.TriggerPriceType != "index" && - arg.TriggerPriceType != "mark" { - return nil, errors.New("only last, index and mark trigger price types are allowed") + switch arg.TriggerPriceType { + case "", "last", "index", "mark": + default: + return nil, fmt.Errorf("%w, only last, index and mark trigger price types are allowed", order.ErrUnknownPriceType) } return ok.PlaceAlgoOrder(ctx, arg) } // CancelAdvanceAlgoOrder Cancel unfilled algo orders // A maximum of 10 orders can be canceled at a time. -// Request parameters should be passed in the form of an array. -func (ok *Okx) CancelAdvanceAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams) ([]AlgoOrder, error) { - if args == nil { - return nil, errNilArgument +// Request parameters should be passed in the form of an array +func (ok *Okx) CancelAdvanceAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams) (*AlgoOrder, error) { + if len(args) == 0 { + return nil, common.ErrEmptyParams } - return ok.cancelAlgoOrder(ctx, args, cancelAdvancedAlgoOrder, cancelAdvanceAlgoOrderEPL) + return ok.cancelAlgoOrder(ctx, args, "trade/cancel-advance-algos", cancelAdvanceAlgoOrderEPL) } // CancelAlgoOrder to cancel unfilled algo orders (not including Iceberg order, TWAP order, Trailing Stop order). // A maximum of 10 orders can be canceled at a time. -// Request parameters should be passed in the form of an array. -func (ok *Okx) CancelAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams) ([]AlgoOrder, error) { - if args == nil { - return nil, errNilArgument +// Request parameters should be passed in the form of an array +func (ok *Okx) CancelAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams) (*AlgoOrder, error) { + if len(args) == 0 { + return nil, common.ErrEmptyParams } - return ok.cancelAlgoOrder(ctx, args, cancelAlgoOrder, cancelAlgoOrderEPL) + return ok.cancelAlgoOrder(ctx, args, "trade/cancel-algos", cancelAlgoOrderEPL) } -// cancelAlgoOrder to cancel unfilled algo orders. -func (ok *Okx) cancelAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams, route string, rateLimit request.EndpointLimit) ([]AlgoOrder, error) { +// cancelAlgoOrder to cancel unfilled algo orders +func (ok *Okx) cancelAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams, route string, rateLimit request.EndpointLimit) (*AlgoOrder, error) { for x := range args { - arg := args[x] - if arg.AlgoOrderID == "" { - return nil, errMissingAlgoOrderID - } else if arg.InstrumentID == "" { + if args[x] == (AlgoOrderCancelParams{}) { + return nil, common.ErrEmptyParams + } + if args[x].AlgoOrderID == "" { + return nil, fmt.Errorf("%w, AlgoOrderID is required", order.ErrOrderIDNotSet) + } else if args[x].InstrumentID == "" { return nil, errMissingInstrumentID } } - if len(args) == 0 { - return nil, errors.New("no parameter") + var resp *AlgoOrder + err := ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodPost, route, &args, &resp, request.AuthenticatedRequest) + if err != nil { + if resp != nil && resp.StatusMessage != "" { + return nil, fmt.Errorf("%w, error code: %s, error message: %s", err, resp.StatusCode, resp.StatusMessage) + } + return nil, err } - resp := []AlgoOrder{} - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodPost, route, &args, &resp, true) + return resp, nil } -// GetAlgoOrderList retrieves a list of untriggered Algo orders under the current account. -func (ok *Okx) GetAlgoOrderList(ctx context.Context, orderType, algoOrderID, clientOrderID, instrumentType, instrumentID string, after, before time.Time, limit int64) ([]AlgoOrderResponse, error) { +// AmendAlgoOrder amend unfilled algo orders (Support stop order only, not including Move_order_stop order, Trigger order, Iceberg order, TWAP order, Trailing Stop order). +// Only applicable to Futures and Perpetual swap +func (ok *Okx) AmendAlgoOrder(ctx context.Context, arg *AmendAlgoOrderParam) (*AmendAlgoResponse, error) { + if arg == nil { + return nil, common.ErrEmptyParams + } + if arg.InstrumentID == "" { + return nil, errMissingInstrumentID + } + if arg.AlgoID == "" && arg.ClientSuppliedAlgoOrderID == "" { + return nil, fmt.Errorf("%w either AlgoID or ClientSuppliedAlgoOrderID is required", order.ErrOrderIDNotSet) + } + var resp *AmendAlgoResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendAlgoOrderEPL, http.MethodPost, "trade/amend-algos", arg, &resp, request.AuthenticatedRequest) +} + +// GetAlgoOrderDetail retrieves algo order details +func (ok *Okx) GetAlgoOrderDetail(ctx context.Context, algoID, clientSuppliedAlgoID string) (*AlgoOrderDetail, error) { + if algoID == "" && clientSuppliedAlgoID == "" { + return nil, fmt.Errorf("%w either AlgoID or ClientSuppliedAlgoID is required", order.ErrOrderIDNotSet) + } params := url.Values{} + params.Set("algoId", algoID) + params.Set("algoClOrdId", clientSuppliedAlgoID) + var resp *AlgoOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderDetailEPL, http.MethodGet, common.EncodeURLValues("trade/order-algo", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetAlgoOrderList retrieves a list of untriggered Algo orders under the current account +func (ok *Okx) GetAlgoOrderList(ctx context.Context, orderType, algoOrderID, clientOrderID, instrumentType, instrumentID string, after, before time.Time, limit int64) ([]AlgoOrderResponse, error) { orderType = strings.ToLower(orderType) if orderType == "" { - return nil, errors.New("order type is required") + return nil, order.ErrTypeIsInvalid } + params := url.Values{} params.Set("ordType", orderType) - var resp []AlgoOrderResponse if algoOrderID != "" { params.Set("algoId", algoOrderID) } @@ -942,23 +623,20 @@ func (ok *Okx) GetAlgoOrderList(ctx context.Context, orderType, algoOrderID, cli if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderListEPL, http.MethodGet, common.EncodeURLValues(getAlgoOrders, params), nil, &resp, true) + var resp []AlgoOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderListEPL, http.MethodGet, common.EncodeURLValues("trade/orders-algo-pending", params), nil, &resp, request.AuthenticatedRequest) } -// GetAlgoOrderHistory load a list of all algo orders under the current account in the last 3 months. +// GetAlgoOrderHistory load a list of all algo orders under the current account in the last 3 months func (ok *Okx) GetAlgoOrderHistory(ctx context.Context, orderType, state, algoOrderID, instrumentType, instrumentID string, after, before time.Time, limit int64) ([]AlgoOrderResponse, error) { - params := url.Values{} if orderType == "" { - return nil, errors.New("order type is required") + return nil, order.ErrTypeIsInvalid } - params.Set("ordType", strings.ToLower(orderType)) - var resp []AlgoOrderResponse - if algoOrderID == "" && - state != "effective" && - state != "order_failed" && - state != "canceled" { + if algoOrderID == "" && !slices.Contains([]string{"effective", "order_failed", "canceled"}, state) { return nil, errMissingEitherAlgoIDOrState } + params := url.Values{} + params.Set("ordType", strings.ToLower(orderType)) if algoOrderID != "" { params.Set("algoId", algoOrderID) } else { @@ -980,35 +658,34 @@ func (ok *Okx) GetAlgoOrderHistory(ctx context.Context, orderType, state, algoOr if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderHistoryEPL, http.MethodGet, common.EncodeURLValues(algoOrderHistory, params), nil, &resp, true) + var resp []AlgoOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("trade/orders-algo-history", params), nil, &resp, request.AuthenticatedRequest) } -// GetEasyConvertCurrencyList retrieve list of small convertibles and mainstream currencies. Only applicable to the crypto balance less than $10. -func (ok *Okx) GetEasyConvertCurrencyList(ctx context.Context) (*EasyConvertDetail, error) { - var resp []EasyConvertDetail - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getEasyConvertCurrencyListEPL, http.MethodGet, - easyConvertCurrencyList, nil, &resp, true); err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil +// GetEasyConvertCurrencyList retrieve list of small convertibles and mainstream currencies. Only applicable to the crypto balance less than $10 +func (ok *Okx) GetEasyConvertCurrencyList(ctx context.Context, source string) (*EasyConvertDetail, error) { + params := url.Values{} + if source != "" { + params.Set("source", source) } - return nil, errNoValidResponseFromServer + var resp *EasyConvertDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEasyConvertCurrencyListEPL, http.MethodGet, + common.EncodeURLValues("trade/easy-convert-currency-list", params), nil, &resp, request.AuthenticatedRequest) } -// PlaceEasyConvert converts small currencies to mainstream currencies. Only applicable to the crypto balance less than $10. +// PlaceEasyConvert converts small currencies to mainstream currencies. Only applicable to the crypto balance less than $10 func (ok *Okx) PlaceEasyConvert(ctx context.Context, arg PlaceEasyConvertParam) ([]EasyConvertItem, error) { if len(arg.FromCurrency) == 0 { - return nil, fmt.Errorf("%w, missing 'fromCcy'", errMissingRequiredParameter) + return nil, fmt.Errorf("%w, missing FromCurrency", currency.ErrCurrencyCodeEmpty) } if arg.ToCurrency == "" { - return nil, fmt.Errorf("%w, missing 'toCcy'", errMissingRequiredParameter) + return nil, fmt.Errorf("%w, missing ToCurrency", currency.ErrCurrencyCodeEmpty) } var resp []EasyConvertItem - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeEasyConvertEPL, http.MethodPost, easyConvert, &arg, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeEasyConvertEPL, http.MethodPost, "trade/easy-convert", &arg, &resp, request.AuthenticatedRequest) } -// GetEasyConvertHistory retrieves the history and status of easy convert trades. +// GetEasyConvertHistory retrieves the history and status of easy convert trades func (ok *Okx) GetEasyConvertHistory(ctx context.Context, after, before time.Time, limit int64) ([]EasyConvertItem, error) { params := url.Values{} if !before.IsZero() { @@ -1021,7 +698,8 @@ func (ok *Okx) GetEasyConvertHistory(ctx context.Context, after, before time.Tim params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []EasyConvertItem - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEasyConvertHistoryEPL, http.MethodGet, easyConvertHistoryPath, nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEasyConvertHistoryEPL, http.MethodGet, + common.EncodeURLValues("trade/easy-convert-history", params), nil, &resp, request.AuthenticatedRequest) } // GetOneClickRepayCurrencyList retrieves list of debt currency data and repay currencies. Debt currencies include both cross and isolated debts. @@ -1032,22 +710,23 @@ func (ok *Okx) GetOneClickRepayCurrencyList(ctx context.Context, debtType string params.Set("debtType", debtType) } var resp []CurrencyOneClickRepay - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, oneClickRepayCurrencyListEPL, http.MethodGet, common.EncodeURLValues(oneClickRepayCurrencyListPath, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, oneClickRepayCurrencyListEPL, http.MethodGet, + common.EncodeURLValues("trade/one-click-repay-currency-list", params), nil, &resp, request.AuthenticatedRequest) } -// TradeOneClickRepay trade one-click repay to repay cross debts. Isolated debts are not applicable. The maximum repayment amount is based on the remaining available balance of funding and trading accounts. +// TradeOneClickRepay trade one-click repay to repay cross debts. Isolated debts are not applicable. The maximum repayment amount is based on the remaining available balance of funding and trading accounts func (ok *Okx) TradeOneClickRepay(ctx context.Context, arg TradeOneClickRepayParam) ([]CurrencyOneClickRepay, error) { if len(arg.DebtCurrency) == 0 { - return nil, fmt.Errorf("%w, missing 'debtCcy'", errMissingRequiredParameter) + return nil, fmt.Errorf("%w, missing 'debtCcy'", currency.ErrCurrencyCodeEmpty) } if arg.RepayCurrency == "" { - return nil, fmt.Errorf("%w, missing 'repayCcy'", errMissingRequiredParameter) + return nil, fmt.Errorf("%w, missing 'repayCcy'", currency.ErrCurrencyCodeEmpty) } var resp []CurrencyOneClickRepay - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, tradeOneClickRepayEPL, http.MethodPost, oneClickRepay, &arg, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, tradeOneClickRepayEPL, http.MethodPost, "trade/one-click-repay", &arg, &resp, request.AuthenticatedRequest) } -// GetOneClickRepayHistory get the history and status of one-click repay trades. +// GetOneClickRepayHistory get the history and status of one-click repay trades func (ok *Okx) GetOneClickRepayHistory(ctx context.Context, after, before time.Time, limit int64) ([]CurrencyOneClickRepay, error) { params := url.Values{} if !before.IsZero() { @@ -1060,149 +739,201 @@ func (ok *Okx) GetOneClickRepayHistory(ctx context.Context, after, before time.T params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []CurrencyOneClickRepay - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOneClickRepayHistoryEPL, http.MethodGet, common.EncodeURLValues(oneClickRepayHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOneClickRepayHistoryEPL, http.MethodGet, common.EncodeURLValues("trade/one-click-repay-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// CancelAllMMPOrders cancel all the MMP pending orders of an instrument family. +// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required +func (ok *Okx) CancelAllMMPOrders(ctx context.Context, instrumentType, instrumentFamily string, lockInterval int64) (*CancelMMPResponse, error) { + if instrumentType == "" { + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) + } + if instrumentFamily == "" { + return nil, errInstrumentFamilyRequired + } + if lockInterval < 0 || lockInterval > 10000 { + return nil, fmt.Errorf("%w, LockInterval value range should be between 0 and 10000", errMissingIntervalValue) + } + arg := &struct { + InstrumentType string `json:"instType,omitempty"` + InstrumentFamily string `json:"instFamily,omitempty"` + LockInterval int64 `json:"lockInterval,omitempty"` + }{ + InstrumentType: instrumentType, + InstrumentFamily: instrumentFamily, + LockInterval: lockInterval, + } + var resp *CancelMMPResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, tradeOneClickRepayEPL, http.MethodPost, "trade/mass-cancel", arg, &resp, request.AuthenticatedRequest) +} + +// CancelAllDelayed cancel all pending orders after the countdown timeout. +// Applicable to all trading symbols through order book (except Spread trading) +func (ok *Okx) CancelAllDelayed(ctx context.Context, timeout int64, orderTag string) (*CancelResponse, error) { + if (timeout != 0) && (timeout < 10 || timeout > 120) { + return nil, fmt.Errorf("%w, Range of value can be 0, [10, 120]", errCountdownTimeoutRequired) + } + arg := &struct { + TimeOut int64 `json:"timeOut,string"` + OrderTag string `json:"tag,omitempty"` + }{ + TimeOut: timeout, + OrderTag: orderTag, + } + var resp *CancelResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllAfterCountdownEPL, http.MethodPost, "trade/cancel-all-after", arg, &resp, request.AuthenticatedRequest) +} + +// GetTradeAccountRateLimit get account rate limit related information. +// Only new order requests and amendment order requests will be counted towards this limit. For batch order requests consisting of multiple orders, each order will be counted individually +func (ok *Okx) GetTradeAccountRateLimit(ctx context.Context) (*AccountRateLimit, error) { + var resp *AccountRateLimit + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTradeAccountRateLimitEPL, http.MethodGet, "trade/account-rate-limit", nil, &resp, request.AuthenticatedRequest) +} + +// PreCheckOrder returns the account information before and after placing a potential order +// Only applicable to Multi-currency margin mode, and Portfolio margin mode +func (ok *Okx) PreCheckOrder(ctx context.Context, arg *OrderPreCheckParams) (*OrderPreCheckResponse, error) { + if arg == nil { + return nil, common.ErrNilPointer + } + if arg.InstrumentID == "" { + return nil, errMissingInstrumentID + } + if arg.TradeMode == "" { + return nil, errInvalidTradeModeValue + } + if arg.Side == "" { + return nil, order.ErrSideIsInvalid + } + if arg.OrderType == "" { + return nil, order.ErrTypeIsInvalid + } + if arg.Size <= 0 { + return nil, order.ErrAmountBelowMin + } + var resp *OrderPreCheckResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, orderPreCheckEPL, http.MethodPost, "trade/order-precheck", arg, &resp, request.AuthenticatedRequest) } /*************************************** Block trading ********************************/ -// GetCounterparties retrieves the list of counterparties that the user has permissions to trade with. +// GetCounterparties retrieves the list of counterparties that the user has permissions to trade with func (ok *Okx) GetCounterparties(ctx context.Context) ([]CounterpartiesResponse, error) { var resp []CounterpartiesResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getCounterpartiesEPL, http.MethodGet, rfqCounterparties, nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getCounterpartiesEPL, http.MethodGet, "rfq/counterparties", nil, &resp, request.AuthenticatedRequest) } -// CreateRfq Creates a new Rfq -func (ok *Okx) CreateRfq(ctx context.Context, arg CreateRfqInput) (*RfqResponse, error) { +// CreateRFQ Creates a new RFQ +func (ok *Okx) CreateRFQ(ctx context.Context, arg *CreateRFQInput) (*RFQResponse, error) { if len(arg.CounterParties) == 0 { return nil, errInvalidCounterParties } if len(arg.Legs) == 0 { - return nil, errInvalidLegs - } - var resp []RfqResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, createRfqEPL, http.MethodPost, rfqCreateRfq, &arg, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil + return nil, errMissingLegs } - return nil, errNoValidResponseFromServer + var resp *RFQResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, createRFQEPL, http.MethodPost, "rfq/create-rfq", &arg, &resp, request.AuthenticatedRequest) } -// CancelRfq Cancel an existing active Rfq that you has previously created. -func (ok *Okx) CancelRfq(ctx context.Context, arg CancelRfqRequestParam) (*CancelRfqResponse, error) { - if arg.RfqID == "" && arg.ClientRfqID == "" { - return nil, errMissingRfqIDAndClientRfqID +// CancelRFQ cancels a request for quotation +func (ok *Okx) CancelRFQ(ctx context.Context, rfqID, clientRFQID string) (*CancelRFQResponse, error) { + if rfqID == "" && clientRFQID == "" { + return nil, order.ErrOrderIDNotSet } - var resp []CancelRfqResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelRfqEPL, http.MethodPost, rfqCancelRfq, &arg, &resp, true) - if err != nil { - return nil, err + var resp *CancelRFQResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelRFQEPL, http.MethodPost, "rfq/cancel-rfq", &CancelRFQRequestParam{ + RFQID: rfqID, + ClientRFQID: clientRFQID, + }, &resp, request.AuthenticatedRequest) +} + +// CancelMultipleRFQs cancel multiple active RFQs in a single batch. Maximum 100 RFQ orders can be canceled at a time +func (ok *Okx) CancelMultipleRFQs(ctx context.Context, arg *CancelRFQRequestsParam) ([]CancelRFQResponse, error) { + if arg == nil { + return nil, common.ErrNilPointer } - if len(resp) == 1 { - return &resp[0], nil + if len(arg.RFQIDs) == 0 && len(arg.ClientRFQIDs) == 0 { + return nil, order.ErrOrderIDNotSet + } else if len(arg.RFQIDs)+len(arg.ClientRFQIDs) > 100 { + return nil, errMaxRFQOrdersToCancel } - return nil, errNoValidResponseFromServer + var resp []CancelRFQResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleRFQEPL, http.MethodPost, "rfq/cancel-batch-rfqs", &arg, &resp, request.AuthenticatedRequest) } -// CancelMultipleRfqs cancel multiple active Rfqs in a single batch. Maximum 100 Rfq orders can be canceled at a time. -func (ok *Okx) CancelMultipleRfqs(ctx context.Context, arg CancelRfqRequestsParam) ([]CancelRfqResponse, error) { - if len(arg.RfqIDs) == 0 && len(arg.ClientRfqIDs) == 0 { - return nil, errMissingRfqIDAndClientRfqID - } else if len(arg.RfqIDs)+len(arg.ClientRfqIDs) > 100 { - return nil, errMaxRfqOrdersToCancel - } - var resp []CancelRfqResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleRfqEPL, http.MethodPost, rfqCancelRfqs, &arg, &resp, true) +// CancelAllRFQs cancels all active RFQs +func (ok *Okx) CancelAllRFQs(ctx context.Context) (types.Time, error) { + var resp types.Time + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllRFQsEPL, http.MethodPost, "rfq/cancel-all-rfqs", nil, &struct { + Timestamp *types.Time `json:"ts"` + }{Timestamp: &resp}, request.AuthenticatedRequest) } -// CancelAllRfqs cancels all active Rfqs. -func (ok *Okx) CancelAllRfqs(ctx context.Context) (time.Time, error) { - var resp []TimestampResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllRfqsEPL, http.MethodPost, rfqCancelAllRfqs, nil, &resp, true) - if err != nil { - return time.Time{}, err - } - if len(resp) == 1 { - return resp[0].Timestamp.Time(), nil +// ExecuteQuote executes a Quote. It is only used by the creator of the RFQ +func (ok *Okx) ExecuteQuote(ctx context.Context, rfqID, quoteID string) (*ExecuteQuoteResponse, error) { + if rfqID == "" || quoteID == "" { + return nil, errMissingRFQIDOrQuoteID } - return time.Time{}, errNoValidResponseFromServer + var resp *ExecuteQuoteResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, executeQuoteEPL, http.MethodPost, "rfq/execute-quote", &ExecuteQuoteParams{ + RFQID: rfqID, + QuoteID: quoteID, + }, &resp, request.AuthenticatedRequest) } -// ExecuteQuote executes a Quote. It is only used by the creator of the Rfq -func (ok *Okx) ExecuteQuote(ctx context.Context, arg ExecuteQuoteParams) (*ExecuteQuoteResponse, error) { - if arg.RfqID == "" || arg.QuoteID == "" { - return nil, errMissingRfqIDOrQuoteID - } - var resp []ExecuteQuoteResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, executeQuoteEPL, http.MethodPost, rfqExecuteQuote, &arg, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer +// GetQuoteProducts retrieve the products which makers want to quote and receive RFQs for, and the corresponding price and size limit +func (ok *Okx) GetQuoteProducts(ctx context.Context) ([]QuoteProduct, error) { + var resp []QuoteProduct + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getQuoteProductsEPL, http.MethodGet, "rfq/maker-instrument-settings", nil, &resp, request.AuthenticatedRequest) } -// SetQuoteProducts customize the products which makers want to quote and receive Rfqs for, and the corresponding price and size limit. +// SetQuoteProducts customize the products which makers want to quote and receive RFQs for, and the corresponding price and size limit func (ok *Okx) SetQuoteProducts(ctx context.Context, args []SetQuoteProductParam) (*SetQuoteProductsResult, error) { if len(args) == 0 { - return nil, errEmptyArgument + return nil, common.ErrEmptyParams } for x := range args { args[x].InstrumentType = strings.ToUpper(args[x].InstrumentType) - if args[x].InstrumentType != okxInstTypeSwap && - args[x].InstrumentType != okxInstTypeSpot && - args[x].InstrumentType != okxInstTypeFutures && - args[x].InstrumentType != okxInstTypeOption { + if !slices.Contains([]string{instTypeSwap, instTypeSpot, instTypeFutures, instTypeOption}, args[x].InstrumentType) { return nil, fmt.Errorf("%w received %v", errInvalidInstrumentType, args[x].InstrumentType) } if len(args[x].Data) == 0 { return nil, errMissingMakerInstrumentSettings } for y := range args[x].Data { - if (args[x].InstrumentType == okxInstTypeSwap || - args[x].InstrumentType == okxInstTypeFutures || - args[x].InstrumentType == okxInstTypeOption) && args[x].Data[y].Underlying == "" { + if slices.Contains([]string{instTypeSwap, instTypeFutures, instTypeOption}, args[x].InstrumentType) && args[x].Data[y].Underlying == "" { return nil, fmt.Errorf("%w, for instrument type %s and %s", errInvalidUnderlying, args[x].InstrumentType, args[x].Data[x].Underlying) } - if (args[x].InstrumentType == okxInstTypeSpot) && args[x].Data[x].InstrumentID == "" { + if (args[x].InstrumentType == instTypeSpot) && args[x].Data[x].InstrumentID == "" { return nil, fmt.Errorf("%w, for instrument type %s and %s", errMissingInstrumentID, args[x].InstrumentType, args[x].Data[x].InstrumentID) } } } - var resp []SetQuoteProductsResult - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, setQuoteProductsEPL, http.MethodPost, makerInstrumentSettings, &args, &resp, true); err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *SetQuoteProductsResult + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setQuoteProductsEPL, http.MethodPost, "rfq/maker-instrument-settings", &args, &resp, request.AuthenticatedRequest) } -// ResetMMPStatus reset the MMP status to be inactive. -func (ok *Okx) ResetMMPStatus(ctx context.Context) (time.Time, error) { - var resp []TimestampResponse - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, restMMPStatusEPL, http.MethodPost, mmpReset, nil, &resp, true); err != nil { - return time.Time{}, err - } - if len(resp) == 1 { - return resp[0].Timestamp.Time(), nil - } - return time.Time{}, errNoValidResponseFromServer +// ResetRFQMMPStatus reset the MMP status to be inactive +func (ok *Okx) ResetRFQMMPStatus(ctx context.Context) (time.Time, error) { + resp := &struct { + Timestamp types.Time `json:"ts"` + }{} + return resp.Timestamp.Time(), ok.SendHTTPRequest(ctx, exchange.RestSpot, resetRFQMMPEPL, http.MethodPost, "rfq/mmp-reset", nil, resp, request.AuthenticatedRequest) } -// CreateQuote allows the user to Quote an Rfq that they are a counterparty to. The user MUST quote -// the entire Rfq and not part of the legs or part of the quantity. Partial quoting or partial fills are not allowed. -func (ok *Okx) CreateQuote(ctx context.Context, arg CreateQuoteParams) (*QuoteResponse, error) { +// CreateQuote allows the user to Quote an RFQ that they are a counterparty to. The user MUST quote +// the entire RFQ and not part of the legs or part of the quantity. Partial quoting or partial fills are not allowed +func (ok *Okx) CreateQuote(ctx context.Context, arg *CreateQuoteParams) (*QuoteResponse, error) { + if arg == nil { + return nil, common.ErrNilPointer + } + arg.QuoteSide = strings.ToLower(arg.QuoteSide) switch { - case arg.RfqID == "": - return nil, errMissingRfqID - case arg.QuoteSide != order.Buy && arg.QuoteSide != order.Sell: - return nil, errInvalidOrderSide + case arg.RFQID == "": + return nil, errMissingRFQID + case arg.QuoteSide != order.Buy.Lower() && arg.QuoteSide != order.Sell.Lower(): + return nil, order.ErrSideIsInvalid case len(arg.Legs) == 0: return nil, errMissingLegs } @@ -1213,72 +944,56 @@ func (ok *Okx) CreateQuote(ctx context.Context, arg CreateQuoteParams) (*QuoteRe case arg.Legs[x].SizeOfQuoteLeg <= 0: return nil, errMissingSizeOfQuote case arg.Legs[x].Price <= 0: - return nil, errMossingLegsQuotePrice + return nil, errMissingLegsQuotePrice case arg.Legs[x].Side == order.UnknownSide: - return nil, errInvalidOrderSide + return nil, order.ErrSideIsInvalid } } - var resp []QuoteResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, createQuoteEPL, http.MethodPost, rfqCreateQuote, &arg, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *QuoteResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, createQuoteEPL, http.MethodPost, "rfq/create-quote", &arg, &resp, request.AuthenticatedRequest) } -// CancelQuote cancels an existing active quote you have created in response to an Rfq. -// rfqCancelQuote = "rfq/cancel-quote" -func (ok *Okx) CancelQuote(ctx context.Context, arg CancelQuoteRequestParams) (*CancelQuoteResponse, error) { - var resp []CancelQuoteResponse - if arg.ClientQuoteID == "" && arg.QuoteID == "" { - return nil, errMissingQuoteIDOrClientQuoteID - } - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelQuoteEPL, http.MethodPost, rfqCancelQuote, &arg, &resp, true) - if err != nil { - return nil, err +// CancelQuote cancels an existing active quote you have created in response to an RFQ +func (ok *Okx) CancelQuote(ctx context.Context, quoteID, clientQuoteID string) (*CancelQuoteResponse, error) { + if clientQuoteID == "" && quoteID == "" { + return nil, order.ErrOrderIDNotSet } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *CancelQuoteResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelQuoteEPL, http.MethodPost, "rfq/cancel-quote", &CancelQuoteRequestParams{ + QuoteID: quoteID, + ClientQuoteID: clientQuoteID, + }, &resp, request.AuthenticatedRequest) } -// CancelMultipleQuote cancel multiple active Quotes in a single batch. Maximum 100 quote orders can be canceled at a time. +// CancelMultipleQuote cancels multiple active quotes in a single batch, with a maximum of 100 quote orders cancellable at once func (ok *Okx) CancelMultipleQuote(ctx context.Context, arg CancelQuotesRequestParams) ([]CancelQuoteResponse, error) { if len(arg.QuoteIDs) == 0 && len(arg.ClientQuoteIDs) == 0 { - return nil, errMissingEitherQuoteIDAOrClientQuoteIDs + return nil, order.ErrOrderIDNotSet } var resp []CancelQuoteResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleQuotesEPL, http.MethodPost, rfqCancelBatchQuotes, &arg, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleQuotesEPL, http.MethodPost, "rfq/cancel-batch-quotes", &arg, &resp, request.AuthenticatedRequest) } -// CancelAllQuotes cancels all active Quotes. -func (ok *Okx) CancelAllQuotes(ctx context.Context) (time.Time, error) { - var resp []TimestampResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllQuotesEPL, http.MethodPost, rfqCancelAllQuotes, nil, &resp, true) - if err != nil { - return time.Time{}, err - } - if len(resp) == 1 { - return resp[0].Timestamp.Time(), nil - } - return time.Time{}, errMissingResponseBody +// CancelAllRFQQuotes cancels all active quote orders +func (ok *Okx) CancelAllRFQQuotes(ctx context.Context) (types.Time, error) { + var resp types.Time + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllQuotesEPL, http.MethodPost, "rfq/cancel-all-quotes", nil, + &struct { + Timestamp *types.Time `json:"ts"` + }{Timestamp: &resp}, request.AuthenticatedRequest) } -// GetRfqs retrieves details of Rfqs that the user is a counterparty to (either as the creator or the receiver of the Rfq). -func (ok *Okx) GetRfqs(ctx context.Context, arg *RfqRequestParams) ([]RfqResponse, error) { - if arg == nil { - return nil, errNilArgument +// GetRFQs retrieves details of RFQs where the user is a counterparty, either as the creator or the recipient +func (ok *Okx) GetRFQs(ctx context.Context, arg *RFQRequestParams) ([]RFQResponse, error) { + if *arg == (RFQRequestParams{}) { + return nil, common.ErrEmptyParams } params := url.Values{} - if arg.RfqID != "" { - params.Set("rfqId", arg.RfqID) + if arg.RFQID != "" { + params.Set("rfqId", arg.RFQID) } - if arg.ClientRfqID != "" { - params.Set("clRfqId", arg.ClientRfqID) + if arg.ClientRFQID != "" { + params.Set("clRFQId", arg.ClientRFQID) } if arg.State != "" { params.Set("state", strings.ToLower(arg.State)) @@ -1292,21 +1007,21 @@ func (ok *Okx) GetRfqs(ctx context.Context, arg *RfqRequestParams) ([]RfqRespons if arg.Limit > 0 { params.Set("limit", strconv.FormatInt(arg.Limit, 10)) } - var resp []RfqResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getRfqsEPL, http.MethodGet, common.EncodeURLValues(rfqRfqs, params), nil, &resp, true) + var resp []RFQResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getRFQsEPL, http.MethodGet, common.EncodeURLValues("rfq/rfqs", params), nil, &resp, request.AuthenticatedRequest) } -// GetQuotes retrieves all Quotes that the user is a counterparty to (either as the creator or the receiver). +// GetQuotes retrieves all Quotes where the user is a counterparty, either as the creator or the receiver func (ok *Okx) GetQuotes(ctx context.Context, arg *QuoteRequestParams) ([]QuoteResponse, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (QuoteRequestParams{}) { + return nil, common.ErrEmptyParams } params := url.Values{} - if arg.RfqID != "" { - params.Set("rfqId", arg.RfqID) + if arg.RFQID != "" { + params.Set("rfqId", arg.RFQID) } - if arg.ClientRfqID != "" { - params.Set("clRfqId", arg.ClientRfqID) + if arg.ClientRFQID != "" { + params.Set("clRFQId", arg.ClientRFQID) } if arg.QuoteID != "" { params.Set("quoteId", arg.QuoteID) @@ -1327,20 +1042,20 @@ func (ok *Okx) GetQuotes(ctx context.Context, arg *QuoteRequestParams) ([]QuoteR params.Set("limit", strconv.FormatInt(arg.Limit, 10)) } var resp []QuoteResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getQuotesEPL, http.MethodGet, common.EncodeURLValues(rfqQuotes, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getQuotesEPL, http.MethodGet, common.EncodeURLValues("rfq/quotes", params), nil, &resp, request.AuthenticatedRequest) } -// GetRfqTrades retrieves the executed trades that the user is a counterparty to (either as the creator or the receiver). -func (ok *Okx) GetRfqTrades(ctx context.Context, arg *RfqTradesRequestParams) ([]RfqTradeResponse, error) { - if arg == nil { - return nil, errNilArgument +// GetRFQTrades retrieves executed trades where the user is a counterparty, either as the creator or the receiver +func (ok *Okx) GetRFQTrades(ctx context.Context, arg *RFQTradesRequestParams) ([]RFQTradeResponse, error) { + if *arg == (RFQTradesRequestParams{}) { + return nil, common.ErrEmptyParams } params := url.Values{} - if arg.RfqID != "" { - params.Set("rfqId", arg.RfqID) + if arg.RFQID != "" { + params.Set("rfqId", arg.RFQID) } - if arg.ClientRfqID != "" { - params.Set("clRfqId", arg.ClientRfqID) + if arg.ClientRFQID != "" { + params.Set("clRFQId", arg.ClientRFQID) } if arg.QuoteID != "" { params.Set("quoteId", arg.QuoteID) @@ -1363,12 +1078,12 @@ func (ok *Okx) GetRfqTrades(ctx context.Context, arg *RfqTradesRequestParams) ([ if arg.Limit > 0 { params.Set("limit", strconv.FormatInt(arg.Limit, 10)) } - var resp []RfqTradeResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTradesEPL, http.MethodGet, common.EncodeURLValues(rfqTrades, params), nil, &resp, true) + var resp []RFQTradeResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTradesEPL, http.MethodGet, common.EncodeURLValues("rfq/trades", params), nil, &resp, request.AuthenticatedRequest) } -// GetPublicBlockTrades retrieves the recent executed block trades. -func (ok *Okx) GetPublicBlockTrades(ctx context.Context, beginID, endID string, limit int64) ([]PublicBlockTradesResponse, error) { +// GetPublicRFQTrades retrieves recent executed block trades +func (ok *Okx) GetPublicRFQTrades(ctx context.Context, beginID, endID string, limit int64) ([]PublicTradesResponse, error) { params := url.Values{} if beginID != "" { params.Set("beginId", beginID) @@ -1379,70 +1094,81 @@ func (ok *Okx) GetPublicBlockTrades(ctx context.Context, beginID, endID string, if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - var resp []PublicBlockTradesResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPublicTradesEPL, http.MethodGet, common.EncodeURLValues(rfqPublicTrades, params), nil, &resp, false) + var resp []PublicTradesResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPublicTradesEPL, http.MethodGet, common.EncodeURLValues("rfq/public-trades", params), nil, &resp, request.UnauthenticatedRequest) } /*************************************** Funding Tradings ********************************/ -// GetFundingCurrencies Retrieve a list of all currencies. -func (ok *Okx) GetFundingCurrencies(ctx context.Context) ([]CurrencyResponse, error) { +// GetFundingCurrencies retrieve a list of all currencies +func (ok *Okx) GetFundingCurrencies(ctx context.Context, ccy currency.Code) ([]CurrencyResponse, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } var resp []CurrencyResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getCurrenciesEPL, http.MethodGet, assetCurrencies, nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getCurrenciesEPL, http.MethodGet, + common.EncodeURLValues("asset/currencies", params), nil, &resp, request.AuthenticatedRequest) } -// GetBalance retrieves the funding account balances of all the assets and the amount that is available or on hold. -func (ok *Okx) GetBalance(ctx context.Context, currency string) ([]AssetBalance, error) { +// GetBalance retrieves the funding account balances of all the assets and the amount that is available or on hold +func (ok *Okx) GetBalance(ctx context.Context, ccy currency.Code) ([]AssetBalance, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } var resp []AssetBalance + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBalanceEPL, http.MethodGet, common.EncodeURLValues("asset/balances", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetNonTradableAssets retrieves non tradable assets +func (ok *Okx) GetNonTradableAssets(ctx context.Context, ccy currency.Code) ([]NonTradableAsset, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBalanceEPL, http.MethodGet, common.EncodeURLValues(assetBalance, params), nil, &resp, true) + var resp []NonTradableAsset + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getNonTradableAssetsEPL, http.MethodGet, common.EncodeURLValues("asset/non-tradable-assets", params), nil, &resp, request.AuthenticatedRequest) } // GetAccountAssetValuation view account asset valuation -func (ok *Okx) GetAccountAssetValuation(ctx context.Context, currency string) ([]AccountAssetValuation, error) { +func (ok *Okx) GetAccountAssetValuation(ctx context.Context, ccy currency.Code) ([]AccountAssetValuation, error) { params := url.Values{} - currency = strings.ToUpper(currency) - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.Upper().String()) } var resp []AccountAssetValuation - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountAssetValuationEPL, http.MethodGet, common.EncodeURLValues(assetValuation, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountAssetValuationEPL, http.MethodGet, common.EncodeURLValues("asset/asset-valuation", params), nil, &resp, request.AuthenticatedRequest) } // FundingTransfer transfer of funds between your funding account and trading account, -// and from the master account to sub-accounts. +// and from the master account to sub-accounts func (ok *Okx) FundingTransfer(ctx context.Context, arg *FundingTransferRequestInput) ([]FundingTransferResponse, error) { - var resp []FundingTransferResponse - if arg == nil { - return nil, errors.New("argument can not be null") + if *arg == (FundingTransferRequestInput{}) { + return nil, common.ErrEmptyParams } if arg.Amount <= 0 { - return nil, errors.New("invalid funding amount") - } - if arg.Currency == "" { - return nil, errors.New("invalid currency value") + return nil, fmt.Errorf("%w, funding amount must be greater than 0", order.ErrAmountBelowMin) } - if arg.From != "6" && arg.From != "18" { - return nil, errors.New("missing funding source field \"From\", only '6' and '18' are supported") + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } - if arg.To == "" { - return nil, errors.New("missing funding destination field \"To\", only '6' and '18' are supported") + if arg.RemittingAccountType != "6" && arg.RemittingAccountType != "18" { + return nil, fmt.Errorf("%w, remitting account type 6: Funding account 18: Trading account", errAddressRequired) } - if arg.From == arg.To { - return nil, errors.New("parameter 'from' can not equal to parameter 'to'") + if arg.BeneficiaryAccountType != "6" && arg.BeneficiaryAccountType != "18" { + return nil, fmt.Errorf("%w, beneficiary account type 6: Funding account 18: Trading account", errAddressRequired) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, fundsTransferEPL, http.MethodPost, assetTransfer, arg, &resp, true) + var resp []FundingTransferResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, fundsTransferEPL, http.MethodPost, "asset/transfer", arg, &resp, request.AuthenticatedRequest) } -// GetFundsTransferState get funding rate response. +// GetFundsTransferState get funding rate response func (ok *Okx) GetFundsTransferState(ctx context.Context, transferID, clientID string, transferType int64) ([]TransferFundRateResponse, error) { - params := url.Values{} if transferID == "" && clientID == "" { - return nil, errors.New("either 'transfer id' or 'client id' is required") + return nil, fmt.Errorf("%w, 'transfer id' or 'client id' is required", order.ErrOrderIDNotSet) } + params := url.Values{} if transferID != "" { params.Set("transId", transferID) } @@ -1453,18 +1179,18 @@ func (ok *Okx) GetFundsTransferState(ctx context.Context, transferID, clientID s params.Set("type", strconv.FormatInt(transferType, 10)) } var resp []TransferFundRateResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundsTransferStateEPL, http.MethodGet, common.EncodeURLValues(assetTransferState, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundsTransferStateEPL, http.MethodGet, common.EncodeURLValues("asset/transfer-state", params), nil, &resp, request.AuthenticatedRequest) } -// GetAssetBillsDetails Query the billing record, you can get the latest 1 month historical data -func (ok *Okx) GetAssetBillsDetails(ctx context.Context, currency, clientID string, after, before time.Time, billType, limit int64) ([]AssetBillDetail, error) { +// GetAssetBillsDetails query the billing record, you can get the latest 1 month historical data +// Bills type possible values are listed here: https://www.okx.com/docs-v5/en/#funding-account-rest-api-asset-bills-details +func (ok *Okx) GetAssetBillsDetails(ctx context.Context, ccy currency.Code, clientID string, after, before time.Time, billType, limit int64) ([]AssetBillDetail, error) { params := url.Values{} - billTypeMap := map[int64]bool{1: true, 2: true, 13: true, 20: true, 21: true, 28: true, 47: true, 48: true, 49: true, 50: true, 51: true, 52: true, 53: true, 54: true, 61: true, 68: true, 69: true, 72: true, 73: true, 74: true, 75: true, 76: true, 77: true, 78: true, 79: true, 80: true, 81: true, 82: true, 83: true, 84: true, 85: true, 86: true, 87: true, 88: true, 89: true, 90: true, 91: true, 92: true, 93: true, 94: true, 95: true, 96: true, 97: true, 98: true, 99: true, 102: true, 103: true, 104: true, 105: true, 106: true, 107: true, 108: true, 109: true, 110: true, 111: true, 112: true, 113: true, 114: true, 115: true, 116: true, 117: true, 118: true, 119: true, 120: true, 121: true, 122: true, 123: true, 124: true, 125: true, 126: true, 127: true, 128: true, 129: true, 130: true, 131: true, 132: true, 133: true, 134: true, 135: true, 136: true, 137: true, 138: true, 139: true, 141: true, 142: true, 143: true, 144: true, 145: true, 146: true, 147: true, 150: true, 151: true, 152: true, 153: true, 154: true, 155: true, 156: true, 157: true, 160: true, 161: true, 162: true, 163: true, 169: true, 170: true, 171: true, 172: true, 173: true, 174: true, 175: true, 176: true, 177: true, 178: true, 179: true, 180: true, 181: true, 182: true, 183: true, 184: true, 185: true, 186: true, 187: true, 188: true, 189: true, 193: true, 194: true, 195: true, 196: true, 197: true, 198: true, 199: true, 200: true, 211: true} - if _, okay := billTypeMap[billType]; okay { + if billType > 0 { params.Set("type", strconv.FormatInt(billType, 10)) } - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if clientID != "" { params.Set("clientId", clientID) @@ -1479,51 +1205,58 @@ func (ok *Okx) GetAssetBillsDetails(ctx context.Context, currency, clientID stri params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []AssetBillDetail - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, assetBillsDetailsEPL, http.MethodGet, common.EncodeURLValues(assetBills, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, assetBillsDetailsEPL, http.MethodGet, common.EncodeURLValues("asset/bills", params), nil, &resp, request.AuthenticatedRequest) } // GetLightningDeposits users can create up to 10 thousand different invoices within 24 hours. -// this method fetches list of lightning deposits filtered by a currency and amount. -func (ok *Okx) GetLightningDeposits(ctx context.Context, currency string, amount float64, to int64) ([]LightningDepositItem, error) { - params := url.Values{} - if currency == "" { - return nil, errInvalidCurrencyValue +// this method fetches list of lightning deposits filtered by a currency and amount +func (ok *Okx) GetLightningDeposits(ctx context.Context, ccy currency.Code, amount float64, to int64) ([]LightningDepositItem, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } - params.Set("ccy", currency) + params := url.Values{} + params.Set("ccy", ccy.String()) if amount <= 0 { - return nil, errInvalidDepositAmount + return nil, order.ErrAmountBelowMin } params.Set("amt", strconv.FormatFloat(amount, 'f', 0, 64)) if to == 6 || to == 18 { params.Set("to", strconv.FormatInt(to, 10)) } var resp []LightningDepositItem - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, lightningDepositsEPL, http.MethodGet, common.EncodeURLValues(lightningDeposit, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, lightningDepositsEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-lightning", params), nil, &resp, request.AuthenticatedRequest) } -// GetCurrencyDepositAddress returns the deposit address and related information for the provided currency information. -func (ok *Okx) GetCurrencyDepositAddress(ctx context.Context, currency string) ([]CurrencyDepositResponseItem, error) { - params := url.Values{} - if currency == "" { - return nil, errInvalidCurrencyValue +// GetCurrencyDepositAddress retrieve the deposit addresses of currencies, including previously-used addresses +func (ok *Okx) GetCurrencyDepositAddress(ctx context.Context, ccy currency.Code) ([]CurrencyDepositResponseItem, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } - params.Set("ccy", currency) + params := url.Values{} + params.Set("ccy", ccy.String()) var resp []CurrencyDepositResponseItem - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDepositAddressEPL, http.MethodGet, common.EncodeURLValues(assetDeposits, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDepositAddressEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-address", params), nil, &resp, request.AuthenticatedRequest) } // GetCurrencyDepositHistory retrieves deposit records and withdrawal status information depending on the currency, timestamp, and chronological order. -func (ok *Okx) GetCurrencyDepositHistory(ctx context.Context, currency, depositID, transactionID string, after, before time.Time, state, limit int64) ([]DepositHistoryResponseItem, error) { +// Possible deposit 'type' are Deposit Type '3': internal transfer '4': deposit from chain +func (ok *Okx) GetCurrencyDepositHistory(ctx context.Context, ccy currency.Code, depositID, transactionID, fromWithdrawalID, depositType string, after, before time.Time, state, limit int64) ([]DepositHistoryResponseItem, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if depositID != "" { params.Set("depId", depositID) } + if fromWithdrawalID != "" { + params.Set("fromWdId", fromWithdrawalID) + } if transactionID != "" { params.Set("txId", transactionID) } + if depositType != "" { + params.Set("type", depositType) + } params.Set("state", strconv.FormatInt(state, 10)) if !after.IsZero() { params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) @@ -1535,57 +1268,47 @@ func (ok *Okx) GetCurrencyDepositHistory(ctx context.Context, currency, depositI params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []DepositHistoryResponseItem - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDepositHistoryEPL, http.MethodGet, common.EncodeURLValues(pathToAssetDepositHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDepositHistoryEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-history", params), nil, &resp, request.AuthenticatedRequest) } -// Withdrawal to perform a withdrawal action. Sub-account does not support withdrawal. -func (ok *Okx) Withdrawal(ctx context.Context, input *WithdrawalInput) (*WithdrawalResponse, error) { - if input == nil { - return nil, errNilArgument +// Withdrawal to perform a withdrawal action. Sub-account does not support withdrawal +func (ok *Okx) Withdrawal(ctx context.Context, arg *WithdrawalInput) (*WithdrawalResponse, error) { + if *arg == (WithdrawalInput{}) { + return nil, common.ErrEmptyParams } - var resp []WithdrawalResponse switch { - case input.Currency == "": - return nil, errInvalidCurrencyValue - case input.Amount <= 0: - return nil, errors.New("invalid withdrawal amount") - case input.WithdrawalDestination == "": - return nil, errors.New("missing withdrawal destination") - case input.ToAddress == "": - return nil, errors.New("missing verified digital currency address \"toAddr\" information") - } - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, withdrawalEPL, http.MethodPost, assetWithdrawal, &input, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil + case arg.Currency.IsEmpty(): + return nil, currency.ErrCurrencyCodeEmpty + case arg.Amount <= 0: + return nil, fmt.Errorf("%w, withdrawal amount required", order.ErrAmountBelowMin) + case arg.WithdrawalDestination == "": + return nil, fmt.Errorf("%w, withdrawal destination required", errAddressRequired) + case arg.ToAddress == "": + return nil, fmt.Errorf("%w, missing verified digital currency address \"toAddr\" information", errAddressRequired) } - return nil, errNoValidResponseFromServer + var resp *WithdrawalResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, withdrawalEPL, http.MethodPost, "asset/withdrawal", &arg, &resp, request.AuthenticatedRequest) } /* This API function service is only open to some users. If you need this function service, please send an email to `liz.jensen@okg.com` to apply */ -// LightningWithdrawal to withdraw a currency from an invoice. -func (ok *Okx) LightningWithdrawal(ctx context.Context, arg LightningWithdrawalRequestInput) (*LightningWithdrawalResponse, error) { - if arg.Currency == "" { - return nil, errInvalidCurrencyValue - } else if arg.Invoice == "" { - return nil, errors.New("missing invoice text") +// LightningWithdrawal to withdraw a currency from an invoice +func (ok *Okx) LightningWithdrawal(ctx context.Context, arg *LightningWithdrawalRequestInput) (*LightningWithdrawalResponse, error) { + if *arg == (LightningWithdrawalRequestInput{}) { + return nil, common.ErrEmptyParams } - var resp []LightningWithdrawalResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, lightningWithdrawalsEPL, http.MethodPost, assetLightningWithdrawal, &arg, &resp, true) - if err != nil { - return nil, err - } else if len(resp) == 1 { - return &resp[0], nil + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } else if arg.Invoice == "" { + return nil, errInvoiceTextMissing } - return nil, errNoValidResponseFromServer + var resp *LightningWithdrawalResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, lightningWithdrawalsEPL, http.MethodPost, "asset/withdrawal-lightning", &arg, &resp, request.AuthenticatedRequest) } -// CancelWithdrawal You can cancel normal withdrawal, but can not cancel the withdrawal on Lightning. +// CancelWithdrawal cancels a normal withdrawal request but cannot be used to cancel Lightning withdrawals func (ok *Okx) CancelWithdrawal(ctx context.Context, withdrawalID string) (string, error) { if withdrawalID == "" { return "", errMissingValidWithdrawalID @@ -1593,19 +1316,19 @@ func (ok *Okx) CancelWithdrawal(ctx context.Context, withdrawalID string) (strin type withdrawData struct { WithdrawalID string `json:"wdId"` } - request := &withdrawData{ + arg := &withdrawData{ WithdrawalID: withdrawalID, } var response withdrawData - return response.WithdrawalID, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelWithdrawalEPL, http.MethodPost, cancelWithdrawal, request, &response, true) + return response.WithdrawalID, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelWithdrawalEPL, http.MethodPost, "asset/cancel-withdrawal", arg, &response, request.AuthenticatedRequest) } // GetWithdrawalHistory retrieves the withdrawal records according to the currency, withdrawal status, and time range in reverse chronological order. -// The 100 most recent records are returned by default. -func (ok *Okx) GetWithdrawalHistory(ctx context.Context, currency, withdrawalID, clientID, transactionID, state string, after, before time.Time, limit int64) ([]WithdrawalHistoryResponse, error) { +// The 100 most recent records are returned by default +func (ok *Okx) GetWithdrawalHistory(ctx context.Context, ccy currency.Code, withdrawalID, clientID, transactionID, state string, after, before time.Time, limit int64) ([]WithdrawalHistoryResponse, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if withdrawalID != "" { params.Set("wdId", withdrawalID) @@ -1629,65 +1352,82 @@ func (ok *Okx) GetWithdrawalHistory(ctx context.Context, currency, withdrawalID, params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []WithdrawalHistoryResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getWithdrawalHistoryEPL, http.MethodGet, common.EncodeURLValues(withdrawalHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getWithdrawalHistoryEPL, http.MethodGet, common.EncodeURLValues("asset/withdrawal-history", params), nil, &resp, request.AuthenticatedRequest) } -// SmallAssetsConvert Convert small assets in funding account to OKB. Only one convert is allowed within 24 hours. -func (ok *Okx) SmallAssetsConvert(ctx context.Context, currency []string) (*SmallAssetConvertResponse, error) { - input := map[string][]string{"ccy": currency} - var resp []SmallAssetConvertResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, smallAssetsConvertEPL, http.MethodPost, smallAssetsConvert, input, &resp, true) - if err != nil { - return nil, err +// GetDepositWithdrawalStatus retrieves the detailed status and estimated completion time for deposits and withdrawals +func (ok *Okx) GetDepositWithdrawalStatus(ctx context.Context, ccy currency.Code, withdrawalID, transactionID, addressTo, chain string) ([]DepositWithdrawStatus, error) { + if withdrawalID == "" && transactionID == "" { + return nil, fmt.Errorf("%w, either withdrawal id or transaction id is required", order.ErrOrderIDNotSet) } - if len(resp) == 1 { - return &resp[0], nil + if withdrawalID == "" { + return nil, errMissingValidWithdrawalID } - return nil, errNoValidResponseFromServer -} - -// GetSavingBalance returns saving balance, and only assets in the funding account can be used for saving. -func (ok *Okx) GetSavingBalance(ctx context.Context, currency string) ([]SavingBalanceResponse, error) { - var resp []SavingBalanceResponse params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + params.Set("wdId", withdrawalID) + if transactionID != "" { + params.Set("txId", transactionID) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSavingBalanceEPL, http.MethodGet, common.EncodeURLValues(financeSavingBalance, params), nil, &resp, true) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if addressTo != "" { + params.Set("to", addressTo) + } + if chain != "" { + params.Set("chain", chain) + } + var resp []DepositWithdrawStatus + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDepositWithdrawalStatusEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-withdraw-status", params), nil, &resp, request.AuthenticatedRequest) } -// SavingsPurchaseOrRedemption creates a purchase or redemption instance +// SmallAssetsConvert Convert small assets in funding account to OKB. Only one convert is allowed within 24 hours +func (ok *Okx) SmallAssetsConvert(ctx context.Context, currency []string) (*SmallAssetConvertResponse, error) { + var resp *SmallAssetConvertResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, smallAssetsConvertEPL, http.MethodPost, "asset/convert-dust-assets", map[string][]string{"ccy": currency}, &resp, request.AuthenticatedRequest) +} + +// GetPublicExchangeList retrieves exchanges +func (ok *Okx) GetPublicExchangeList(ctx context.Context) ([]ExchangeInfo, error) { + var resp []ExchangeInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPublicExchangeListEPL, http.MethodGet, "asset/exchange-list", nil, &resp, request.UnauthenticatedRequest) +} + +// GetSavingBalance returns saving balance, and only assets in the funding account can be used for saving +func (ok *Okx) GetSavingBalance(ctx context.Context, ccy currency.Code) ([]SavingBalanceResponse, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + var resp []SavingBalanceResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSavingBalanceEPL, http.MethodGet, common.EncodeURLValues("finance/savings/balance", params), nil, &resp, request.AuthenticatedRequest) +} + +// SavingsPurchaseOrRedemption creates a purchase or redemption instance func (ok *Okx) SavingsPurchaseOrRedemption(ctx context.Context, arg *SavingsPurchaseRedemptionInput) (*SavingsPurchaseRedemptionResponse, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (SavingsPurchaseRedemptionInput{}) { + return nil, common.ErrEmptyParams } arg.ActionType = strings.ToLower(arg.ActionType) switch { - case arg.Currency == "": - return nil, errInvalidCurrencyValue + case arg.Currency.IsEmpty(): + return nil, currency.ErrCurrencyCodeEmpty case arg.Amount <= 0: - return nil, errUnacceptableAmount + return nil, order.ErrAmountBelowMin case arg.ActionType != "purchase" && arg.ActionType != "redempt": - return nil, errors.New("invalid side value, side has to be either \"redempt\" or \"purchase\"") + return nil, fmt.Errorf("%w, side has to be either 'redempt' or 'purchase'", order.ErrSideIsInvalid) case arg.ActionType == "purchase" && (arg.Rate < 0.01 || arg.Rate > 3.65): - return nil, errors.New("the rate value range is between 1% (0.01) and 365% (3.65)") - } - var resp []SavingsPurchaseRedemptionResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, savingsPurchaseRedemptionEPL, http.MethodPost, financePurchaseRedempt, &arg, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil + return nil, fmt.Errorf("%w, the rate value range is between 0.01 and 3.65", errRateRequired) } - return nil, errNoValidResponseFromServer + var resp *SavingsPurchaseRedemptionResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, savingsPurchaseRedemptionEPL, http.MethodPost, "finance/savings/purchase-redempt", &arg, &resp, request.AuthenticatedRequest) } // GetLendingHistory lending history -func (ok *Okx) GetLendingHistory(ctx context.Context, currency string, before, after time.Time, limit int64) ([]LendingHistory, error) { +func (ok *Okx) GetLendingHistory(ctx context.Context, ccy currency.Code, before, after time.Time, limit int64) ([]LendingHistory, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if !before.IsZero() { params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) @@ -1699,42 +1439,38 @@ func (ok *Okx) GetLendingHistory(ctx context.Context, currency string, before, a params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []LendingHistory - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLendingHistoryEPL, http.MethodGet, common.EncodeURLValues(financeLendingHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLendingHistoryEPL, http.MethodGet, common.EncodeURLValues("finance/savings/lending-history", params), nil, &resp, request.AuthenticatedRequest) } // SetLendingRate sets an assets lending rate -func (ok *Okx) SetLendingRate(ctx context.Context, arg LendingRate) (*LendingRate, error) { - if arg.Currency == "" { - return nil, errInvalidCurrencyValue - } else if arg.Rate < 0.01 || arg.Rate > 3.65 { - return nil, errors.New("invalid lending rate value. the rate value range is between 1% (0.01) and 365% (3.65)") - } - var resp []LendingRate - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, setLendingRateEPL, http.MethodPost, financeSetLendingRate, &arg, &resp, true) - if err != nil { - return nil, err +func (ok *Okx) SetLendingRate(ctx context.Context, arg *LendingRate) (*LendingRate, error) { + if *arg == (LendingRate{}) { + return nil, common.ErrEmptyParams } - if len(resp) == 1 { - return &resp[0], nil + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } else if arg.Rate < 0.01 || arg.Rate > 3.65 { + return nil, fmt.Errorf("%w, rate value range is between 1 percent (0.01) and 365 percent (3.65)", errRateRequired) } - return nil, errNoValidResponseFromServer + var resp *LendingRate + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setLendingRateEPL, http.MethodPost, "finance/savings/set-lending-rate", &arg, &resp, request.AuthenticatedRequest) } -// GetPublicBorrowInfo returns the public borrow info. -func (ok *Okx) GetPublicBorrowInfo(ctx context.Context, currency string) ([]PublicBorrowInfo, error) { +// GetPublicBorrowInfo returns the public borrow info +func (ok *Okx) GetPublicBorrowInfo(ctx context.Context, ccy currency.Code) ([]PublicBorrowInfo, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } var resp []PublicBorrowInfo - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPublicBorrowInfoEPL, http.MethodGet, common.EncodeURLValues(financePublicBorrowInfo, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPublicBorrowInfoEPL, http.MethodGet, common.EncodeURLValues("finance/savings/lending-rate-summary", params), nil, &resp, request.UnauthenticatedRequest) } -// GetPublicBorrowHistory return list of publix borrow history. -func (ok *Okx) GetPublicBorrowHistory(ctx context.Context, currency string, before, after time.Time, limit int64) ([]PublicBorrowHistory, error) { +// GetPublicBorrowHistory return list of publix borrow history +func (ok *Okx) GetPublicBorrowHistory(ctx context.Context, ccy currency.Code, before, after time.Time, limit int64) ([]PublicBorrowHistory, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if !before.IsZero() { params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) @@ -1746,107 +1482,116 @@ func (ok *Okx) GetPublicBorrowHistory(ctx context.Context, currency string, befo params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []PublicBorrowHistory - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPublicBorrowHistoryEPL, http.MethodGet, common.EncodeURLValues(financePublicBorrowHistory, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPublicBorrowHistoryEPL, http.MethodGet, common.EncodeURLValues("finance/savings/lending-rate-history", params), nil, &resp, request.UnauthenticatedRequest) } /***********************************Convert Endpoints | Authenticated s*****************************************/ -// GetConvertCurrencies retrieves the currency conversion information. +// ApplyForMonthlyStatement requests a monthly statement for any month within the past year +func (ok *Okx) ApplyForMonthlyStatement(ctx context.Context, month string) (types.Time, error) { + if month == "" { + return types.Time{}, errMonthNameRequired + } + resp := &struct { + Timestamp types.Time `json:"ts"` + }{} + return resp.Timestamp, ok.SendHTTPRequest(ctx, exchange.RestSpot, applyForMonthlyStatementEPL, + http.MethodPost, "asset/monthly-statement", &map[string]string{"month": month}, &resp, request.AuthenticatedRequest) +} + +// GetMonthlyStatement retrieves monthly statements for the past year. +// Month is in the form of Jan, Feb, March etc. +func (ok *Okx) GetMonthlyStatement(ctx context.Context, month string) ([]MonthlyStatement, error) { + if month == "" { + return nil, errMonthNameRequired + } + params := url.Values{} + params.Set("month", month) + var resp []MonthlyStatement + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMonthlyStatementEPL, http.MethodGet, + common.EncodeURLValues("asset/monthly-statement", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetConvertCurrencies retrieves the currency conversion information func (ok *Okx) GetConvertCurrencies(ctx context.Context) ([]ConvertCurrency, error) { var resp []ConvertCurrency - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getConvertCurrenciesEPL, http.MethodGet, assetConvertCurrencies, nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getConvertCurrenciesEPL, http.MethodGet, "asset/convert/currencies", nil, &resp, request.AuthenticatedRequest) } // GetConvertCurrencyPair retrieves the currency conversion response detail given the 'currency from' and 'currency to' -func (ok *Okx) GetConvertCurrencyPair(ctx context.Context, fromCurrency, toCurrency string) (*ConvertCurrencyPair, error) { - params := url.Values{} - if fromCurrency == "" { - return nil, errors.New("missing reference currency name \"fromCcy\"") - } - if toCurrency == "" { - return nil, errors.New("missing destination currency name \"toCcy\"") - } - params.Set("fromCcy", fromCurrency) - params.Set("toCcy", toCurrency) - var resp []ConvertCurrencyPair - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getConvertCurrencyPairEPL, http.MethodGet, common.EncodeURLValues(convertCurrencyPairsPath, params), nil, &resp, true); err != nil { - return nil, err +func (ok *Okx) GetConvertCurrencyPair(ctx context.Context, fromCcy, toCcy currency.Code) (*ConvertCurrencyPair, error) { + if fromCcy.IsEmpty() { + return nil, fmt.Errorf("%w, source currency is required", currency.ErrCurrencyCodeEmpty) } - if len(resp) == 1 { - return &resp[0], nil + if toCcy.IsEmpty() { + return nil, fmt.Errorf("%w, target currency is required", currency.ErrCurrencyCodeEmpty) } - return nil, errNoValidResponseFromServer + params := url.Values{} + params.Set("fromCcy", fromCcy.String()) + params.Set("toCcy", toCcy.String()) + var resp *ConvertCurrencyPair + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getConvertCurrencyPairEPL, http.MethodGet, common.EncodeURLValues("asset/convert/currency-pair", params), nil, &resp, request.AuthenticatedRequest) } -// EstimateQuote retrieves quote estimation detail result given the base and quote currency. +// EstimateQuote retrieves quote estimation detail result given the base and quote currency func (ok *Okx) EstimateQuote(ctx context.Context, arg *EstimateQuoteRequestInput) (*EstimateQuoteResponse, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (EstimateQuoteRequestInput{}) { + return nil, common.ErrEmptyParams } - if arg.BaseCurrency == "" { - return nil, errors.New("missing base currency") + if arg.BaseCurrency.IsEmpty() { + return nil, fmt.Errorf("%w, base currency is required", currency.ErrCurrencyCodeEmpty) } - if arg.QuoteCurrency == "" { - return nil, errors.New("missing quote currency") + if arg.QuoteCurrency.IsEmpty() { + return nil, fmt.Errorf("%w, quote currency is required", currency.ErrCurrencyCodeEmpty) } arg.Side = strings.ToLower(arg.Side) - if arg.Side != order.Buy.Lower() && arg.Side != order.Sell.Lower() { - return nil, errInvalidOrderSide - } - if arg.RfqAmount <= 0 { - return nil, errors.New("missing rfq amount") - } - if arg.RfqSzCurrency == "" { - return nil, errors.New("missing rfq currency") + switch arg.Side { + case order.Buy.Lower(), order.Sell.Lower(): + default: + return nil, order.ErrSideIsInvalid } - var resp []EstimateQuoteResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, estimateQuoteEPL, http.MethodPost, assetEstimateQuote, arg, &resp, true) - if err != nil { - return nil, err + if arg.RFQAmount <= 0 { + return nil, fmt.Errorf("%w, RFQ amount required", order.ErrAmountBelowMin) } - if len(resp) == 1 { - return &resp[0], nil + if arg.RFQSzCurrency == "" { + return nil, fmt.Errorf("%w, missing RFQ currency", currency.ErrCurrencyCodeEmpty) } - return nil, errNoValidResponseFromServer + var resp *EstimateQuoteResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, estimateQuoteEPL, http.MethodPost, "asset/convert/estimate-quote", arg, &resp, request.AuthenticatedRequest) } -// ConvertTrade converts a base currency to quote currency. +// ConvertTrade converts a base currency to quote currency func (ok *Okx) ConvertTrade(ctx context.Context, arg *ConvertTradeInput) (*ConvertTradeResponse, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (ConvertTradeInput{}) { + return nil, common.ErrEmptyParams } if arg.BaseCurrency == "" { - return nil, errors.New("missing base currency") + return nil, fmt.Errorf("%w, base currency required", currency.ErrCurrencyCodeEmpty) } if arg.QuoteCurrency == "" { - return nil, errors.New("missing quote currency") + return nil, fmt.Errorf("%w, quote currency required", currency.ErrCurrencyCodeEmpty) } arg.Side = strings.ToLower(arg.Side) - if arg.Side != order.Buy.Lower() && - arg.Side != order.Sell.Lower() { - return nil, errInvalidOrderSide + switch arg.Side { + case order.Buy.Lower(), + order.Sell.Lower(): + default: + return nil, order.ErrSideIsInvalid } if arg.Size <= 0 { - return nil, errors.New("quote amount should be more than 0 and Rfq amount") + return nil, order.ErrAmountBelowMin } - if arg.SizeCurrency == "" { - return nil, errors.New("missing size currency") + if arg.SizeCurrency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } if arg.QuoteID == "" { - return nil, errors.New("missing quote id") - } - var resp []ConvertTradeResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, convertTradeEPL, http.MethodPost, assetConvertTrade, &arg, &resp, true) - if err != nil { - return nil, err + return nil, fmt.Errorf("%w, quote id required", order.ErrOrderIDNotSet) } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *ConvertTradeResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, convertTradeEPL, http.MethodPost, "asset/convert/trade", &arg, &resp, request.AuthenticatedRequest) } -// GetConvertHistory gets the recent history. +// GetConvertHistory gets the recent history func (ok *Okx) GetConvertHistory(ctx context.Context, before, after time.Time, limit int64, tag string) ([]ConvertHistory, error) { params := url.Values{} if !before.IsZero() { @@ -1862,27 +1607,61 @@ func (ok *Okx) GetConvertHistory(ctx context.Context, before, after time.Time, l params.Set("tag", tag) } var resp []ConvertHistory - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getConvertHistoryEPL, http.MethodGet, common.EncodeURLValues(assetConvertHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getConvertHistoryEPL, http.MethodGet, common.EncodeURLValues("asset/convert/history", params), nil, &resp, request.AuthenticatedRequest) } /********************************** Account endpoints ***************************************************/ +// GetAccountInstruments retrieve available instruments info of current account +func (ok *Okx) GetAccountInstruments(ctx context.Context, instrumentType asset.Item, underlying, instrumentFamily, instrumentID string) ([]AccountInstrument, error) { + if instrumentType == asset.Empty { + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) + } + params := url.Values{} + switch instrumentType { + case asset.Margin, asset.PerpetualSwap, asset.Futures: + if underlying == "" { + return nil, fmt.Errorf("%w, underlying is required", errInvalidUnderlying) + } + params.Set("uly", underlying) + case asset.Options: + if underlying == "" && instrumentFamily == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired + } + if underlying != "" { + params.Set("uly", underlying) + } + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } + } + instTypeString, err := assetTypeString(instrumentType) + if err != nil { + return nil, err + } + params.Set("instType", instTypeString) + if instrumentID != "" { + params.Set("instId", instrumentID) + } + var resp []AccountInstrument + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountInstrumentsEPL, http.MethodGet, common.EncodeURLValues("account/instruments", params), nil, &resp, request.AuthenticatedRequest) +} + // AccountBalance retrieves a list of assets (with non-zero balance), remaining balance, and available amount in the trading account. -// Interest-free quota and discount rates are public data and not displayed on the account interface. -func (ok *Okx) AccountBalance(ctx context.Context, currency string) ([]Account, error) { - var resp []Account +// Interest-free quota and discount rates are public data and not displayed on the account interface +func (ok *Okx) AccountBalance(ctx context.Context, ccy currency.Code) ([]Account, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountBalanceEPL, http.MethodGet, common.EncodeURLValues(accountBalance, params), nil, &resp, true) + var resp []Account + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountBalanceEPL, http.MethodGet, common.EncodeURLValues("account/balance", params), nil, &resp, request.AuthenticatedRequest) } -// GetPositions retrieves information on your positions. When the account is in net mode, net positions will be displayed, and when the account is in long/short mode, long or short positions will be displayed. +// GetPositions retrieves information on your positions. When the account is in net mode, net positions will be displayed, and when the account is in long/short mode, long or short positions will be displayed func (ok *Okx) GetPositions(ctx context.Context, instrumentType, instrumentID, positionID string) ([]AccountPosition, error) { params := url.Values{} if instrumentType != "" { - instrumentType = strings.ToUpper(instrumentType) params.Set("instType", instrumentType) } if instrumentID != "" { @@ -1892,13 +1671,12 @@ func (ok *Okx) GetPositions(ctx context.Context, instrumentType, instrumentID, p params.Set("posId", positionID) } var resp []AccountPosition - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPositionsEPL, http.MethodGet, common.EncodeURLValues(accountPosition, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPositionsEPL, http.MethodGet, common.EncodeURLValues("account/positions", params), nil, &resp, request.AuthenticatedRequest) } -// GetPositionsHistory retrieves the updated position data for the last 3 months. -func (ok *Okx) GetPositionsHistory(ctx context.Context, instrumentType, instrumentID, marginMode string, closePositionType, limit int64, after, before time.Time) ([]AccountPositionHistory, error) { +// GetPositionsHistory retrieves the updated position data for the last 3 months +func (ok *Okx) GetPositionsHistory(ctx context.Context, instrumentType, instrumentID, marginMode, positionID string, closePositionType, limit int64, after, before time.Time) ([]AccountPositionHistory, error) { params := url.Values{} - instrumentType = strings.ToUpper(instrumentType) if instrumentType != "" { params.Set("instType", instrumentType) } @@ -1914,6 +1692,9 @@ func (ok *Okx) GetPositionsHistory(ctx context.Context, instrumentType, instrume if closePositionType > 0 { params.Set("type", strconv.FormatInt(closePositionType, 10)) } + if positionID != "" { + params.Set("posId", positionID) + } if !after.IsZero() { params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) } @@ -1924,43 +1705,74 @@ func (ok *Okx) GetPositionsHistory(ctx context.Context, instrumentType, instrume params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []AccountPositionHistory - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPositionsHistoryEPL, http.MethodGet, common.EncodeURLValues(accountPositionHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPositionsHistoryEPL, http.MethodGet, common.EncodeURLValues("account/positions-history", params), nil, &resp, request.AuthenticatedRequest) } -// GetAccountAndPositionRisk get account and position risks. +// GetAccountAndPositionRisk get account and position risks func (ok *Okx) GetAccountAndPositionRisk(ctx context.Context, instrumentType string) ([]AccountAndPositionRisk, error) { params := url.Values{} if instrumentType != "" { params.Set("instType", strings.ToUpper(instrumentType)) } var resp []AccountAndPositionRisk - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountAndPositionRiskEPL, http.MethodGet, common.EncodeURLValues(accountAndPositionRisk, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountAndPositionRiskEPL, http.MethodGet, common.EncodeURLValues("account/account-position-risk", params), nil, &resp, request.AuthenticatedRequest) } -// GetBillsDetailLast7Days The bill refers to all transaction records that result in changing the balance of an account. Pagination is supported, and the response is sorted with the most recent first. This endpoint can retrieve data from the last 7 days. +// GetBillsDetailLast7Days The bill refers to all transaction records that result in changing the balance of an account. Pagination is supported, and the response is sorted with the most recent first. This endpoint can retrieve data from the last 7 days func (ok *Okx) GetBillsDetailLast7Days(ctx context.Context, arg *BillsDetailQueryParameter) ([]BillsDetailResponse, error) { - return ok.GetBillsDetail(ctx, arg, accountBillsDetail) + return ok.GetBillsDetail(ctx, arg, "account/bills", getBillsDetailsEPL) } // GetBillsDetail3Months retrieves the account’s bills. // The bill refers to all transaction records that result in changing the balance of an account. // Pagination is supported, and the response is sorted with most recent first. -// This endpoint can retrieve data from the last 3 months. +// This endpoint can retrieve data from the last 3 months func (ok *Okx) GetBillsDetail3Months(ctx context.Context, arg *BillsDetailQueryParameter) ([]BillsDetailResponse, error) { - return ok.GetBillsDetail(ctx, arg, accountBillsDetailArchive) + return ok.GetBillsDetail(ctx, arg, "account/bills-archive", getBillsDetailArchiveEPL) } -// GetBillsDetail retrieves the bills of the account. -func (ok *Okx) GetBillsDetail(ctx context.Context, arg *BillsDetailQueryParameter, route string) ([]BillsDetailResponse, error) { - if arg == nil { - return nil, errNilArgument +// ApplyBillDetails apply for bill data since 1 February, 2021 except for the current quarter. +// Quarter, valid value is Q1, Q2, Q3, Q4 +func (ok *Okx) ApplyBillDetails(ctx context.Context, year, quarter string) ([]BillsDetailResp, error) { + if year == "" { + return nil, errYearRequired + } + if quarter == "" { + return nil, errQuarterValueRequired + } + var resp []BillsDetailResp + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, billHistoryArchiveEPL, http.MethodPost, "account/bills-history-archive", map[string]string{"year": year, "quarter": quarter}, &resp, request.AuthenticatedRequest, true) +} + +// GetBillsHistoryArchive retrieves bill data archive +func (ok *Okx) GetBillsHistoryArchive(ctx context.Context, year, quarter string) ([]BillsArchiveInfo, error) { + if year == "" { + return nil, errYearRequired + } + if quarter == "" { + return nil, errQuarterValueRequired + } + params := url.Values{} + params.Set("year", year) + params.Set("quarter", quarter) + var resp []BillsArchiveInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBillHistoryArchiveEPL, http.MethodGet, common.EncodeURLValues("account/bills-history-archive", params), nil, &resp, request.AuthenticatedRequest, true) +} + +// GetBillsDetail retrieves the bills of the account +func (ok *Okx) GetBillsDetail(ctx context.Context, arg *BillsDetailQueryParameter, route string, epl request.EndpointLimit) ([]BillsDetailResponse, error) { + if *arg == (BillsDetailQueryParameter{}) { + return nil, common.ErrEmptyParams } params := url.Values{} if arg.InstrumentType != "" { params.Set("instType", strings.ToUpper(arg.InstrumentType)) } - if arg.Currency != "" { - params.Set("ccy", strings.ToUpper(arg.Currency)) + if arg.InstrumentID != "" { + params.Set("instId", arg.InstrumentID) + } + if !arg.Currency.IsEmpty() { + params.Set("ccy", arg.Currency.Upper().String()) } if arg.MarginMode != "" { params.Set("mgnMode", arg.MarginMode) @@ -1972,7 +1784,7 @@ func (ok *Okx) GetBillsDetail(ctx context.Context, arg *BillsDetailQueryParamete params.Set("type", strconv.Itoa(int(arg.BillType))) } if arg.BillSubType != 0 { - params.Set("subType", strconv.Itoa(arg.BillSubType)) + params.Set("subType", strconv.FormatInt(arg.BillSubType, 10)) } if arg.After != "" { params.Set("after", arg.After) @@ -1990,64 +1802,61 @@ func (ok *Okx) GetBillsDetail(ctx context.Context, arg *BillsDetailQueryParamete params.Set("limit", strconv.FormatInt(arg.Limit, 10)) } var resp []BillsDetailResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBillsDetailsEPL, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest) } -// GetAccountConfiguration retrieves current account configuration. +// GetAccountConfiguration retrieves current account configuration func (ok *Okx) GetAccountConfiguration(ctx context.Context) ([]AccountConfigurationResponse, error) { var resp []AccountConfigurationResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountConfigurationEPL, http.MethodGet, accountConfiguration, nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountConfigurationEPL, http.MethodGet, "account/config", nil, &resp, request.AuthenticatedRequest, true) } // SetPositionMode FUTURES and SWAP support both long/short mode and net mode. In net mode, users can only have positions in one direction; In long/short mode, users can hold positions in long and short directions. -func (ok *Okx) SetPositionMode(ctx context.Context, positionMode string) (string, error) { +// Position mode 'long_short_mode': long/short, only applicable to FUTURES/SWAP'net_mode': net +func (ok *Okx) SetPositionMode(ctx context.Context, positionMode string) (*PositionMode, error) { if positionMode != "long_short_mode" && positionMode != "net_mode" { - return "", errors.New("invalid position mode") + return nil, errInvalidPositionMode } - input := &PositionMode{ + var resp *PositionMode + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setPositionModeEPL, http.MethodPost, "account/set-position-mode", &PositionMode{ PositionMode: positionMode, - } - var resp []PositionMode - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, setPositionModeEPL, http.MethodPost, accountSetPositionMode, input, &resp, true) - if err != nil { - return "", err - } - if len(resp) == 1 { - return resp[0].PositionMode, nil - } - return "", errNoValidResponseFromServer + }, &resp, request.AuthenticatedRequest) } -// SetLeverageRate sets a leverage setting for instrument id. -func (ok *Okx) SetLeverageRate(ctx context.Context, arg SetLeverageInput) (*SetLeverageResponse, error) { - if arg.InstrumentID == "" && arg.Currency == "" { - return nil, errors.New("either instrument id or currency is required") +// SetLeverageRate sets a leverage setting for instrument id +func (ok *Okx) SetLeverageRate(ctx context.Context, arg *SetLeverageInput) (*SetLeverageResponse, error) { + if *arg == (SetLeverageInput{}) { + return nil, common.ErrEmptyParams } - arg.PositionSide = strings.ToLower(arg.PositionSide) - var resp []SetLeverageResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, setLeverageEPL, http.MethodPost, accountSetLeverage, &arg, &resp, true) - if err != nil { - return nil, err + if arg.InstrumentID == "" && arg.Currency.IsEmpty() { + return nil, errEitherInstIDOrCcyIsRequired } - if len(resp) == 1 { - return &resp[0], nil + switch arg.AssetType { + case asset.Futures, asset.PerpetualSwap: + if arg.PositionSide == "" && arg.MarginMode == "isolated" { + return nil, fmt.Errorf("%w: `%s`", order.ErrSideIsInvalid, arg.PositionSide) + } } - return nil, errNoValidResponseFromServer + arg.PositionSide = strings.ToLower(arg.PositionSide) + var resp *SetLeverageResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setLeverageEPL, http.MethodPost, "account/set-leverage", &arg, &resp, request.AuthenticatedRequest) } -// GetMaximumBuySellAmountOROpenAmount retrieves the maximum buy or sell amount for a specific instrument id -func (ok *Okx) GetMaximumBuySellAmountOROpenAmount(ctx context.Context, instrumentID, tradeMode, currency, leverage string, price float64) ([]MaximumBuyAndSell, error) { - params := url.Values{} +// GetMaximumBuySellAmountOROpenAmount retrieves the maximum buy or sell amount for a sell id +func (ok *Okx) GetMaximumBuySellAmountOROpenAmount(ctx context.Context, ccy currency.Code, instrumentID, tradeMode, leverage string, price float64, unSpotOffset bool) ([]MaximumBuyAndSell, error) { if instrumentID == "" { - return nil, errors.New("missing instrument id") + return nil, errMissingInstrumentID } - params.Set("instId", instrumentID) - if tradeMode != TradeModeCross && tradeMode != TradeModeIsolated && tradeMode != TradeModeCash { - return nil, errors.New("missing valid trade mode") + switch tradeMode { + case TradeModeCross, TradeModeIsolated, TradeModeCash: + default: + return nil, fmt.Errorf("%w, trade mode: %s", errInvalidTradeModeValue, tradeMode) } + params := url.Values{} + params.Set("instId", instrumentID) params.Set("tdMode", tradeMode) - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if price > 0 { params.Set("px", strconv.FormatFloat(price, 'f', -1, 64)) @@ -2055,96 +1864,133 @@ func (ok *Okx) GetMaximumBuySellAmountOROpenAmount(ctx context.Context, instrume if leverage != "" { params.Set("leverage", leverage) } + if unSpotOffset { + params.Set("unSpotOffset", "true") + } var resp []MaximumBuyAndSell - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumBuyOrSellAmountEPL, http.MethodGet, common.EncodeURLValues(accountMaxSize, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumBuyOrSellAmountEPL, http.MethodGet, common.EncodeURLValues("account/max-size", params), nil, &resp, request.AuthenticatedRequest) } // GetMaximumAvailableTradableAmount retrieves the maximum tradable amount for specific instrument id, and/or currency -func (ok *Okx) GetMaximumAvailableTradableAmount(ctx context.Context, instrumentID, currency, tradeMode string, reduceOnly bool, price float64) ([]MaximumTradableAmount, error) { - params := url.Values{} - if instrumentID != "" { - params.Set("instId", instrumentID) - } else { - return nil, errors.New("missing instrument id") +func (ok *Okx) GetMaximumAvailableTradableAmount(ctx context.Context, ccy currency.Code, instrumentID, tradeMode, quickMarginType string, reduceOnly, upSpotOffset bool, price float64) ([]MaximumTradableAmount, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID } - if currency != "" { - params.Set("ccy", currency) + if tradeMode == "" { + // Supported tradeMode values are 'cross', 'isolated', 'cash', and 'spot_isolated' + return nil, fmt.Errorf("%w, possible values are 'cross', 'isolated', 'cash', and 'spot_isolated'", errInvalidTradeModeValue) } - if tradeMode != TradeModeIsolated && - tradeMode != TradeModeCross && - tradeMode != TradeModeCash { - return nil, errInvalidTradeModeValue + params := url.Values{} + params.Set("instId", instrumentID) + params.Set("tdMode", tradeMode) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if reduceOnly { params.Set("reduceOnly", "true") } - params.Set("tdMode", tradeMode) - params.Set("px", strconv.FormatFloat(price, 'f', 0, 64)) + if price != 0 { + params.Set("px", strconv.FormatFloat(price, 'f', 0, 64)) + } + if quickMarginType != "" { + params.Set("quickMgnType", quickMarginType) + } + if upSpotOffset { + params.Set("upSpotOffset", "true") + } var resp []MaximumTradableAmount - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumAvailableTradableAmountEPL, http.MethodGet, common.EncodeURLValues(accountMaxAvailSize, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumAvailableTradableAmountEPL, http.MethodGet, common.EncodeURLValues("account/max-avail-size", params), nil, &resp, request.AuthenticatedRequest) } -// IncreaseDecreaseMargin Increase or decrease the margin of the isolated position. Margin reduction may result in the change of the actual leverage. +// IncreaseDecreaseMargin Increase or decrease the margin of the isolated position. Margin reduction may result in the change of the actual leverage func (ok *Okx) IncreaseDecreaseMargin(ctx context.Context, arg *IncreaseDecreaseMarginInput) (*IncreaseDecreaseMargin, error) { + if *arg == (IncreaseDecreaseMarginInput{}) { + return nil, common.ErrEmptyParams + } if arg.InstrumentID == "" { return nil, errMissingInstrumentID } - if arg.PositionSide != positionSideLong && - arg.PositionSide != positionSideShort && - arg.PositionSide != positionSideNet { - return nil, errors.New("missing valid position side") + if !slices.Contains([]string{positionSideLong, positionSideShort, positionSideNet}, arg.PositionSide) { + return nil, fmt.Errorf("%w, position side is required", order.ErrSideIsInvalid) } - if arg.Type != "add" && arg.Type != "reduce" { - return nil, errors.New("missing valid 'type', 'add': add margin 'reduce': reduce margin are allowed") + if arg.MarginBalanceType != marginBalanceAdd && arg.MarginBalanceType != marginBalanceReduce { + return nil, fmt.Errorf("%w, missing valid 'type', 'add': add margin 'reduce': reduce margin are allowed", order.ErrTypeIsInvalid) } if arg.Amount <= 0 { - return nil, errors.New("missing valid amount") - } - var resp []IncreaseDecreaseMargin - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, increaseOrDecreaseMarginEPL, http.MethodPost, accountPositionMarginBalance, &arg, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil + return nil, order.ErrAmountBelowMin } - return nil, errNoValidResponseFromServer + var resp *IncreaseDecreaseMargin + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, increaseOrDecreaseMarginEPL, http.MethodPost, "account/position/margin-balance", &arg, &resp, request.AuthenticatedRequest) } -// GetLeverageRate retrieves leverage data for different instrument id or margin mode. -func (ok *Okx) GetLeverageRate(ctx context.Context, instrumentID, marginMode string) ([]LeverageResponse, error) { - params := url.Values{} - if instrumentID != "" { - params.Set("instId", instrumentID) - } else { +// GetLeverageRate retrieves leverage data for different instrument id or margin mode +func (ok *Okx) GetLeverageRate(ctx context.Context, instrumentID, marginMode string, ccy currency.Code) ([]LeverageResponse, error) { + if instrumentID == "" { return nil, errMissingInstrumentID } - if marginMode != TradeModeCross && marginMode != TradeModeIsolated { - return nil, errors.New("missing margin mode `mgnMode`") + switch marginMode { + case TradeModeCross, TradeModeIsolated, TradeModeCash: + default: + return nil, margin.ErrMarginTypeUnsupported } + params := url.Values{} + params.Set("instId", instrumentID) params.Set("mgnMode", marginMode) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } var resp []LeverageResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeverageEPL, http.MethodGet, common.EncodeURLValues(accountLeverageInfo, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeverageEPL, http.MethodGet, common.EncodeURLValues("account/leverage-info", params), nil, &resp, request.AuthenticatedRequest) } -// GetMaximumLoanOfInstrument returns list of maximum loan of instruments. -func (ok *Okx) GetMaximumLoanOfInstrument(ctx context.Context, instrumentID, marginMode, mgnCurrency string) ([]MaximumLoanInstrument, error) { +// GetLeverageEstimatedInfo retrieves leverage estimated information. +// Instrument type: possible values are MARGIN, SWAP, FUTURES +func (ok *Okx) GetLeverageEstimatedInfo(ctx context.Context, instrumentType, marginMode, leverage, positionSide, instrumentID string, ccy currency.Code) ([]LeverageEstimatedInfo, error) { + if instrumentType == "" { + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) + } + switch marginMode { + case TradeModeCross, TradeModeIsolated: + default: + return nil, margin.ErrMarginTypeUnsupported + } + if leverage == "" { + return nil, errInvalidLeverage + } + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } params := url.Values{} + params.Set("instType", instrumentType) + params.Set("lever", leverage) + params.Set("mgnMode", marginMode) + params.Set("ccy", ccy.String()) if instrumentID != "" { params.Set("instId", instrumentID) - } else { + } + if positionSide != "" { + params.Set("posSide", positionSide) + } + var resp []LeverageEstimatedInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeverateEstimatedInfoEPL, http.MethodGet, common.EncodeURLValues("account/adjust-leverage-info", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetMaximumLoanOfInstrument returns list of maximum loan of instruments +func (ok *Okx) GetMaximumLoanOfInstrument(ctx context.Context, instrumentID, marginMode string, mgnCurrency currency.Code) ([]MaximumLoanInstrument, error) { + if instrumentID == "" { return nil, errMissingInstrumentID } - if marginMode != "" { - params.Set("mgnMode", marginMode) - } else { - return nil, errMissingMarginMode + if marginMode == "" { + return nil, margin.ErrInvalidMarginType } - if mgnCurrency != "" { - params.Set("mgnCcy", mgnCurrency) + params := url.Values{} + params.Set("instId", instrumentID) + params.Set("mgnMode", marginMode) + if !mgnCurrency.IsEmpty() { + params.Set("mgnCcy", mgnCurrency.String()) } var resp []MaximumLoanInstrument - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTheMaximumLoanOfInstrumentEPL, http.MethodGet, common.EncodeURLValues(accountMaxLoan, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTheMaximumLoanOfInstrumentEPL, http.MethodGet, common.EncodeURLValues("account/max-loan", params), nil, &resp, request.AuthenticatedRequest) } // GetFee returns Cryptocurrency trade fee, and offline trade fee @@ -2157,11 +2003,11 @@ func (ok *Okx) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (flo if err != nil { return 0, err } - responses, err := ok.GetTradeFee(ctx, okxInstTypeSpot, uly, "") + responses, err := ok.GetTradeFee(ctx, instTypeSpot, uly, "", "", "") if err != nil { return 0, err } else if len(responses) == 0 { - return 0, errors.New("no trade fee response found") + return 0, common.ErrNoResponse } if feeBuilder.IsMaker { if feeBuilder.Pair.Quote.Equal(currency.USDC) { @@ -2177,22 +2023,22 @@ func (ok *Okx) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (flo } } if fee != 0 { - fee = -fee // Negative fee rate means commission else rebate. + fee = -fee // A negative fee rate indicates a commission charge; a positive rate indicates a rebate. } return fee * feeBuilder.Amount * feeBuilder.PurchasePrice, nil case exchange.OfflineTradeFee: return 0.0015 * feeBuilder.PurchasePrice * feeBuilder.Amount, nil + default: + return fee, errFeeTypeUnsupported } - return fee, nil } -// GetTradeFee query trade fee rate of various instrument types and instrument ids. -func (ok *Okx) GetTradeFee(ctx context.Context, instrumentType, instrumentID, underlying string) ([]TradeFeeRate, error) { - params := url.Values{} +// GetTradeFee queries the trade fee rates for various instrument types and their respective IDs +func (ok *Okx) GetTradeFee(ctx context.Context, instrumentType, instrumentID, underlying, instrumentFamily, ruleType string) ([]TradeFeeRate, error) { if instrumentType == "" { - return nil, errInstrumentTypeRequired + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } - instrumentType = strings.ToUpper(instrumentType) + params := url.Values{} params.Set("instType", instrumentType) if instrumentID != "" { params.Set("instId", instrumentID) @@ -2200,18 +2046,24 @@ func (ok *Okx) GetTradeFee(ctx context.Context, instrumentType, instrumentID, un if underlying != "" { params.Set("uly", underlying) } + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } + if ruleType != "" { + params.Set("ruleType", ruleType) + } var resp []TradeFeeRate - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFeeRatesEPL, http.MethodGet, common.EncodeURLValues(accountTradeFee, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFeeRatesEPL, http.MethodGet, common.EncodeURLValues("account/trade-fee", params), nil, &resp, request.AuthenticatedRequest) } -// GetInterestAccruedData returns accrued interest data -func (ok *Okx) GetInterestAccruedData(ctx context.Context, loanType, limit int64, currency, instrumentID, marginMode string, after, before time.Time) ([]InterestAccruedData, error) { +// GetInterestAccruedData retrieves data on accrued interest +func (ok *Okx) GetInterestAccruedData(ctx context.Context, loanType, limit int64, ccy currency.Code, instrumentID, marginMode string, after, before time.Time) ([]InterestAccruedData, error) { params := url.Values{} if loanType == 1 || loanType == 2 { params.Set("type", strconv.FormatInt(loanType, 10)) } - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if instrumentID != "" { params.Set("instId", instrumentID) @@ -2229,17 +2081,17 @@ func (ok *Okx) GetInterestAccruedData(ctx context.Context, loanType, limit int64 params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []InterestAccruedData - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestAccruedDataEPL, http.MethodGet, common.EncodeURLValues(accountInterestAccrued, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestAccruedDataEPL, http.MethodGet, common.EncodeURLValues("account/interest-accrued", params), nil, &resp, request.AuthenticatedRequest) } // GetInterestRate get the user's current leveraged currency borrowing interest rate -func (ok *Okx) GetInterestRate(ctx context.Context, currency string) ([]InterestRateResponse, error) { +func (ok *Okx) GetInterestRate(ctx context.Context, ccy currency.Code) ([]InterestRateResponse, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } var resp []InterestRateResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateEPL, http.MethodGet, common.EncodeURLValues(accountInterestRate, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateEPL, http.MethodGet, common.EncodeURLValues("account/interest-rate", params), nil, &resp, request.AuthenticatedRequest) } // SetGreeks set the display type of Greeks. PA: Greeks in coins BS: Black-Scholes Greeks in dollars @@ -2251,84 +2103,120 @@ func (ok *Okx) SetGreeks(ctx context.Context, greeksType string) (*GreeksType, e input := &GreeksType{ GreeksType: greeksType, } - var resp []GreeksType - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, setGreeksEPL, http.MethodPost, accountSetGreeks, input, &resp, true) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *GreeksType + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setGreeksEPL, http.MethodPost, "account/set-greeks", input, &resp, request.AuthenticatedRequest) } -// IsolatedMarginTradingSettings to set the currency margin and futures/perpetual Isolated margin trading mode. -func (ok *Okx) IsolatedMarginTradingSettings(ctx context.Context, arg IsolatedMode) (*IsolatedMode, error) { +// IsolatedMarginTradingSettings configures the currency margin and sets the isolated margin trading mode for futures or perpetual contracts +func (ok *Okx) IsolatedMarginTradingSettings(ctx context.Context, arg *IsolatedMode) (*IsolatedMode, error) { + if *arg == (IsolatedMode{}) { + return nil, common.ErrEmptyParams + } arg.IsoMode = strings.ToLower(arg.IsoMode) if arg.IsoMode != "automatic" && arg.IsoMode != "autonomy" { return nil, errMissingIsolatedMarginTradingSetting } - arg.InstrumentType = strings.ToUpper(arg.InstrumentType) - if arg.InstrumentType != okxInstTypeMargin && - arg.InstrumentType != okxInstTypeContract { + if arg.InstrumentType != instTypeMargin && + arg.InstrumentType != instTypeContract { return nil, fmt.Errorf("%w, received '%v' only margin and contract instrument types are allowed", errInvalidInstrumentType, arg.InstrumentType) } - var resp []IsolatedMode - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, isolatedMarginTradingSettingsEPL, http.MethodPost, accountSetIsolatedMode, &arg, &resp, true) - if err != nil { - return nil, err + var resp *IsolatedMode + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, isolatedMarginTradingSettingsEPL, http.MethodPost, "account/set-isolated-mode", &arg, &resp, request.AuthenticatedRequest) +} + +// ManualBorrowAndRepayInQuickMarginMode initiates a new manual borrow and repayment process in Quick Margin mode +func (ok *Okx) ManualBorrowAndRepayInQuickMarginMode(ctx context.Context, arg *BorrowAndRepay) (*BorrowAndRepay, error) { + if *arg == (BorrowAndRepay{}) { + return nil, common.ErrEmptyParams + } + if arg.Amount <= 0 { + return nil, order.ErrAmountBelowMin + } + if arg.LoanCcy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.Side == "" { + return nil, fmt.Errorf("%w, possible values are 'borrow' and 'repay'", order.ErrSideIsInvalid) + } + if arg.InstrumentID == "" { + return nil, errMissingInstrumentID + } + var resp *BorrowAndRepay + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, manualBorrowAndRepayEPL, http.MethodPost, "account/quick-margin-borrow-repay", arg, &resp, request.AuthenticatedRequest) +} + +// GetBorrowAndRepayHistoryInQuickMarginMode retrieves borrow and repay history in quick margin mode +func (ok *Okx) GetBorrowAndRepayHistoryInQuickMarginMode(ctx context.Context, instrumentID currency.Pair, ccy currency.Code, side, afterPaginationID, beforePaginationID string, beginTime, endTime time.Time, limit int64) ([]BorrowRepayHistoryItem, error) { + params := url.Values{} + if !instrumentID.IsEmpty() { + params.Set("instId", instrumentID.String()) + } + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if side != "" { + params.Set("side", side) + } + if afterPaginationID != "" { + params.Set("after", afterPaginationID) + } + if beforePaginationID != "" { + params.Set("before", beforePaginationID) + } + if !beginTime.IsZero() { + params.Set("begin", strconv.FormatInt(beginTime.UnixMilli(), 10)) + } + if !endTime.IsZero() { + params.Set("end", strconv.FormatInt(endTime.UnixMilli(), 10)) } - if len(resp) == 1 { - return &resp[0], nil + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return nil, errNoValidResponseFromServer + var resp []BorrowRepayHistoryItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowAndRepayHistoryEPL, http.MethodGet, common.EncodeURLValues("account/quick-margin-borrow-repay-history", params), nil, &resp, request.AuthenticatedRequest) } -// GetMaximumWithdrawals retrieves the maximum transferable amount from trading account to funding account. -func (ok *Okx) GetMaximumWithdrawals(ctx context.Context, currency string) ([]MaximumWithdrawal, error) { +// GetMaximumWithdrawals retrieves the maximum transferable amount from a trading account to a funding account for quick margin borrowing and repayment +func (ok *Okx) GetMaximumWithdrawals(ctx context.Context, ccy currency.Code) ([]MaximumWithdrawal, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } var resp []MaximumWithdrawal - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumWithdrawalsEPL, http.MethodGet, common.EncodeURLValues(accountMaxWithdrawal, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumWithdrawalsEPL, http.MethodGet, common.EncodeURLValues("account/max-withdrawal", params), nil, &resp, request.AuthenticatedRequest) } // GetAccountRiskState gets the account risk status. // only applicable to Portfolio margin account func (ok *Okx) GetAccountRiskState(ctx context.Context) ([]AccountRiskState, error) { var resp []AccountRiskState - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountRiskStateEPL, http.MethodGet, accountRiskState, nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAccountRiskStateEPL, http.MethodGet, "account/risk-state", nil, &resp, request.AuthenticatedRequest) } -// VIPLoansBorrowAndRepay creates VIP borrow or repay for a currency. -func (ok *Okx) VIPLoansBorrowAndRepay(ctx context.Context, arg LoanBorrowAndReplayInput) (*LoanBorrowAndReplay, error) { - if arg.Currency == "" { - return nil, errInvalidCurrencyValue +// VIPLoansBorrowAndRepay creates VIP borrow or repay for a currency +func (ok *Okx) VIPLoansBorrowAndRepay(ctx context.Context, arg *LoanBorrowAndReplayInput) (*LoanBorrowAndReplay, error) { + if *arg == (LoanBorrowAndReplayInput{}) { + return nil, common.ErrEmptyParams + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } if arg.Side == "" { - return nil, errInvalidOrderSide + return nil, order.ErrSideIsInvalid } if arg.Amount <= 0 { - return nil, errors.New("amount must be greater than zero") - } - var resp []LoanBorrowAndReplay - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, vipLoansBorrowAnsRepayEPL, http.MethodPost, accountBorrowReply, &arg, &resp, true) - if err != nil { - return nil, err + return nil, order.ErrAmountBelowMin } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *LoanBorrowAndReplay + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, vipLoansBorrowAnsRepayEPL, http.MethodPost, "account/borrow-repay", &arg, &resp, request.AuthenticatedRequest) } -// GetBorrowAndRepayHistoryForVIPLoans retrieves borrow and repay history for VIP loans. -func (ok *Okx) GetBorrowAndRepayHistoryForVIPLoans(ctx context.Context, currency string, after, before time.Time, limit int64) ([]BorrowRepayHistory, error) { +// GetBorrowAndRepayHistoryForVIPLoans retrieves borrow and repay history for VIP loans +func (ok *Okx) GetBorrowAndRepayHistoryForVIPLoans(ctx context.Context, ccy currency.Code, after, before time.Time, limit int64) ([]BorrowRepayHistory, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if !after.IsZero() { params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) @@ -2340,150 +2228,597 @@ func (ok *Okx) GetBorrowAndRepayHistoryForVIPLoans(ctx context.Context, currency params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []BorrowRepayHistory - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowAnsRepayHistoryHistoryEPL, http.MethodGet, common.EncodeURLValues(accountBorrowRepayHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowAnsRepayHistoryHistoryEPL, http.MethodGet, common.EncodeURLValues("account/borrow-repay-history", params), nil, &resp, request.AuthenticatedRequest) } -// GetBorrowInterestAndLimit borrow interest and limit -func (ok *Okx) GetBorrowInterestAndLimit(ctx context.Context, loanType int64, currency string) ([]BorrowInterestAndLimitResponse, error) { +// GetVIPInterestAccruedData retrieves VIP interest accrued data +func (ok *Okx) GetVIPInterestAccruedData(ctx context.Context, ccy currency.Code, orderID string, after, before time.Time, limit int64) ([]VIPInterestData, error) { params := url.Values{} - if loanType == 1 || loanType == 2 { - params.Set("type", strconv.FormatInt(loanType, 10)) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - if currency != "" { - params.Set("ccy", currency) + if orderID != "" { + params.Set("ordId", orderID) } - var resp []BorrowInterestAndLimitResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowInterestAndLimitEPL, http.MethodGet, common.EncodeURLValues(accountInterestLimits, params), nil, &resp, true) -} - -// PositionBuilder calculates portfolio margin information for simulated position or current position of the user. You can add up to 200 simulated positions in one request. -// Instrument type SWAP FUTURES, and OPTION are supported -func (ok *Okx) PositionBuilder(ctx context.Context, arg PositionBuilderInput) ([]PositionBuilderResponse, error) { - arg.InstrumentType = strings.ToUpper(arg.InstrumentType) - var resp []PositionBuilderResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, positionBuilderEPL, http.MethodPost, accountSimulatedMargin, &arg, &resp, true) -} - -// GetGreeks retrieves a greeks list of all assets in the account. -func (ok *Okx) GetGreeks(ctx context.Context, currency string) ([]GreeksItem, error) { - params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) } - var resp []GreeksItem - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGreeksEPL, http.MethodGet, common.EncodeURLValues(accountGreeks, params), nil, &resp, true) -} - -// GetPMLimitation retrieve cross position limitation of SWAP/FUTURES/OPTION under Portfolio margin mode. -func (ok *Okx) GetPMLimitation(ctx context.Context, instrumentType, underlying string) ([]PMLimitationResponse, error) { - params := url.Values{} - if instrumentType == "" { - return nil, errInstrumentTypeRequired + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) } - if underlying == "" { - return nil, errInvalidUnderlying + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - params.Set("instType", strings.ToUpper(instrumentType)) - params.Set("uly", underlying) - var resp []PMLimitationResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPMLimitationEPL, http.MethodGet, common.EncodeURLValues(accountPortfolioMarginLimitation, params), nil, &resp, true) + var resp []VIPInterestData + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getVIPInterestAccruedDataEPL, http.MethodGet, common.EncodeURLValues("account/vip-interest-accrued", params), nil, &resp, request.AuthenticatedRequest) } -/********************************** Subaccount Endpoints ***************************************************/ - -// ViewSubAccountList applies to master accounts only -func (ok *Okx) ViewSubAccountList(ctx context.Context, enable bool, subaccountName string, after, before time.Time, limit int64) ([]SubaccountInfo, error) { +// GetVIPInterestDeductedData retrieves a VIP interest deducted data +func (ok *Okx) GetVIPInterestDeductedData(ctx context.Context, ccy currency.Code, orderID string, after, before time.Time, limit int64) ([]VIPInterestData, error) { params := url.Values{} - params.Set("enable", strconv.FormatBool(enable)) - if subaccountName != "" { - params.Set("subAcct", subaccountName) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if orderID != "" { + params.Set("ordId", orderID) } if !after.IsZero() { params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) } if !before.IsZero() { - params.Set("before", strconv.FormatInt(after.UnixMilli(), 10)) + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - var resp []SubaccountInfo - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, viewSubaccountListEPL, http.MethodGet, common.EncodeURLValues(usersSubaccountList, params), nil, &resp, true) + var resp []VIPInterestData + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getVIPInterestDeductedDataEPL, http.MethodGet, common.EncodeURLValues("account/vip-interest-deducted", params), nil, &resp, request.AuthenticatedRequest) } -// ResetSubAccountAPIKey applies to master accounts only and master accounts APIKey must be linked to IP addresses. -func (ok *Okx) ResetSubAccountAPIKey(ctx context.Context, arg *SubAccountAPIKeyParam) (*SubAccountAPIKeyResponse, error) { +// GetVIPLoanOrderList retrieves VIP loan order list +// state: possible values are 1:Borrowing 2:Borrowed 3:Repaying 4:Repaid 5:Borrow failed +func (ok *Okx) GetVIPLoanOrderList(ctx context.Context, orderID, state string, ccy currency.Code, after, before time.Time, limit int64) ([]VIPLoanOrder, error) { params := url.Values{} - if arg == nil { - return nil, errNilArgument - } - if arg.SubAccountName == "" { - return nil, errInvalidSubAccountName + if orderID != "" { + params.Set("ordId", orderID) } - params.Set("subAcct", arg.SubAccountName) - if arg.APIKey == "" { - return nil, errInvalidAPIKey + if state != "" { + params.Set("state", state) } - var resp []SubAccountAPIKeyResponse - if arg.IP != "" && net.ParseIP(arg.IP).To4() == nil { - return nil, errInvalidIPAddress + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - if arg.APIKeyPermission == "" && len(arg.Permissions) != 0 { - for x := range arg.Permissions { - if arg.Permissions[x] != "read" && - arg.Permissions[x] != "withdraw" && - arg.Permissions[x] != "trade" && - arg.Permissions[x] != "read_only" { - return nil, errInvalidAPIKeyPermission - } - if x != 0 { - arg.APIKeyPermission += "," - } - arg.APIKeyPermission += arg.Permissions[x] - } - } else if arg.APIKeyPermission != "read" && arg.APIKeyPermission != "withdraw" && arg.APIKeyPermission != "trade" && arg.APIKeyPermission != "read_only" { - return nil, errInvalidAPIKeyPermission + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) } - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, resetSubAccountAPIKeyEPL, http.MethodPost, subAccountModifyAPIKey, &arg, &resp, true); err != nil { - return nil, err + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) } - if len(resp) == 1 { - return &resp[0], nil + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return nil, errNoValidResponseFromServer + var resp []VIPLoanOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getVIPLoanOrderListEPL, http.MethodGet, common.EncodeURLValues("account/vip-loan-order-list", params), nil, &resp, request.AuthenticatedRequest) } -// GetSubaccountTradingBalance query detailed balance info of Trading Account of a sub-account via the master account (applies to master accounts only) -func (ok *Okx) GetSubaccountTradingBalance(ctx context.Context, subaccountName string) ([]SubaccountBalanceResponse, error) { +// GetVIPLoanOrderDetail retrieves list of loan order details +func (ok *Okx) GetVIPLoanOrderDetail(ctx context.Context, orderID string, ccy currency.Code, after, before time.Time, limit int64) (*VIPLoanOrderDetail, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } params := url.Values{} - if subaccountName == "" { - return nil, errMissingRequiredParameterSubaccountName + params.Set("ordId", orderID) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp *VIPLoanOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getVIPLoanOrderDetailEPL, http.MethodGet, common.EncodeURLValues("account/vip-loan-order-detail", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetBorrowInterestAndLimit borrow interest and limit +func (ok *Okx) GetBorrowInterestAndLimit(ctx context.Context, loanType int64, ccy currency.Code) ([]BorrowInterestAndLimitResponse, error) { + params := url.Values{} + if loanType == 1 || loanType == 2 { + params.Set("type", strconv.FormatInt(loanType, 10)) + } + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + var resp []BorrowInterestAndLimitResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowInterestAndLimitEPL, http.MethodGet, common.EncodeURLValues("account/interest-limits", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetFixedLoanBorrowLimit retrieves a fixed loadn borrow limit information +func (ok *Okx) GetFixedLoanBorrowLimit(ctx context.Context) (*FixedLoanBorrowLimitInformation, error) { + var resp *FixedLoanBorrowLimitInformation + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFixedLoanBorrowLimitEPL, http.MethodGet, "account/fixed-loan/borrowing-limit", nil, &resp, request.AuthenticatedRequest) +} + +// GetFixedLoanBorrowQuote retrieves a fixed loan borrow quote information +func (ok *Okx) GetFixedLoanBorrowQuote(ctx context.Context, borrowingCurrency currency.Code, borrowType, term, orderID string, amount, maxRate float64) (*FixedLoanBorrowQuote, error) { + if borrowType == "" { + return nil, errBorrowTypeRequired + } + if borrowType == "normal" { + if borrowingCurrency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if amount <= 0 { + return nil, order.ErrAmountBelowMin + } + if maxRate <= 0 { + return nil, errMaxRateRequired + } + if term == "" { + return nil, errLendingTermIsRequired + } + } else if borrowType == "reborrow" { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + } + params := url.Values{} + params.Set("type", borrowType) + if !borrowingCurrency.IsEmpty() { + params.Set("ccy", borrowingCurrency.String()) + } + if amount > 0 { + params.Set("amt", strconv.FormatFloat(amount, 'f', -1, 64)) + } + if maxRate > 0 { + params.Set("maxRate", strconv.FormatFloat(maxRate, 'f', -1, 64)) + } + if term != "" { + params.Set("term", term) + } + if orderID != "" { + params.Set("ordId", orderID) + } + var resp *FixedLoanBorrowQuote + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFixedLoanBorrowQuoteEPL, http.MethodGet, common.EncodeURLValues("account/fixed-loan/borrowing-quote", params), nil, &resp, request.AuthenticatedRequest) +} + +// PlaceFixedLoanBorrowingOrder for new borrowing orders, they belong to the IOC (immediately close and cancel the remaining) type. For renewal orders, they belong to the FOK (Fill-or-kill) type +func (ok *Okx) PlaceFixedLoanBorrowingOrder(ctx context.Context, ccy currency.Code, amount, maxRate, reborrowRate float64, term string, reborrow bool) (*OrderIDResponse, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if amount <= 0 { + return nil, order.ErrAmountBelowMin + } + if maxRate <= 0 { + return nil, errMaxRateRequired + } + if term == "" { + return nil, errLendingTermIsRequired + } + arg := &struct { + Currency string `json:"ccy"` + Amount float64 `json:"amt,string"` + MaxRate float64 `jsons:"maxRate,string"` + Term string `json:"term"` + Reborrow bool `json:"reborrow,omitempty"` + ReborrowRate float64 `json:"reborrowRate,string,omitempty"` + }{ + Currency: ccy.String(), + Amount: amount, + MaxRate: maxRate, + Term: term, + Reborrow: reborrow, + ReborrowRate: reborrowRate, + } + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeFixedLoanBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/borrowing-order", arg, &resp, request.AuthenticatedRequest) +} + +// AmendFixedLoanBorrowingOrder amends a fixed loan borrowing order +func (ok *Okx) AmendFixedLoanBorrowingOrder(ctx context.Context, orderID string, reborrow bool, renewMaxRate float64) (*OrderIDResponse, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + arg := &struct { + OrderID string `json:"ordId"` + Reborrow bool `json:"reborrow,omitempty"` + RenewMaxRate float64 `json:"renewMaxRate,omitempty,string"` + }{ + OrderID: orderID, + Reborrow: reborrow, + RenewMaxRate: renewMaxRate, + } + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendFixedLaonBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/amend-borrowing-order", arg, &resp, request.AuthenticatedRequest) +} + +// ManualRenewFixedLoanBorrowingOrder manual renew fixed loan borrowing order +func (ok *Okx) ManualRenewFixedLoanBorrowingOrder(ctx context.Context, orderID string, maxRate float64) (*OrderIDResponse, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + if maxRate <= 0 { + return nil, errMaxRateRequired + } + arg := &struct { + OrderID string `json:"ordId"` + MaxRate float64 `json:"maxRate,string"` + }{ + OrderID: orderID, + MaxRate: maxRate, + } + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, manualRenewFixedLoanBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/manual-reborrow", arg, &resp, request.AuthenticatedRequest) +} + +// RepayFixedLoanBorrowingOrder repays fixed loan borrowing order +func (ok *Okx) RepayFixedLoanBorrowingOrder(ctx context.Context, orderID string) (*OrderIDResponse, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, repayFixedLoanBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/repay-borrowing-order", map[string]string{"ordId": orderID}, &resp, request.AuthenticatedRequest) +} + +// ConvertFixedLoanToMarketLoan converts fixed loan to market loan +func (ok *Okx) ConvertFixedLoanToMarketLoan(ctx context.Context, orderID string) (*OrderIDResponse, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, convertFixedLoanToMarketLoanEPL, http.MethodPost, "account/fixed-loan/convert-to-market-loan", nil, &resp, request.AuthenticatedRequest) +} + +// ReduceLiabilitiesForFixedLoan provide the function of "setting pending repay state / canceling pending repay state" for fixed loan order +func (ok *Okx) ReduceLiabilitiesForFixedLoan(ctx context.Context, orderID string, pendingRepay bool) (*ReduceLiabilities, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + arg := &struct { + OrderID string `json:"ordId"` + PendingRepayment bool `json:"pendingRepay,string"` + }{ + OrderID: orderID, + PendingRepayment: pendingRepay, + } + var resp *ReduceLiabilities + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, reduceLiabilitiesForFixedLoanEPL, http.MethodPost, "account/fixed-loan/reduce-liabilities", arg, &resp, request.AuthenticatedRequest) +} + +// GetFixedLoanBorrowOrderList retrieves fixed loan borrow order list +// State '1': Borrowing '2': Borrowed '3': Settled (Repaid) '4': Borrow failed '5': Overdue '6': Settling '7': Reborrowing '8': Pending repay +func (ok *Okx) GetFixedLoanBorrowOrderList(ctx context.Context, ccy currency.Code, orderID, state, term string, after, before time.Time, limit int64) ([]FixedLoanBorrowOrderDetail, error) { + params := url.Values{} + if orderID != "" { + params.Set("ordId", orderID) + } + if ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if state != "" { + params.Set("state", state) + } + if term != "" { + params.Set("term", term) + } + if !after.IsZero() && !before.IsZero() { + err := common.StartEndTimeCheck(after, before) + if err != nil { + return nil, err + } + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []FixedLoanBorrowOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFixedLoanBorrowOrderListEPL, http.MethodGet, common.EncodeURLValues("account/fixed-loan/borrowing-orders-list", params), nil, &resp, request.AuthenticatedRequest) +} + +// ManualBorrowOrRepay borrow or repay assets. only applicable to Spot mode (enabled borrowing) +func (ok *Okx) ManualBorrowOrRepay(ctx context.Context, ccy currency.Code, side string, amount float64) (*BorrowOrRepay, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if side == "" { + return nil, errLendingSideRequired + } + if amount <= 0 { + return nil, order.ErrAmountBelowMin + } + arg := &struct { + Currency string `json:"ccy"` + Side string `json:"side"` + Amount float64 `json:"amt"` + }{ + Currency: ccy.String(), + Side: side, + Amount: amount, + } + var resp *BorrowOrRepay + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, manualBorrowOrRepayEPL, http.MethodPost, "account/spot-manual-borrow-repay", arg, &resp, request.AuthenticatedRequest) +} + +// SetAutoRepay represents an auto-repay. Only applicable to Spot mode (enabled borrowing) +func (ok *Okx) SetAutoRepay(ctx context.Context, autoRepay bool) (*AutoRepay, error) { + arg := AutoRepay{ + AutoRepay: autoRepay, + } + var resp *AutoRepay + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setAutoRepayEPL, http.MethodPost, "account/set-auto-repay", arg, &resp, request.AuthenticatedRequest) +} + +// GetBorrowRepayHistory retrieve the borrow/repay history under Spot mode +func (ok *Okx) GetBorrowRepayHistory(ctx context.Context, ccy currency.Code, eventType string, after, before time.Time, limit int64) ([]BorrowRepayItem, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if eventType != "" { + params.Set("type", eventType) + } + if !after.IsZero() && !before.IsZero() { + err := common.StartEndTimeCheck(after, before) + if err != nil { + return nil, err + } + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []BorrowRepayItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowRepayHistoryEPL, http.MethodGet, common.EncodeURLValues("account/spot-borrow-repay-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// NewPositionBuilder calculates portfolio margin information for virtual position/assets or current position of the user. +// You can add up to 200 virtual positions and 200 virtual assets in one request +func (ok *Okx) NewPositionBuilder(ctx context.Context, arg *PositionBuilderParam) (*PositionBuilderDetail, error) { + if arg == nil { + return nil, common.ErrNilPointer + } + var resp *PositionBuilderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, newPositionBuilderEPL, http.MethodPost, "account/position-builder", arg, &resp, request.AuthenticatedRequest) +} + +// SetRiskOffsetAmount set risk offset amount. This does not represent the actual spot risk offset amount. Only applicable to Portfolio Margin Mode +func (ok *Okx) SetRiskOffsetAmount(ctx context.Context, ccy currency.Code, clientSpotInUseAmount float64) (*RiskOffsetAmount, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if clientSpotInUseAmount <= 0 { + return nil, order.ErrAmountBelowMin + } + arg := &struct { + Currency string `json:"ccy"` + ClientSpotInUseAmount float64 `json:"clSpotInUseAmt,string"` + }{ + Currency: ccy.String(), + ClientSpotInUseAmount: clientSpotInUseAmount, + } + var resp *RiskOffsetAmount + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setRiskOffsetAmountEPL, http.MethodPost, "account/set-riskOffset-amt", arg, &resp, request.AuthenticatedRequest) +} + +// GetGreeks retrieves a greeks list of all assets in the account +func (ok *Okx) GetGreeks(ctx context.Context, ccy currency.Code) ([]GreeksItem, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + var resp []GreeksItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGreeksEPL, http.MethodGet, common.EncodeURLValues("account/greeks", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetPMPositionLimitation retrieve cross position limitation of SWAP/FUTURES/OPTION under Portfolio margin mode +func (ok *Okx) GetPMPositionLimitation(ctx context.Context, instrumentType, underlying, instrumentFamily string) ([]PMLimitationResponse, error) { + if instrumentType == "" { + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) + } + if underlying == "" && instrumentFamily == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired + } + params := url.Values{} + params.Set("instType", strings.ToUpper(instrumentType)) + if underlying != "" { + params.Set("uly", underlying) + } + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } + var resp []PMLimitationResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPMLimitationEPL, http.MethodGet, common.EncodeURLValues("account/position-tiers", params), nil, &resp, request.AuthenticatedRequest) +} + +// SetRiskOffsetType configure the risk offset type in portfolio margin mode. +// riskOffsetType possible values are: +// 1: Spot-derivatives (USDT) risk offset +// 2: Spot-derivatives (Crypto) risk offset +// 3:Derivatives only mode +func (ok *Okx) SetRiskOffsetType(ctx context.Context, riskOffsetType string) (*RiskOffsetType, error) { + if riskOffsetType == "" { + return nil, errors.New("missing risk offset type") + } + var resp *RiskOffsetType + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setRiskOffsetLimiterEPL, http.MethodPost, "account/set-riskOffset-type", &map[string]string{"type": riskOffsetType}, &resp, request.AuthenticatedRequest) +} + +// ActivateOption activates option +func (ok *Okx) ActivateOption(ctx context.Context) (types.Time, error) { + resp := &struct { + Timestamp types.Time `json:"ts"` + }{} + return resp.Timestamp, ok.SendHTTPRequest(ctx, exchange.RestSpot, activateOptionEPL, http.MethodPost, "account/activate-option", nil, &resp, request.AuthenticatedRequest) +} + +// SetAutoLoan only applicable to Multi-currency margin and Portfolio margin +func (ok *Okx) SetAutoLoan(ctx context.Context, autoLoan bool) (*AutoLoan, error) { + var resp *AutoLoan + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setAutoLoanEPL, http.MethodPost, "account/set-auto-loan", &AutoLoan{AutoLoan: autoLoan}, &resp, request.AuthenticatedRequest) +} + +// SetAccountMode to set on the Web/App for the first set of every account mode. +// Account mode 1: Simple mode 2: Single-currency margin mode 3: Multi-currency margin code 4: Portfolio margin mode +func (ok *Okx) SetAccountMode(ctx context.Context, accountLevel string) (*AccountMode, error) { + var resp *AccountMode + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setAccountLevelEPL, http.MethodPost, "account/set-account-level", &map[string]string{"acctLv": accountLevel}, &resp, request.AuthenticatedRequest) +} + +// ResetMMPStatus reset the MMP status to be inactive. +// you can unfreeze by this endpoint once MMP is triggered. +// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required +func (ok *Okx) ResetMMPStatus(ctx context.Context, instrumentType, instrumentFamily string) (*MMPStatusResponse, error) { + if instrumentFamily == "" { + return nil, errInstrumentFamilyRequired + } + arg := &struct { + InstrumentType string `json:"instType,omitempty"` + InstrumentFamily string `json:"instFamily"` + }{ + InstrumentType: instrumentType, + InstrumentFamily: instrumentFamily, + } + var resp *MMPStatusResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, resetMMPStatusEPL, http.MethodPost, "account/mmp-reset", arg, &resp, request.AuthenticatedRequest) +} + +// SetMMP set MMP configure +// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required +func (ok *Okx) SetMMP(ctx context.Context, arg *MMPConfig) (*MMPConfig, error) { + if *arg == (MMPConfig{}) { + return nil, common.ErrEmptyParams + } + if arg.InstrumentFamily == "" { + return nil, errInstrumentFamilyRequired + } + if arg.QuantityLimit <= 0 { + return nil, errInvalidQuantityLimit + } + var resp *MMPConfig + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setMMPEPL, http.MethodPost, "account/mmp-config", arg, &resp, request.AuthenticatedRequest) +} + +// GetMMPConfig retrieves MMP configure information +// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required +func (ok *Okx) GetMMPConfig(ctx context.Context, instrumentFamily string) ([]MMPConfigDetail, error) { + params := url.Values{} + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } + var resp []MMPConfigDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMMPConfigEPL, http.MethodGet, common.EncodeURLValues("account/mmp-config", params), nil, &resp, request.AuthenticatedRequest) +} + +/********************************** Subaccount Endpoints ***************************************************/ + +// ViewSubAccountList applies to master accounts only +func (ok *Okx) ViewSubAccountList(ctx context.Context, enable bool, subaccountName string, after, before time.Time, limit int64) ([]SubaccountInfo, error) { + params := url.Values{} + if enable { + params.Set("enable", "true") + } + if subaccountName != "" { + params.Set("subAcct", subaccountName) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(after.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []SubaccountInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, viewSubaccountListEPL, http.MethodGet, common.EncodeURLValues("users/subaccount/list", params), nil, &resp, request.AuthenticatedRequest) +} + +// ResetSubAccountAPIKey applies to master accounts only and master accounts APIKey must be linked to IP addresses +func (ok *Okx) ResetSubAccountAPIKey(ctx context.Context, arg *SubAccountAPIKeyParam) (*SubAccountAPIKeyResponse, error) { + if arg == nil { + return nil, common.ErrNilPointer + } + if arg.SubAccountName == "" { + return nil, errInvalidSubAccountName + } + if arg.APIKey == "" { + return nil, errInvalidAPIKey + } + if arg.IP != "" && net.ParseIP(arg.IP).To4() == nil { + return nil, errInvalidIPAddress + } + if arg.APIKeyPermission == "" && len(arg.Permissions) != 0 { + for x := range arg.Permissions { + if !slices.Contains([]string{"read", "withdraw", "trade", "read_only"}, arg.Permissions[x]) { + return nil, errInvalidAPIKeyPermission + } + if x != 0 { + arg.APIKeyPermission += "," + } + arg.APIKeyPermission += arg.Permissions[x] + } + } else if !slices.Contains([]string{"read", "withdraw", "trade", "read_only"}, arg.APIKeyPermission) { + return nil, errInvalidAPIKeyPermission + } + var resp *SubAccountAPIKeyResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, resetSubAccountAPIKeyEPL, http.MethodPost, "users/subaccount/modify-apikey", &arg, &resp, request.AuthenticatedRequest) +} + +// GetSubaccountTradingBalance query detailed balance info of Trading Account of a sub-account via the master account (applies to master accounts only) +func (ok *Okx) GetSubaccountTradingBalance(ctx context.Context, subaccountName string) ([]SubaccountBalanceResponse, error) { + if subaccountName == "" { + return nil, errInvalidSubAccountName + } + params := url.Values{} params.Set("subAcct", subaccountName) var resp []SubaccountBalanceResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSubaccountTradingBalanceEPL, http.MethodGet, common.EncodeURLValues(accountSubaccountBalances, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSubaccountTradingBalanceEPL, http.MethodGet, common.EncodeURLValues("account/subaccount/balances", params), nil, &resp, request.AuthenticatedRequest) } // GetSubaccountFundingBalance query detailed balance info of Funding Account of a sub-account via the master account (applies to master accounts only) -func (ok *Okx) GetSubaccountFundingBalance(ctx context.Context, subaccountName, currency string) ([]FundingBalance, error) { - params := url.Values{} +func (ok *Okx) GetSubaccountFundingBalance(ctx context.Context, subaccountName string, ccy currency.Code) ([]FundingBalance, error) { if subaccountName == "" { - return nil, errMissingRequiredParameterSubaccountName + return nil, errInvalidSubAccountName } + params := url.Values{} params.Set("subAcct", subaccountName) - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } var resp []FundingBalance - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSubaccountFundingBalanceEPL, http.MethodGet, common.EncodeURLValues(assetSubaccountBalances, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSubaccountFundingBalanceEPL, http.MethodGet, + common.EncodeURLValues("asset/subaccount/balances", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetSubAccountMaximumWithdrawal retrieve the maximum withdrawal information of a sub-account via the master account (applies to master accounts only). If no currency is specified, the transferable amount of all owned currencies will be returned +func (ok *Okx) GetSubAccountMaximumWithdrawal(ctx context.Context, subAccountName string, ccy currency.Code) ([]SubAccountMaximumWithdrawal, error) { + if subAccountName == "" { + return nil, errInvalidSubAccountName + } + params := url.Values{} + params.Set("subAcct", subAccountName) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + var resp []SubAccountMaximumWithdrawal + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSubAccountMaxWithdrawalEPL, http.MethodGet, + common.EncodeURLValues("account/subaccount/max-withdrawal", params), nil, &resp, request.AuthenticatedRequest) } // HistoryOfSubaccountTransfer retrieves subaccount transfer histories; applies to master accounts only. -// Retrieve the transfer data for the last 3 months. -func (ok *Okx) HistoryOfSubaccountTransfer(ctx context.Context, currency, subaccountType, subaccountName string, before, after time.Time, limit int64) ([]SubaccountBillItem, error) { +// retrieve the transfer data for the last 3 months +func (ok *Okx) HistoryOfSubaccountTransfer(ctx context.Context, ccy currency.Code, subaccountType, subaccountName string, before, after time.Time, limit int64) ([]SubaccountBillItem, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if subaccountType != "" { params.Set("type", subaccountType) @@ -2501,16 +2836,48 @@ func (ok *Okx) HistoryOfSubaccountTransfer(ctx context.Context, currency, subacc params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []SubaccountBillItem - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, historyOfSubaccountTransferEPL, http.MethodGet, common.EncodeURLValues(assetSubaccountBills, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, historyOfSubaccountTransferEPL, http.MethodGet, common.EncodeURLValues("asset/subaccount/bills", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetHistoryOfManagedSubAccountTransfer retrieves managed sub-account transfers. +// nly applicable to the trading team's master account to getting transfer records of managed sub accounts entrusted to oneself +func (ok *Okx) GetHistoryOfManagedSubAccountTransfer(ctx context.Context, ccy currency.Code, transferType, subAccountName, subAccountUID string, after, before time.Time, limit int64) ([]SubAccountTransfer, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if transferType != "" { + params.Set("type", transferType) + } + if subAccountName != "" { + params.Set("subAcct", subAccountName) + } + if subAccountUID != "" { + params.Set("subUid", subAccountUID) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []SubAccountTransfer + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, managedSubAccountTransferEPL, http.MethodGet, common.EncodeURLValues("asset/subaccount/managed-subaccount-bills", params), nil, &resp, request.AuthenticatedRequest) } // MasterAccountsManageTransfersBetweenSubaccounts master accounts manage the transfers between sub-accounts applies to master accounts only func (ok *Okx) MasterAccountsManageTransfersBetweenSubaccounts(ctx context.Context, arg *SubAccountAssetTransferParams) ([]TransferIDInfo, error) { - if arg.Currency == "" { - return nil, errInvalidCurrencyValue + if *arg == (SubAccountAssetTransferParams{}) { + return nil, common.ErrEmptyParams + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } if arg.Amount <= 0 { - return nil, errInvalidTransferAmount + return nil, order.ErrAmountBelowMin } if arg.From == 0 { return nil, errInvalidSubaccount @@ -2519,22 +2886,25 @@ func (ok *Okx) MasterAccountsManageTransfersBetweenSubaccounts(ctx context.Conte return nil, errInvalidSubaccount } if arg.FromSubAccount == "" { - return nil, errMissingInitialSubaccountName + return nil, errInvalidSubAccountName } if arg.ToSubAccount == "" { - return nil, errMissingDestinationSubaccountName + return nil, errInvalidSubAccountName } var resp []TransferIDInfo - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, masterAccountsManageTransfersBetweenSubaccountEPL, http.MethodPost, assetSubaccountTransfer, &arg, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, masterAccountsManageTransfersBetweenSubaccountEPL, http.MethodPost, "asset/subaccount/transfer", &arg, &resp, request.AuthenticatedRequest) } -// SetPermissionOfTransferOut set permission of transfer out for sub-account(only applicable to master account). Sub-account can transfer out to master account by default. -func (ok *Okx) SetPermissionOfTransferOut(ctx context.Context, arg PermissionOfTransfer) ([]PermissionOfTransfer, error) { +// SetPermissionOfTransferOut set permission of transfer out for sub-account(only applicable to master account). Sub-account can transfer out to master account by default +func (ok *Okx) SetPermissionOfTransferOut(ctx context.Context, arg *PermissionOfTransfer) ([]PermissionOfTransfer, error) { + if *arg == (PermissionOfTransfer{}) { + return nil, common.ErrEmptyParams + } if arg.SubAcct == "" { - return nil, errMissingRequiredParameterSubaccountName + return nil, errInvalidSubAccountName } var resp []PermissionOfTransfer - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setPermissionOfTransferOutEPL, http.MethodPost, userSubaccountSetTransferOut, &arg, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setPermissionOfTransferOutEPL, http.MethodPost, "users/subaccount/set-transfer-out", &arg, &resp, request.AuthenticatedRequest) } // GetCustodyTradingSubaccountList the trading team uses this interface to view the list of sub-accounts currently under escrow @@ -2545,150 +2915,938 @@ func (ok *Okx) GetCustodyTradingSubaccountList(ctx context.Context, subaccountNa params.Set("setAcct", subaccountName) } var resp []SubaccountName - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getCustodyTradingSubaccountListEPL, http.MethodGet, common.EncodeURLValues(usersEntrustSubaccountList, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getCustodyTradingSubaccountListEPL, http.MethodGet, common.EncodeURLValues("users/entrust-subaccount-list", params), nil, &resp, request.AuthenticatedRequest) +} + +// SetSubAccountVIPLoanAllocation set the VIP loan allocation of sub-accounts. Only Applicable to master account API keys with Trade access +func (ok *Okx) SetSubAccountVIPLoanAllocation(ctx context.Context, arg *SubAccountLoanAllocationParam) (bool, error) { + if len(arg.Alloc) == 0 { + return false, common.ErrEmptyParams + } + for a := range arg.Alloc { + if arg.Alloc[a] == (subAccountVIPLoanAllocationInfo{}) { + return false, common.ErrEmptyParams + } + if arg.Alloc[a].SubAcct == "" { + return false, errInvalidSubAccountName + } + if arg.Alloc[a].LoanAlloc < 0 { + return false, errInvalidLoanAllocationValue + } + } + resp := &struct { + Result bool `json:"result"` + }{} + return resp.Result, ok.SendHTTPRequest(ctx, exchange.RestSpot, setSubAccountVIPLoanAllocationEPL, http.MethodPost, "account/subaccount/set-loan-allocation", arg, resp, request.AuthenticatedRequest) +} + +// GetSubAccountBorrowInterestAndLimit retrieves sub-account borrow interest and limit +// Only applicable to master account API keys. Only return VIP loan information +func (ok *Okx) GetSubAccountBorrowInterestAndLimit(ctx context.Context, subAccount string, ccy currency.Code) ([]SubAccounBorrowInterestAndLimit, error) { + if subAccount == "" { + return nil, errInvalidSubAccountName + } + params := url.Values{} + params.Set("subAcct", subAccount) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + var resp []SubAccounBorrowInterestAndLimit + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSubAccountBorrowInterestAndLimitEPL, http.MethodGet, common.EncodeURLValues("account/subaccount/interest-limits", params), nil, &resp, request.AuthenticatedRequest) +} + +/*************************************** Grid Trading Endpoints ***************************************************/ + +// PlaceGridAlgoOrder place spot grid algo order +func (ok *Okx) PlaceGridAlgoOrder(ctx context.Context, arg *GridAlgoOrder) (*GridAlgoOrderIDResponse, error) { + if *arg == (GridAlgoOrder{}) { + return nil, common.ErrEmptyParams + } + if arg.InstrumentID == "" { + return nil, errMissingInstrumentID + } + arg.AlgoOrdType = strings.ToLower(arg.AlgoOrdType) + if arg.AlgoOrdType != AlgoOrdTypeGrid && arg.AlgoOrdType != AlgoOrdTypeContractGrid { + return nil, errMissingAlgoOrderType + } + if arg.MaxPrice <= 0 { + return nil, order.ErrPriceBelowMin + } + if arg.MinPrice <= 0 { + return nil, order.ErrPriceBelowMin + } + if arg.GridQuantity < 0 { + return nil, errInvalidGridQuantity + } + isSpotGridOrder := arg.QuoteSize > 0 || arg.BaseSize > 0 + if !isSpotGridOrder { + if arg.Size <= 0 { + return nil, fmt.Errorf("%w: parameter Size is required", order.ErrAmountMustBeSet) + } + arg.Direction = strings.ToLower(arg.Direction) + if !slices.Contains([]string{positionSideLong, positionSideShort, "neutral"}, arg.Direction) { + return nil, errMissingRequiredArgumentDirection + } + if arg.Leverage == "" { + return nil, errInvalidLeverage + } + } + var resp *GridAlgoOrderIDResponse + err := ok.SendHTTPRequest(ctx, exchange.RestSpot, gridTradingEPL, http.MethodPost, "tradingBot/grid/order-algo", &arg, &resp, request.AuthenticatedRequest) + if err != nil { + if resp != nil && resp.StatusMessage != "" { + return nil, fmt.Errorf("%w, error code: %s error message: %s", err, resp.StatusCode, resp.StatusMessage) + } + return nil, err + } + return resp, nil +} + +// AmendGridAlgoOrder supported contract grid algo order amendment +func (ok *Okx) AmendGridAlgoOrder(ctx context.Context, arg *GridAlgoOrderAmend) (*GridAlgoOrderIDResponse, error) { + if *arg == (GridAlgoOrderAmend{}) { + return nil, common.ErrEmptyParams + } + if arg.AlgoID == "" { + return nil, errAlgoIDRequired + } + if arg.InstrumentID == "" { + return nil, errMissingInstrumentID + } + var resp *GridAlgoOrderIDResponse + err := ok.SendHTTPRequest(ctx, exchange.RestSpot, amendGridAlgoOrderEPL, http.MethodPost, "tradingBot/grid/amend-order-algo", &arg, &resp, request.AuthenticatedRequest) + if err != nil { + if resp != nil && resp.StatusMessage == "" { + return nil, fmt.Errorf("%w, error code: %s and error message: %s", err, resp.StatusMessage, resp.StatusCode) + } + return nil, err + } + return resp, nil +} + +// StopGridAlgoOrder stop a batch of grid algo orders. +// A maximum of 10 orders can be canceled per request +func (ok *Okx) StopGridAlgoOrder(ctx context.Context, arg []StopGridAlgoOrderRequest) ([]GridAlgoOrderIDResponse, error) { + if len(arg) == 0 { + return nil, common.ErrEmptyParams + } + for x := range arg { + if (arg[x]) == (StopGridAlgoOrderRequest{}) { + return nil, common.ErrEmptyParams + } + if arg[x].AlgoID == "" { + return nil, errAlgoIDRequired + } + if arg[x].InstrumentID == "" { + return nil, errMissingInstrumentID + } + arg[x].AlgoOrderType = strings.ToLower(arg[x].AlgoOrderType) + if arg[x].AlgoOrderType != AlgoOrdTypeGrid && arg[x].AlgoOrderType != AlgoOrdTypeContractGrid { + return nil, errMissingAlgoOrderType + } + if arg[x].StopType != 1 && arg[x].StopType != 2 { + return nil, errMissingValidStopType + } + } + var resp []GridAlgoOrderIDResponse + err := ok.SendHTTPRequest(ctx, exchange.RestSpot, stopGridAlgoOrderEPL, http.MethodPost, "tradingBot/grid/stop-order-algo", arg, &resp, request.AuthenticatedRequest) + if err != nil { + if len(resp) == 0 { + return nil, err + } + return nil, fmt.Errorf("error code:%s error message: %v", resp[0].StatusCode, resp[0].StatusMessage) + } + return resp, nil +} + +// ClosePositionForContractID close position when the contract grid stop type is 'keep position' +func (ok *Okx) ClosePositionForContractID(ctx context.Context, arg *ClosePositionParams) (*ClosePositionContractGridResponse, error) { + if *arg == (ClosePositionParams{}) { + return nil, common.ErrEmptyParams + } + if arg.AlgoID == "" { + return nil, errAlgoIDRequired + } + if !arg.MarketCloseAllPositions && arg.Size <= 0 { + return nil, fmt.Errorf("%w 'size' is required", order.ErrAmountMustBeSet) + } + if !arg.MarketCloseAllPositions && arg.Price <= 0 { + return nil, order.ErrPriceBelowMin + } + var resp *ClosePositionContractGridResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, closePositionForForContractGridEPL, http.MethodPost, "tradingBot/grid/close-position", arg, &resp, request.AuthenticatedRequest) +} + +// CancelClosePositionOrderForContractGrid cancels close position order for contract grid +func (ok *Okx) CancelClosePositionOrderForContractGrid(ctx context.Context, arg *CancelClosePositionOrder) (*ClosePositionContractGridResponse, error) { + if *arg == (CancelClosePositionOrder{}) { + return nil, common.ErrEmptyParams + } + if arg.AlgoID == "" { + return nil, errAlgoIDRequired + } + if arg.OrderID == "" { + return nil, order.ErrOrderIDNotSet + } + var resp *ClosePositionContractGridResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelClosePositionOrderForContractGridEPL, http.MethodPost, "tradingBot/grid/cancel-close-order", arg, &resp, request.AuthenticatedRequest) +} + +// InstantTriggerGridAlgoOrder triggers grid algo order +func (ok *Okx) InstantTriggerGridAlgoOrder(ctx context.Context, algoID string) (*TriggeredGridAlgoOrderInfo, error) { + var resp *TriggeredGridAlgoOrderInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, instantTriggerGridAlgoOrderEPL, http.MethodPost, "tradingBot/grid/order-instant-trigger", &map[string]string{"algoId": algoID}, &resp, request.AuthenticatedRequest) +} + +// GetGridAlgoOrdersList retrieves list of pending grid algo orders with the complete data +func (ok *Okx) GetGridAlgoOrdersList(ctx context.Context, algoOrderType, algoID, + instrumentID, instrumentType, + after, before string, limit int64) ([]GridAlgoOrderResponse, error) { + return ok.getGridAlgoOrders(ctx, algoOrderType, algoID, + instrumentID, instrumentType, + after, before, "tradingBot/grid/orders-algo-pending", limit) +} + +// GetGridAlgoOrderHistory retrieves list of grid algo orders with the complete data including the stopped orders +func (ok *Okx) GetGridAlgoOrderHistory(ctx context.Context, algoOrderType, algoID, + instrumentID, instrumentType, + after, before string, limit int64) ([]GridAlgoOrderResponse, error) { + return ok.getGridAlgoOrders(ctx, algoOrderType, algoID, + instrumentID, instrumentType, + after, before, "tradingBot/grid/orders-algo-history", limit) +} + +// getGridAlgoOrderList retrieves list of grid algo orders with the complete data +func (ok *Okx) getGridAlgoOrders(ctx context.Context, algoOrderType, algoID, + instrumentID, instrumentType, + after, before, route string, limit int64) ([]GridAlgoOrderResponse, error) { + algoOrderType = strings.ToLower(algoOrderType) + if algoOrderType != AlgoOrdTypeGrid && algoOrderType != AlgoOrdTypeContractGrid { + return nil, errMissingAlgoOrderType + } + params := url.Values{} + params.Set("algoOrdType", algoOrderType) + if algoID != "" { + params.Set("algoId", algoID) + } + if instrumentID != "" { + params.Set("instId", instrumentID) + } + if instrumentType != "" { + params.Set("instType", strings.ToUpper(instrumentType)) + } + if after != "" { + params.Set("after", after) + } + if before != "" { + params.Set("before", before) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + epl := getGridAlgoOrderListEPL + if route == "tradingBot/grid/orders-algo-history" { + epl = getGridAlgoOrderHistoryEPL + } + var resp []GridAlgoOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest) +} + +// GetGridAlgoOrderDetails retrieves grid algo order details +func (ok *Okx) GetGridAlgoOrderDetails(ctx context.Context, algoOrderType, algoID string) (*GridAlgoOrderResponse, error) { + if algoOrderType != AlgoOrdTypeGrid && + algoOrderType != AlgoOrdTypeContractGrid { + return nil, errMissingAlgoOrderType + } + if algoID == "" { + return nil, errAlgoIDRequired + } + params := url.Values{} + params.Set("algoOrdType", algoOrderType) + params.Set("algoId", algoID) + var resp *GridAlgoOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoOrderDetailsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/orders-algo-details", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetGridAlgoSubOrders retrieves grid algo sub orders +func (ok *Okx) GetGridAlgoSubOrders(ctx context.Context, algoOrderType, algoID, subOrderType, groupID, after, before string, limit int64) ([]GridAlgoOrderResponse, error) { + if algoOrderType != AlgoOrdTypeGrid && + algoOrderType != AlgoOrdTypeContractGrid { + return nil, errMissingAlgoOrderType + } + if algoID == "" { + return nil, errAlgoIDRequired + } + if subOrderType != "live" && subOrderType != order.Filled.String() { + return nil, errMissingSubOrderType + } + params := url.Values{} + params.Set("algoOrdType", algoOrderType) + params.Set("algoId", algoID) + params.Set("type", subOrderType) + if groupID != "" { + params.Set("groupId", groupID) + } + if after != "" { + params.Set("after", after) + } + if before != "" { + params.Set("before", before) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []GridAlgoOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoSubOrdersEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/sub-orders", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetGridAlgoOrderPositions retrieves grid algo order positions +func (ok *Okx) GetGridAlgoOrderPositions(ctx context.Context, algoOrderType, algoID string) ([]AlgoOrderPosition, error) { + if algoOrderType != AlgoOrdTypeGrid && algoOrderType != AlgoOrdTypeContractGrid { + return nil, errInvalidAlgoOrderType + } + if algoID == "" { + return nil, errAlgoIDRequired + } + params := url.Values{} + params.Set("algoOrdType", algoOrderType) + params.Set("algoId", algoID) + var resp []AlgoOrderPosition + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoOrderPositionsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/positions", params), nil, &resp, request.AuthenticatedRequest) +} + +// SpotGridWithdrawProfit returns the spot grid orders withdrawal profit given an instrument id +func (ok *Okx) SpotGridWithdrawProfit(ctx context.Context, algoID string) (*AlgoOrderWithdrawalProfit, error) { + if algoID == "" { + return nil, errAlgoIDRequired + } + input := &struct { + AlgoID string `json:"algoId"` + }{ + AlgoID: algoID, + } + var resp *AlgoOrderWithdrawalProfit + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, spotGridWithdrawIncomeEPL, http.MethodPost, "tradingBot/grid/withdraw-income", input, &resp, request.AuthenticatedRequest) +} + +// ComputeMarginBalance computes margin balance with 'add' and 'reduce' balance type +func (ok *Okx) ComputeMarginBalance(ctx context.Context, arg MarginBalanceParam) (*ComputeMarginBalance, error) { + if arg.AlgoID == "" { + return nil, errAlgoIDRequired + } + if arg.AdjustMarginBalanceType != "add" && arg.AdjustMarginBalanceType != marginBalanceReduce { + return nil, errInvalidMarginTypeAdjust + } + var resp *ComputeMarginBalance + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, computeMarginBalanceEPL, http.MethodPost, "tradingBot/grid/compute-margin-balance", &arg, &resp, request.AuthenticatedRequest) +} + +// AdjustMarginBalance retrieves adjust margin balance with 'add' and 'reduce' balance type +func (ok *Okx) AdjustMarginBalance(ctx context.Context, arg *MarginBalanceParam) (*AdjustMarginBalanceResponse, error) { + if *arg == (MarginBalanceParam{}) { + return nil, common.ErrEmptyParams + } + if arg.AlgoID == "" { + return nil, errAlgoIDRequired + } + if arg.AdjustMarginBalanceType != "add" && arg.AdjustMarginBalanceType != marginBalanceReduce { + return nil, errInvalidMarginTypeAdjust + } + if arg.Percentage <= 0 && arg.Amount <= 0 { + return nil, fmt.Errorf("%w, either percentage or amount is required", order.ErrAmountIsInvalid) + } + var resp *AdjustMarginBalanceResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, adjustMarginBalanceEPL, http.MethodPost, "tradingBot/grid/margin-balance", &arg, &resp, request.AuthenticatedRequest) +} + +// GetGridAIParameter retrieves grid AI parameter +func (ok *Okx) GetGridAIParameter(ctx context.Context, algoOrderType, instrumentID, direction, duration string) ([]GridAIParameterResponse, error) { + if !slices.Contains([]string{"moon_grid", "contract_grid", "grid"}, algoOrderType) { + return nil, errInvalidAlgoOrderType + } + if instrumentID == "" { + return nil, errMissingInstrumentID + } + if algoOrderType == "contract_grid" && !slices.Contains([]string{positionSideLong, positionSideShort, "neutral"}, direction) { + return nil, fmt.Errorf("%w, required for 'contract_grid' algo order type", errMissingRequiredArgumentDirection) + } + params := url.Values{} + params.Set("direction", direction) + params.Set("algoOrdType", algoOrderType) + params.Set("instId", instrumentID) + if !slices.Contains([]string{"", "7D", "30D", "180D"}, duration) { + return nil, errInvalidDuration + } + var resp []GridAIParameterResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAIParameterEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/ai-param", params), nil, &resp, request.UnauthenticatedRequest) +} + +// ComputeMinInvestment computes minimum investment +func (ok *Okx) ComputeMinInvestment(ctx context.Context, arg *ComputeInvestmentDataParam) (*InvestmentResult, error) { + if arg.InstrumentID == "" { + return nil, errMissingInstrumentID + } + switch arg.AlgoOrderType { + case "grid", "contract_grid": + default: + return nil, errInvalidAlgoOrderType + } + if arg.MaxPrice <= 0 { + return nil, fmt.Errorf("%w, maxPrice = %f", order.ErrPriceBelowMin, arg.MaxPrice) + } + if arg.MinPrice <= 0 { + return nil, fmt.Errorf("%w, minPrice = %f", order.ErrPriceBelowMin, arg.MaxPrice) + } + if arg.GridNumber == 0 { + return nil, fmt.Errorf("%w, grid number is required", errInvalidGridQuantity) + } + if arg.RunType == "" { + return nil, errRunTypeRequired + } + if arg.AlgoOrderType == "contract_grid" { + switch arg.Direction { + case positionSideLong, positionSideShort, "neutral": + default: + return nil, fmt.Errorf("%w, invalid grid direction %s", errMissingRequiredArgumentDirection, arg.Direction) + } + if arg.Leverage <= 0 { + return nil, errInvalidLeverage + } + } + for x := range arg.InvestmentData { + if arg.InvestmentData[x].Amount <= 0 { + return nil, fmt.Errorf("%w, investment amt = %f", order.ErrAmountBelowMin, arg.InvestmentData[x].Amount) + } + if arg.InvestmentData[x].Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + } + var resp *InvestmentResult + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, computeMinInvestmentEPL, http.MethodPost, "tradingBot/grid/min-investment", arg, &resp, request.UnauthenticatedRequest) +} + +// RSIBackTesting relative strength index(RSI) backtesting +// Parameters: +// +// TriggerCondition: possible values are "cross_up" "cross_down" "above" "below" "cross" Default is cross_down +// +// Threshold: The value should be an integer between 1 to 100 +func (ok *Okx) RSIBackTesting(ctx context.Context, instrumentID, triggerCondition, duration string, threshold, timePeriod int64, timeFrame kline.Interval) (*RSIBacktestingResponse, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } + if threshold > 100 || threshold < 1 { + return nil, errors.New("threshold should be an integer between 1 to 100") + } + timeFrameString := IntervalFromString(timeFrame, false) + if timeFrameString == "" { + return nil, errors.New("timeframe is required") + } + params := url.Values{} + params.Set("instId", instrumentID) + params.Set("timeframe", timeFrameString) + params.Set("thold", strconv.FormatInt(threshold, 10)) + if timePeriod > 0 { + params.Set("timePeriod", strconv.FormatInt(timePeriod, 10)) + } + if triggerCondition != "" { + params.Set("triggerCond", triggerCondition) + } + if duration != "" { + params.Set("duration", duration) + } + var resp *RSIBacktestingResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rsiBackTestingEPL, http.MethodGet, common.EncodeURLValues("tradingBot/public/rsi-back-testing", params), nil, &resp, request.UnauthenticatedRequest) +} + +// ****************************************** Signal bot trading ************************************************** + +// GetSignalBotOrderDetail create and customize your own signals while gaining access to a diverse selection of signals from top providers. +// Empower your trading strategies and stay ahead of the game with our comprehensive signal trading platform +func (ok *Okx) GetSignalBotOrderDetail(ctx context.Context, algoOrderType, algoID string) (*SignalBotOrderDetail, error) { + if algoOrderType == "" { + return nil, errInvalidAlgoOrderType + } + if algoID == "" { + return nil, errAlgoIDRequired + } + params := url.Values{} + params.Set("algoId", algoID) + params.Set("algoOrdType", algoOrderType) + var resp *SignalBotOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, signalBotOrderDetailsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/orders-algo-details", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetSignalOrderPositions retrieves signal bot order positions +func (ok *Okx) GetSignalOrderPositions(ctx context.Context, algoOrderType, algoID string) (*SignalBotPosition, error) { + if algoOrderType == "" { + return nil, errInvalidAlgoOrderType + } + if algoID == "" { + return nil, errAlgoIDRequired + } + params := url.Values{} + params.Set("algoId", algoID) + params.Set("algoOrdType", algoOrderType) + var resp *SignalBotPosition + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, signalBotOrderPositionsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/positions", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetSignalBotSubOrders retrieves historical filled sub orders and designated sub orders +func (ok *Okx) GetSignalBotSubOrders(ctx context.Context, algoID, algoOrderType, subOrderType, clientOrderID, afterPaginationID, beforePaginationID string, begin, end time.Time, limit int64) ([]SubOrder, error) { + if algoID == "" { + return nil, errAlgoIDRequired + } + if algoOrderType == "" { + return nil, errInvalidAlgoOrderType + } + if subOrderType == "" && clientOrderID == "" { + return nil, fmt.Errorf("%w, either client order ID or sub-order state is required", order.ErrOrderIDNotSet) + } + params := url.Values{} + params.Set("algoId", algoID) + params.Set("algoOrdType", algoOrderType) + if subOrderType != "" { + params.Set("type", subOrderType) + } else { + params.Set("clOrdId", clientOrderID) + } + if afterPaginationID != "" { + params.Set("after", afterPaginationID) + } + if beforePaginationID != "" { + params.Set("before", beforePaginationID) + } + if !begin.IsZero() { + params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) + } + if !end.IsZero() { + params.Set("end", strconv.FormatInt(end.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []SubOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, signalBotSubOrdersEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/sub-orders", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetSignalBotEventHistory retrieves signal bot event history +func (ok *Okx) GetSignalBotEventHistory(ctx context.Context, algoID string, after, before time.Time, limit int64) ([]SignalBotEventHistory, error) { + if algoID == "" { + return nil, errAlgoIDRequired + } + params := url.Values{} + params.Set("algoId", algoID) + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []SignalBotEventHistory + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, signalBotEventHistoryEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/event-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// ****************************************** Recurring Buy ***************************************** + +// PlaceRecurringBuyOrder recurring buy is a strategy for investing a fixed amount in crypto at fixed intervals. +// An appropriate recurring approach in volatile markets allows you to buy crypto at lower costs. Learn more +// The API endpoints of Recurring buy require authentication +func (ok *Okx) PlaceRecurringBuyOrder(ctx context.Context, arg *PlaceRecurringBuyOrderParam) (*RecurringOrderResponse, error) { + if arg == nil { + return nil, common.ErrNilPointer + } + if arg.StrategyName == "" { + return nil, errStrategyNameRequired + } + if len(arg.RecurringList) == 0 { + return nil, fmt.Errorf("%w, no recurring list is provided", common.ErrEmptyParams) + } + for x := range arg.RecurringList { + if arg.RecurringList[x].Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + } + if arg.RecurringDay == "" { + return nil, errRecurringDayRequired + } + if arg.RecurringTime > 23 || arg.RecurringTime < 0 { + return nil, errRecurringBuyTimeRequired + } + if arg.TradeMode == "" { + return nil, errInvalidTradeModeValue + } + var resp *RecurringOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeRecurringBuyOrderEPL, http.MethodPost, "tradingBot/recurring/order-algo", arg, &resp, request.AuthenticatedRequest) +} + +// AmendRecurringBuyOrder amends recurring order +func (ok *Okx) AmendRecurringBuyOrder(ctx context.Context, arg *AmendRecurringOrderParam) (*RecurringOrderResponse, error) { + if (*arg) == (AmendRecurringOrderParam{}) { + return nil, common.ErrEmptyParams + } + if arg.AlgoID == "" { + return nil, errAlgoIDRequired + } + if arg.StrategyName == "" { + return nil, errStrategyNameRequired + } + var resp *RecurringOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendRecurringBuyOrderEPL, http.MethodPost, "tradingBot/recurring/amend-order-algo", arg, &resp, request.AuthenticatedRequest) +} + +// StopRecurringBuyOrder stops recurring buy order. A maximum of 10 orders can be stopped per request +func (ok *Okx) StopRecurringBuyOrder(ctx context.Context, arg []StopRecurringBuyOrder) ([]RecurringOrderResponse, error) { + if len(arg) == 0 { + return nil, common.ErrEmptyParams + } + for x := range arg { + if arg[x].AlgoID == "" { + return nil, errAlgoIDRequired + } + } + var resp []RecurringOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, stopRecurringBuyOrderEPL, http.MethodPost, "tradingBot/recurring/stop-order-algo", arg, &resp, request.AuthenticatedRequest) +} + +// GetRecurringBuyOrderList retrieves recurring buy order list +func (ok *Okx) GetRecurringBuyOrderList(ctx context.Context, algoID, algoOrderState string, after, before time.Time, limit int64) ([]RecurringOrderItem, error) { + params := url.Values{} + if algoOrderState != "" { + params.Set("state", algoOrderState) + } + if algoID != "" { + params.Set("algoId", algoID) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []RecurringOrderItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuyOrderListEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/orders-algo-pending", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetRecurringBuyOrderHistory retrieves recurring buy order history +func (ok *Okx) GetRecurringBuyOrderHistory(ctx context.Context, algoID string, after, before time.Time, limit int64) ([]RecurringOrderItem, error) { + params := url.Values{} + if algoID != "" { + params.Set("algoId", algoID) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []RecurringOrderItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuyOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/orders-algo-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetRecurringOrderDetails retrieves a single recurring order detail +func (ok *Okx) GetRecurringOrderDetails(ctx context.Context, algoID, algoOrderState string) (*RecurringOrderDeail, error) { + if algoID == "" { + return nil, errAlgoIDRequired + } + params := url.Values{} + params.Set("algoId", algoID) + if algoOrderState != "" { + params.Set("state", algoOrderState) + } + var resp *RecurringOrderDeail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuyOrderDetailEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/orders-algo-details", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetRecurringSubOrders retrieves recurring buy sub orders +func (ok *Okx) GetRecurringSubOrders(ctx context.Context, algoID, orderID string, after, before time.Time, limit int64) ([]RecurringBuySubOrder, error) { + if algoID == "" { + return nil, errAlgoIDRequired + } + params := url.Values{} + params.Set("algoId", algoID) + if orderID != "" { + params.Set("ordId", orderID) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []RecurringBuySubOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuySubOrdersEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/sub-orders", params), nil, &resp, request.AuthenticatedRequest) +} + +// ****************************************** Earn ************************************************** + +// GetExistingLeadingPositions retrieves leading positions that are not closed +func (ok *Okx) GetExistingLeadingPositions(ctx context.Context, instrumentType, instrumentID string, before, after time.Time, limit int64) ([]PositionInfo, error) { + params := url.Values{} + if instrumentType != "" { + params.Set("instType", instrumentType) + } + if instrumentID != "" { + params.Set("instId", instrumentID) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []PositionInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getExistingLeadingPositionsEPL, http.MethodGet, common.EncodeURLValues("copytrading/current-subpositions", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetLeadingPositionsHistory leading trader retrieves the completed leading position of the last 3 months. +// Returns reverse chronological order with subPosId +func (ok *Okx) GetLeadingPositionsHistory(ctx context.Context, instrumentType, instrumentID string, before, after time.Time, limit int64) ([]PositionInfo, error) { + params := url.Values{} + if instrumentType != "" { + params.Set("instType", instrumentType) + } + if instrumentID != "" { + params.Set("instId", instrumentID) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []PositionInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadingPositionHistoryEPL, http.MethodGet, common.EncodeURLValues("copytrading/subpositions-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// PlaceLeadingStopOrder holds leading trader sets TP/SL for the current leading position that are not closed +func (ok *Okx) PlaceLeadingStopOrder(ctx context.Context, arg *TPSLOrderParam) (*PositionIDInfo, error) { + if *arg == (TPSLOrderParam{}) { + return nil, common.ErrEmptyParams + } + if arg.SubPositionID == "" { + return nil, errSubPositionIDRequired + } + var resp *PositionIDInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeLeadingStopOrderEPL, http.MethodPost, "copytrading/algo-order", arg, &resp, request.AuthenticatedRequest) +} + +// CloseLeadingPosition close a leading position once a time +func (ok *Okx) CloseLeadingPosition(ctx context.Context, arg *CloseLeadingPositionParam) (*PositionIDInfo, error) { + if *arg == (CloseLeadingPositionParam{}) { + return nil, common.ErrEmptyParams + } + if arg.SubPositionID == "" { + return nil, errSubPositionIDRequired + } + var resp *PositionIDInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, closeLeadingPositionEPL, http.MethodPost, "copytrading/close-subposition", arg, &resp, request.AuthenticatedRequest) +} + +// GetLeadingInstrument retrieves leading instruments +func (ok *Okx) GetLeadingInstrument(ctx context.Context, instrumentType string) ([]LeadingInstrumentItem, error) { + params := url.Values{} + if instrumentType == "" { + params.Set("instType", instrumentType) + } + var resp []LeadingInstrumentItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadingInstrumentsEPL, http.MethodGet, common.EncodeURLValues("copytrading/instruments", params), nil, &resp, request.AuthenticatedRequest) +} + +// AmendLeadingInstruments amend current leading instruments, need to set initial leading instruments while applying to become a leading trader. +// All non-leading contracts can't have position or pending orders for the current request when setting non-leading contracts as leading contracts +func (ok *Okx) AmendLeadingInstruments(ctx context.Context, instrumentID, instrumentType string) ([]LeadingInstrumentItem, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } + var resp []LeadingInstrumentItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadingInstrumentsEPL, http.MethodPost, "copytrading/set-instruments", &struct { + InstrumentType string `json:"instType,omitempty"` + InstrumentID string `json:"instId"` + }{ + InstrumentID: instrumentID, + InstrumentType: instrumentType, + }, &resp, request.AuthenticatedRequest) +} + +// GetProfitSharingDetails gets profits shared details for the last 3 months +func (ok *Okx) GetProfitSharingDetails(ctx context.Context, instrumentType string, before, after time.Time, limit int64) ([]ProfitSharingItem, error) { + params := url.Values{} + if instrumentType != "" { + params.Set("instType", instrumentType) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []ProfitSharingItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getProfitSharingLimitEPL, http.MethodGet, common.EncodeURLValues("copytrading/profit-sharing-details", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetTotalProfitSharing gets the total amount of profit shared since joining the platform. +// Instrument type 'SPOT' 'SWAP' It returns all types by default +func (ok *Okx) GetTotalProfitSharing(ctx context.Context, instrumentType string) ([]TotalProfitSharing, error) { + params := url.Values{} + if instrumentType != "" { + params.Set("instType", instrumentType) + } + var resp []TotalProfitSharing + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTotalProfitSharingEPL, http.MethodGet, common.EncodeURLValues("copytrading/total-profit-sharing", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetUnrealizedProfitSharingDetails gets leading trader gets the profit sharing details that are expected to be shared in the next settlement cycle. +// The unrealized profit sharing details will update once there copy position is closed +func (ok *Okx) GetUnrealizedProfitSharingDetails(ctx context.Context, instrumentType string) ([]ProfitSharingItem, error) { + params := url.Values{} + if instrumentType != "" { + params.Set("instType", instrumentType) + } + var resp []ProfitSharingItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTotalProfitSharingEPL, http.MethodGet, common.EncodeURLValues("copytrading/unrealized-profit-sharing-details", params), nil, &resp, request.AuthenticatedRequest) +} + +// SetFirstCopySettings set first copy settings for the certain lead trader. You need to first copy settings after stopping copying +func (ok *Okx) SetFirstCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseSuccess, error) { + err := validateFirstCopySettings(arg) + if err != nil { + return nil, err + } + var resp *ResponseSuccess + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setFirstCopySettingsEPL, http.MethodPost, "copytrading/first-copy-settings", arg, &resp, request.AuthenticatedRequest) +} + +// AmendCopySettings amends need to use this endpoint for amending copy settings +func (ok *Okx) AmendCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseSuccess, error) { + err := validateFirstCopySettings(arg) + if err != nil { + return nil, err + } + var resp *ResponseSuccess + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendFirstCopySettingsEPL, http.MethodPost, "copytrading/amend-copy-settings", arg, &resp, request.AuthenticatedRequest) } -/*************************************** Grid Trading Endpoints ***************************************************/ - -// PlaceGridAlgoOrder place spot grid algo order. -func (ok *Okx) PlaceGridAlgoOrder(ctx context.Context, arg *GridAlgoOrder) (*GridAlgoOrderIDResponse, error) { - if arg == nil { - return nil, errNilArgument +func validateFirstCopySettings(arg *FirstCopySettings) error { + if *arg == (FirstCopySettings{}) { + return common.ErrEmptyParams } - if arg.InstrumentID == "" { - return nil, errMissingInstrumentID - } - arg.AlgoOrdType = strings.ToLower(arg.AlgoOrdType) - if arg.AlgoOrdType != AlgoOrdTypeGrid && arg.AlgoOrdType != AlgoOrdTypeContractGrid { - return nil, errMissingAlgoOrderType + if arg.UniqueCode == "" { + return errUniqueCodeRequired } - if arg.MaxPrice <= 0 { - return nil, errInvalidMaximumPrice + if arg.CopyInstrumentIDType == "" { + return errCopyInstrumentIDTypeRequired } - if arg.MinPrice < 0 { - return nil, errInvalidMinimumPrice + if arg.CopyTotalAmount <= 0 { + return fmt.Errorf("%w, copyTotalAmount value %f", order.ErrAmountBelowMin, arg.CopyTotalAmount) } - if arg.GridQuantity < 0 { - return nil, errInvalidGridQuantity + if arg.SubPosCloseType == "" { + return errSubPositionCloseTypeRequired } - isSpotGridOrder := arg.QuoteSize > 0 || arg.BaseSize > 0 - if !isSpotGridOrder { - if arg.Size <= 0 { - return nil, errMissingSize - } - arg.Direction = strings.ToLower(arg.Direction) - if arg.Direction != positionSideLong && arg.Direction != positionSideShort && arg.Direction != "neutral" { - return nil, errMissingRequiredArgumentDirection - } - if arg.Lever == "" { - return nil, errRequiredParameterMissingLeverage - } + return nil +} + +// StopCopying need to use this endpoint for amending copy settings +func (ok *Okx) StopCopying(ctx context.Context, arg *StopCopyingParameter) (*ResponseSuccess, error) { + if *arg == (StopCopyingParameter{}) { + return nil, common.ErrEmptyParams } - var resp []GridAlgoOrderIDResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, gridTradingEPL, http.MethodPost, gridOrderAlgo, &arg, &resp, true) - if err != nil { - if len(resp) != 1 { - return nil, err - } - return nil, fmt.Errorf("error code:%s message: %v", resp[0].SCode, resp[0].SMsg) + if arg.UniqueCode == "" { + return nil, errUniqueCodeRequired } - if len(resp) == 1 { - return &resp[0], nil + if arg.SubPositionCloseType == "" { + return nil, errSubPositionCloseTypeRequired } - return nil, errNoValidResponseFromServer + var resp *ResponseSuccess + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, stopCopyingEPL, http.MethodPost, "copytrading/stop-copy-trading", arg, &resp, request.AuthenticatedRequest) } -// AmendGridAlgoOrder supported contract grid algo order amendment. -func (ok *Okx) AmendGridAlgoOrder(ctx context.Context, arg GridAlgoOrderAmend) (*GridAlgoOrderIDResponse, error) { - if arg.AlgoID == "" { - return nil, errMissingAlgoOrderID - } - if arg.InstrumentID == "" { - return nil, errMissingInstrumentID +// GetCopySettings retrieve the copy settings about certain lead trader +func (ok *Okx) GetCopySettings(ctx context.Context, instrumentType, uniqueCode string) (*CopySetting, error) { + if uniqueCode == "" { + return nil, errUniqueCodeRequired } - var resp []GridAlgoOrderIDResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, amendGridAlgoOrderEPL, http.MethodPost, gridAmendOrderAlgo, &arg, &resp, true) - if err != nil { - if len(resp) != 1 { - return nil, err - } - return nil, fmt.Errorf("error code:%s message: %v", resp[0].SCode, resp[0].SMsg) - } - if len(resp) == 1 { - return &resp[0], nil + params := url.Values{} + params.Set("uniqueCode", uniqueCode) + if instrumentType == "" { + params.Set("instType", instrumentType) } - return nil, errNoValidResponseFromServer + var resp *CopySetting + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getCopySettingsEPL, http.MethodGet, common.EncodeURLValues("copytrading/copy-settings", params), nil, &resp, request.AuthenticatedRequest) } -// StopGridAlgoOrder stop a batch of grid algo orders. -func (ok *Okx) StopGridAlgoOrder(ctx context.Context, arg []StopGridAlgoOrderRequest) ([]GridAlgoOrderIDResponse, error) { - if len(arg) == 0 { - return nil, errEmptyArgument - } else if len(arg) > 10 { - return nil, fmt.Errorf("%w, a maximum of 10 orders can be canceled per request", errTooManyArgument) +// GetMultipleLeverages retrieve leverages that belong to the lead trader and you +func (ok *Okx) GetMultipleLeverages(ctx context.Context, marginMode, uniqueCode, instrumentID string) ([]Leverages, error) { + if marginMode == "" { + return nil, margin.ErrInvalidMarginType } - for x := range arg { - if arg[x].AlgoID == "" { - return nil, errMissingAlgoOrderID - } - if arg[x].InstrumentID == "" { - return nil, errMissingInstrumentID - } - arg[x].AlgoOrderType = strings.ToLower(arg[x].AlgoOrderType) - if arg[x].AlgoOrderType != AlgoOrdTypeGrid && arg[x].AlgoOrderType != AlgoOrdTypeContractGrid { - return nil, errMissingAlgoOrderType - } - if arg[x].StopType != 1 && arg[x].StopType != 2 { - return nil, errMissingValidStopType - } + if uniqueCode == "" { + return nil, errUniqueCodeRequired } - var resp []GridAlgoOrderIDResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, stopGridAlgoOrderEPL, http.MethodPost, gridAlgoOrderStop, arg, &resp, true) - if err != nil { - if len(resp) == 0 { - return nil, err - } - return nil, fmt.Errorf("error code:%s message: %v", resp[0].SCode, resp[0].SMsg) + params := url.Values{} + params.Set("mgnMode", marginMode) + params.Set("uniqueCode", uniqueCode) + if instrumentID != "" { + params.Set("instId", instrumentID) } - return resp, nil + var resp []Leverages + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMultipleLeveragesEPL, http.MethodGet, common.EncodeURLValues("copytrading/batch-leverage-info", params), nil, &resp, request.AuthenticatedRequest) } -// GetGridAlgoOrdersList retrieves list of pending grid algo orders with the complete data. -func (ok *Okx) GetGridAlgoOrdersList(ctx context.Context, algoOrderType, algoID, - instrumentID, instrumentType, - after, before string, limit int64) ([]GridAlgoOrderResponse, error) { - return ok.getGridAlgoOrders(ctx, algoOrderType, algoID, - instrumentID, instrumentType, - after, before, gridAlgoOrders, limit) +// SetMultipleLeverages set Multiple leverages +func (ok *Okx) SetMultipleLeverages(ctx context.Context, arg *SetLeveragesParam) (*SetMultipleLeverageResponse, error) { + if *arg == (SetLeveragesParam{}) { + return nil, common.ErrEmptyParams + } + if arg.MarginMode == "" { + return nil, margin.ErrInvalidMarginType + } + if arg.Leverage <= 0 { + return nil, errInvalidLeverage + } + if arg.InstrumentID == "" { + return nil, errMissingInstrumentID + } + var resp *SetMultipleLeverageResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, setBatchLeverageEPL, http.MethodPost, "copytrading/batch-set-leverage", arg, &resp, request.AuthenticatedRequest) } -// GetGridAlgoOrderHistory retrieves list of grid algo orders with the complete data including the stopped orders. -func (ok *Okx) GetGridAlgoOrderHistory(ctx context.Context, algoOrderType, algoID, - instrumentID, instrumentType, - after, before string, limit int64) ([]GridAlgoOrderResponse, error) { - return ok.getGridAlgoOrders(ctx, algoOrderType, algoID, - instrumentID, instrumentType, - after, before, gridAlgoOrdersHistory, limit) +// GetMyLeadTraders retrieve my lead traders +func (ok *Okx) GetMyLeadTraders(ctx context.Context, instrumentType string) ([]CopyTradingLeadTrader, error) { + params := url.Values{} + if instrumentType != "" { + params.Set("instType", instrumentType) + } + var resp []CopyTradingLeadTrader + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMyLeadTradersEPL, http.MethodGet, common.EncodeURLValues("copytrading/current-lead-traders", params), nil, &resp, request.AuthenticatedRequest) } -// getGridAlgoOrderList retrieves list of grid algo orders with the complete data. -func (ok *Okx) getGridAlgoOrders(ctx context.Context, algoOrderType, algoID, - instrumentID, instrumentType, - after, before, route string, limit int64) ([]GridAlgoOrderResponse, error) { +// GetHistoryLeadTraders retrieve my history lead traders +func (ok *Okx) GetHistoryLeadTraders(ctx context.Context, instrumentType, after, before string, limit int64) ([]CopyTradingLeadTrader, error) { params := url.Values{} - algoOrderType = strings.ToLower(algoOrderType) - if algoOrderType != AlgoOrdTypeGrid && algoOrderType != AlgoOrdTypeContractGrid { - return nil, errMissingAlgoOrderType - } - params.Set("algoOrdType", algoOrderType) - if algoID != "" { - params.Set("algoId", algoID) - } - if instrumentID != "" { - params.Set("instId", instrumentID) - } if instrumentType != "" { - params.Set("instType", strings.ToUpper(instrumentType)) + params.Set("instType", instrumentType) } if after != "" { params.Set("after", after) @@ -2699,171 +3857,177 @@ func (ok *Okx) getGridAlgoOrders(ctx context.Context, algoOrderType, algoID, if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - epl := getGridAlgoOrderListEPL - if route == gridAlgoOrdersHistory { - epl = getGridAlgoOrderHistoryEPL - } - var resp []GridAlgoOrderResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, true) + var resp []CopyTradingLeadTrader + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMyLeadTradersEPL, http.MethodGet, common.EncodeURLValues("copytrading/lead-traders-history", params), nil, &resp, request.AuthenticatedRequest) } -// GetGridAlgoOrderDetails retrieves grid algo order details -func (ok *Okx) GetGridAlgoOrderDetails(ctx context.Context, algoOrderType, algoID string) (*GridAlgoOrderResponse, error) { +// GetLeadTradersRanks retrieve lead trader ranks. +// Instrument type: SWAP, the default value +// Sort type"overview": overview, the default value "pnl": profit and loss "aum": assets under management "win_ratio": win ratio "pnl_ratio": pnl ratio "current_copy_trader_pnl": current copy trader pnl +// Lead trader state: "0": All lead traders, the default, including vacancy and non-vacancy "1": lead traders who have vacancy +// Minimum lead days '1': 7 days '2': 30 days '3': 90 days '4': 180 days +func (ok *Okx) GetLeadTradersRanks(ctx context.Context, instrumentType, sortType, state, + minLeadDays, minAssets, maxAssets, minAssetUnderManagement, maxAssetUnderManagement, + dataVersion, page string, limit int64) ([]LeadTradersRank, error) { params := url.Values{} - if algoOrderType != AlgoOrdTypeGrid && - algoOrderType != AlgoOrdTypeContractGrid { - return nil, errMissingAlgoOrderType + if instrumentType != "" { + params.Set("instType", instrumentType) } - if algoID == "" { - return nil, errMissingAlgoOrderID + if sortType != "" { + params.Set("sortType", sortType) } - params.Set("algoOrdType", algoOrderType) - params.Set("algoId", algoID) - var resp []GridAlgoOrderResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoOrderDetailsEPL, http.MethodGet, common.EncodeURLValues(gridOrdersAlgoDetails, params), nil, &resp, true) - if err != nil { - return nil, err + if state != "" { + params.Set("state", state) } - if len(resp) == 1 { - return &resp[0], nil + if minLeadDays != "" { + params.Set("minLeadDays", minLeadDays) } - return nil, errNoValidResponseFromServer -} - -// GetGridAlgoSubOrders retrieves grid algo sub orders -func (ok *Okx) GetGridAlgoSubOrders(ctx context.Context, algoOrderType, algoID, subOrderType, groupID, after, before string, limit int64) ([]GridAlgoOrderResponse, error) { - params := url.Values{} - if algoOrderType != AlgoOrdTypeGrid && - algoOrderType != AlgoOrdTypeContractGrid { - return nil, errMissingAlgoOrderType + if minAssets != "" { + params.Set("minAssets", minAssets) } - params.Set("algoOrdType", algoOrderType) - if algoID != "" { - params.Set("algoId", algoID) - } else { - return nil, errMissingAlgoOrderID + if maxAssets != "" { + params.Set("maxAssets", maxAssets) } - if subOrderType != "live" && subOrderType != order.Filled.String() { - return nil, errMissingSubOrderType + if minAssetUnderManagement != "" { + params.Set("minAum", minAssetUnderManagement) } - params.Set("type", subOrderType) - if groupID != "" { - params.Set("groupId", groupID) + if maxAssetUnderManagement != "" { + params.Set("maxAum", maxAssetUnderManagement) } - if after != "" { - params.Set("after", after) + if dataVersion != "" { + params.Set("dataVer", dataVersion) } - if before != "" { - params.Set("before", before) + if page != "" { + params.Set("page", page) } if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - var resp []GridAlgoOrderResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoSubOrdersEPL, http.MethodGet, common.EncodeURLValues(gridSuborders, params), nil, &resp, true) + var resp []LeadTradersRank + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderRanksEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-lead-traders", params), nil, &resp, request.UnauthenticatedRequest) } -// GetGridAlgoOrderPositions retrieves grid algo order positions. -func (ok *Okx) GetGridAlgoOrderPositions(ctx context.Context, algoOrderType, algoID string) ([]AlgoOrderPosition, error) { - params := url.Values{} - if algoOrderType != AlgoOrdTypeGrid && algoOrderType != AlgoOrdTypeContractGrid { - return nil, errInvalidAlgoOrderType +// GetWeeklyTraderProfitAndLoss retrieve lead trader weekly pnl. Results are returned in counter chronological order +func (ok *Okx) GetWeeklyTraderProfitAndLoss(ctx context.Context, instrumentType, uniqueCode string) ([]TraderWeeklyProfitAndLoss, error) { + if uniqueCode == "" { + return nil, errUniqueCodeRequired } - if algoID == "" { - return nil, errMissingAlgoOrderID + params := url.Values{} + params.Set("uniqueCode", uniqueCode) + if instrumentType != "" { + params.Set("instType", instrumentType) } - params.Set("algoOrdType", algoOrderType) - params.Set("algoId", algoID) - var resp []AlgoOrderPosition - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoOrderPositionsEPL, http.MethodGet, common.EncodeURLValues(gridPositions, params), nil, &resp, true) + var resp []TraderWeeklyProfitAndLoss + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderWeeklyPNLEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-weekly-pnl", params), nil, &resp, request.UnauthenticatedRequest) } -// SpotGridWithdrawProfit returns the spot grid orders withdrawal profit given an instrument id. -func (ok *Okx) SpotGridWithdrawProfit(ctx context.Context, algoID string) (*AlgoOrderWithdrawalProfit, error) { - if algoID == "" { - return nil, errMissingAlgoOrderID - } - input := &struct { - AlgoID string `json:"algoId"` - }{ - AlgoID: algoID, +// GetDailyLeadTraderPNL retrieve lead trader daily pnl. Results are returned in counter chronological order. +// Last days "1": last 7 days "2": last 30 days "3": last 90 days "4": last 365 days +func (ok *Okx) GetDailyLeadTraderPNL(ctx context.Context, instrumentType, uniqueCode, lastDays string) ([]TraderWeeklyProfitAndLoss, error) { + if uniqueCode == "" { + return nil, errUniqueCodeRequired } - var resp []AlgoOrderWithdrawalProfit - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, spotGridWithdrawIncomeEPL, http.MethodPost, gridWithdrawalIncome, input, &resp, true) - if err != nil { - return nil, err + if lastDays == "" { + return nil, errLastDaysRequired } - if len(resp) == 1 { - return &resp[0], nil + params := url.Values{} + params.Set("uniqueCode", uniqueCode) + params.Set("lastDays", lastDays) + if instrumentType != "" { + params.Set("instType", instrumentType) } - return nil, errNoValidResponseFromServer + var resp []TraderWeeklyProfitAndLoss + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderDailyPNLEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-weekly-pnl", params), nil, &resp, request.UnauthenticatedRequest) } -// ComputeMarginBalance computes margin balance with 'add' and 'reduce' balance type -func (ok *Okx) ComputeMarginBalance(ctx context.Context, arg MarginBalanceParam) (*ComputeMarginBalance, error) { - if arg.AlgoID == "" { - return nil, errInvalidAlgoID +// GetLeadTraderStats retrieves key data related to lead trader performance +func (ok *Okx) GetLeadTraderStats(ctx context.Context, instrumentType, uniqueCode, lastDays string) ([]LeadTraderStat, error) { + if uniqueCode == "" { + return nil, errUniqueCodeRequired } - if arg.Type != "add" && arg.Type != "reduce" { - return nil, errInvalidMarginTypeAdjust + if lastDays == "" { + return nil, errLastDaysRequired } - var resp []ComputeMarginBalance - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, computeMarginBalanceEPL, http.MethodPost, gridComputeMarginBalance, &arg, &resp, true); err != nil { - return nil, err + params := url.Values{} + params.Set("uniqueCode", uniqueCode) + params.Set("lastDays", lastDays) + if instrumentType != "" { + params.Set("instType", instrumentType) + } + var resp []LeadTraderStat + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderStatsEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-stats", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetLeadTraderCurrencyPreferences retrieves the most frequently traded crypto of this lead trader. Results are sorted by ratio from large to small +func (ok *Okx) GetLeadTraderCurrencyPreferences(ctx context.Context, instrumentType, uniqueCode, lastDays string) ([]LeadTraderCurrencyPreference, error) { + if uniqueCode == "" { + return nil, errUniqueCodeRequired + } + if lastDays == "" { + return nil, errLastDaysRequired } - if len(resp) == 1 { - return &resp[0], nil + params := url.Values{} + params.Set("uniqueCode", uniqueCode) + params.Set("lastDays", lastDays) + if instrumentType != "" { + params.Set("instType", instrumentType) } - return nil, errNoValidResponseFromServer + var resp []LeadTraderCurrencyPreference + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderCurrencyPreferencesEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-preference-currency", params), nil, &resp, request.UnauthenticatedRequest) } -// AdjustMarginBalance retrieves adjust margin balance with 'add' and 'reduce' balance type -func (ok *Okx) AdjustMarginBalance(ctx context.Context, arg MarginBalanceParam) (*AdjustMarginBalanceResponse, error) { - if arg.AlgoID == "" { - return nil, errInvalidAlgoID +// GetLeadTraderCurrentLeadPositions get current leading positions of lead trader +// Instrument type "SPOT" "SWAP" +func (ok *Okx) GetLeadTraderCurrentLeadPositions(ctx context.Context, instrumentType, uniqueCode, afterSubPositionID, + beforeSubPositionID string, limit int64) ([]LeadTraderCurrentLeadPosition, error) { + if uniqueCode == "" { + return nil, errUniqueCodeRequired } - if arg.Type != "add" && arg.Type != "reduce" { - return nil, errInvalidMarginTypeAdjust + params := url.Values{} + params.Set("uniqueCode", uniqueCode) + if instrumentType != "" { + params.Set("instType", instrumentType) } - if arg.Percentage <= 0 && arg.Amount < 0 { - return nil, errors.New("either percentage or amount is required") + if afterSubPositionID != "" { + params.Set("after", afterSubPositionID) } - var resp []AdjustMarginBalanceResponse - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, adjustMarginBalanceEPL, http.MethodPost, gridMarginBalance, &arg, &resp, true); err != nil { - return nil, err + if beforeSubPositionID != "" { + params.Set("before", beforeSubPositionID) } - if len(resp) == 1 { - return &resp[0], nil + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return nil, errNoValidResponseFromServer + var resp []LeadTraderCurrentLeadPosition + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTraderCurrentLeadPositionsEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-current-subpositions", params), nil, &resp, request.UnauthenticatedRequest) } -// GetGridAIParameter retrieves grid AI parameter -func (ok *Okx) GetGridAIParameter(ctx context.Context, algoOrderType, instrumentID, direction, duration string) ([]GridAIParameterResponse, error) { +// GetLeadTraderLeadPositionHistory retrieve the lead trader completed leading position of the last 3 months. Returns reverse chronological order with subPosId +func (ok *Okx) GetLeadTraderLeadPositionHistory(ctx context.Context, instrumentType, uniqueCode, afterSubPositionID, beforeSubPositionID string, limit int64) ([]LeadPosition, error) { + if uniqueCode == "" { + return nil, errUniqueCodeRequired + } params := url.Values{} - if algoOrderType != "moon_grid" && algoOrderType != "contract_grid" && algoOrderType != "grid" { - return nil, errInvalidAlgoOrderType + params.Set("uniqueCode", uniqueCode) + if instrumentType != "" { + params.Set("instType", instrumentType) } - if instrumentID == "" { - return nil, errMissingInstrumentID + if afterSubPositionID != "" { + params.Set("after", afterSubPositionID) } - if algoOrderType == "contract_grid" && direction != positionSideLong && direction != positionSideShort && direction != "neutral" { - return nil, errors.New("parameter 'direction' is required for 'contract_grid' algo order type") + if beforeSubPositionID != "" { + params.Set("before", beforeSubPositionID) } - params.Set("direction", direction) - params.Set("algoOrdType", algoOrderType) - params.Set("instId", instrumentID) - if duration != "" && duration != "7D" && duration != "30D" && duration != "180D" { - return nil, errInvalidDuration + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - var resp []GridAIParameterResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getGridAIParameterEPL, http.MethodGet, common.EncodeURLValues(gridAIParams, params), nil, &resp, false) + var resp []LeadPosition + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderLeadPositionHistoryEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-subpositions-history", params), nil, &resp, request.UnauthenticatedRequest) } // ****************************************** Earn ************************************************** -// GetOffers retrieves list of offers for different protocols. -func (ok *Okx) GetOffers(ctx context.Context, productID, protocolType, currency string) ([]Offer, error) { +// GetOffers retrieves list of offers for different protocols +func (ok *Okx) GetOffers(ctx context.Context, productID, protocolType string, ccy currency.Code) ([]Offer, error) { params := url.Values{} if productID != "" { params.Set("productId", productID) @@ -2871,75 +4035,66 @@ func (ok *Okx) GetOffers(ctx context.Context, productID, protocolType, currency if protocolType != "" { params.Set("protocolType", protocolType) } - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } var resp []Offer - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOfferEPL, http.MethodGet, common.EncodeURLValues(financeOffers, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOfferEPL, http.MethodGet, common.EncodeURLValues("finance/staking-defi/offers", params), nil, &resp, request.AuthenticatedRequest) } // Purchase invest on specific product -func (ok *Okx) Purchase(ctx context.Context, arg PurchaseRequestParam) (*OrderIDResponse, error) { +func (ok *Okx) Purchase(ctx context.Context, arg *PurchaseRequestParam) (*OrderIDResponse, error) { + if arg == nil { + return nil, common.ErrNilPointer + } if arg.ProductID == "" { return nil, fmt.Errorf("%w, missing product id", errMissingRequiredParameter) } for x := range arg.InvestData { - if arg.InvestData[x].Currency == "" { - return nil, fmt.Errorf("%w, currency information for investment is required", errMissingRequiredParameter) + if arg.InvestData[x].Currency.IsEmpty() { + return nil, fmt.Errorf("%w, currency information for investment is required", currency.ErrCurrencyCodeEmpty) } if arg.InvestData[x].Amount <= 0 { - return nil, errUnacceptableAmount + return nil, order.ErrAmountBelowMin } } - var resp []OrderIDResponse - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, purchaseEPL, http.MethodPost, financePurchase, &arg, &resp, true); err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, purchaseEPL, http.MethodPost, "finance/staking-defi/purchase", &arg, &resp, request.AuthenticatedRequest) } // Redeem redemption of investment -func (ok *Okx) Redeem(ctx context.Context, arg RedeemRequestParam) (*OrderIDResponse, error) { +func (ok *Okx) Redeem(ctx context.Context, arg *RedeemRequestParam) (*OrderIDResponse, error) { + if *arg == (RedeemRequestParam{}) { + return nil, common.ErrEmptyParams + } if arg.OrderID == "" { - return nil, fmt.Errorf("%w, missing 'orderId'", errMissingRequiredParameter) + return nil, fmt.Errorf("%w, missing 'orderId'", order.ErrOrderIDNotSet) } if arg.ProtocolType != "staking" && arg.ProtocolType != "defi" { return nil, errInvalidProtocolType } - var resp []OrderIDResponse - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, redeemEPL, http.MethodPost, financeRedeem, &arg, &resp, true); err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, redeemEPL, http.MethodPost, "finance/staking-defi/redeem", &arg, &resp, request.AuthenticatedRequest) } // CancelPurchaseOrRedemption cancels Purchases or Redemptions -// after cancelling, returning funds will go to the funding account. -func (ok *Okx) CancelPurchaseOrRedemption(ctx context.Context, arg CancelFundingParam) (*OrderIDResponse, error) { +// after cancelling, returning funds will go to the funding account +func (ok *Okx) CancelPurchaseOrRedemption(ctx context.Context, arg *CancelFundingParam) (*OrderIDResponse, error) { + if *arg == (CancelFundingParam{}) { + return nil, common.ErrEmptyParams + } if arg.OrderID == "" { - return nil, fmt.Errorf("%w, missing 'orderId'", errMissingRequiredParameter) + return nil, fmt.Errorf("%w, missing 'orderId'", order.ErrOrderIDNotSet) } if arg.ProtocolType != "staking" && arg.ProtocolType != "defi" { return nil, errInvalidProtocolType } - var resp []OrderIDResponse - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelPurchaseOrRedemptionEPL, http.MethodPost, financeCancelPurchase, &arg, &resp, true); err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *OrderIDResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelPurchaseOrRedemptionEPL, http.MethodPost, "finance/staking-defi/cancel", &arg, &resp, request.AuthenticatedRequest) } -// GetEarnActiveOrders retrieves active orders. -func (ok *Okx) GetEarnActiveOrders(ctx context.Context, productID, protocolType, currency, state string) ([]ActiveFundingOrder, error) { +// GetEarnActiveOrders retrieves active orders +func (ok *Okx) GetEarnActiveOrders(ctx context.Context, productID, protocolType, state string, ccy currency.Code) ([]ActiveFundingOrder, error) { params := url.Values{} if productID != "" { params.Set("productId", productID) @@ -2952,19 +4107,19 @@ func (ok *Okx) GetEarnActiveOrders(ctx context.Context, productID, protocolType, } params.Set("protocolType", protocolType) } - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if state != "" { params.Set("state", state) } var resp []ActiveFundingOrder - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEarnActiveOrdersEPL, http.MethodGet, common.EncodeURLValues(financeActiveOrders, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEarnActiveOrdersEPL, http.MethodGet, common.EncodeURLValues("finance/staking-defi/orders-active", params), nil, &resp, request.AuthenticatedRequest) } // GetFundingOrderHistory retrieves funding order history // valid protocol types are 'staking' and 'defi' -func (ok *Okx) GetFundingOrderHistory(ctx context.Context, productID, protocolType, currency string, after, before time.Time, limit int64) ([]ActiveFundingOrder, error) { +func (ok *Okx) GetFundingOrderHistory(ctx context.Context, productID, protocolType string, ccy currency.Code, after, before time.Time, limit int64) ([]ActiveFundingOrder, error) { params := url.Values{} if productID != "" { params.Set("productId", productID) @@ -2972,8 +4127,8 @@ func (ok *Okx) GetFundingOrderHistory(ctx context.Context, productID, protocolTy if protocolType != "" { params.Set("protocolType", protocolType) } - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if !before.IsZero() { params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) @@ -2985,69 +4140,153 @@ func (ok *Okx) GetFundingOrderHistory(ctx context.Context, productID, protocolTy params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []ActiveFundingOrder - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundingOrderHistoryEPL, http.MethodGet, common.EncodeURLValues(financeOrdersHistory, params), nil, &resp, true) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundingOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("finance/staking-defi/orders-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// **************************************************************** ETH Staking **************************************************************** + +// GetProductInfo retrieves ETH staking products +func (ok *Okx) GetProductInfo(ctx context.Context) (*ProductInfo, error) { + var resp *ProductInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getProductInfoEPL, http.MethodGet, + "finance/staking-defi/eth/product-info", nil, &resp, request.AuthenticatedRequest) +} + +// PurchaseETHStaking staking ETH for BETH +// Only the assets in the funding account can be used +func (ok *Okx) PurchaseETHStaking(ctx context.Context, amount float64) error { + if amount <= 0 { + return order.ErrAmountBelowMin + } + var resp []string + return ok.SendHTTPRequest(ctx, exchange.RestSpot, purchaseETHStakingEPL, http.MethodPost, "finance/staking-defi/eth/purchase", map[string]string{"amt": strconv.FormatFloat(amount, 'f', -1, 64)}, &resp, request.AuthenticatedRequest) +} + +// RedeemETHStaking only the assets in the funding account can be used. If your BETH is in your trading account, you can make funding transfer first +func (ok *Okx) RedeemETHStaking(ctx context.Context, amount float64) error { + if amount <= 0 { + return order.ErrAmountBelowMin + } + var resp []string + return ok.SendHTTPRequest(ctx, exchange.RestSpot, redeemETHStakingEPL, http.MethodPost, "finance/staking-defi/eth/redeem", + map[string]string{"amt": strconv.FormatFloat(amount, 'f', -1, 64)}, &resp, request.AuthenticatedRequest) +} + +// GetBETHAssetsBalance balance is a snapshot summarized all BETH assets in trading and funding accounts. Also, the snapshot updates hourly +func (ok *Okx) GetBETHAssetsBalance(ctx context.Context) (*BETHAssetsBalance, error) { + var resp *BETHAssetsBalance + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBETHBalanceEPL, http.MethodGet, + "finance/staking-defi/eth/balance", nil, &resp, request.AuthenticatedRequest) } -// GetTickers retrieves the latest price snapshots best bid/ ask price, and trading volume in the last 24 hours. -func (ok *Okx) GetTickers(ctx context.Context, instType, uly, instID string) ([]TickerResponse, error) { +// GetPurchaseAndRedeemHistory retrieves purchase and redeem history +// kind possible values are 'purchase' and 'redeem' +// Status 'pending' 'success' 'failed' +func (ok *Okx) GetPurchaseAndRedeemHistory(ctx context.Context, kind, status string, after, before time.Time, limit int64) ([]PurchaseRedeemHistory, error) { + if kind == "" { + return nil, fmt.Errorf("%w, possible values are 'purchase' and 'redeem'", errLendingTermIsRequired) + } params := url.Values{} - instType = strings.ToUpper(instType) - switch { - case instType != "": - params.Set("instType", instType) - if (instType == okxInstTypeSwap || instType == okxInstTypeFutures || instType == okxInstTypeOption) && uly != "" { - params.Set("uly", uly) - } - case instID != "": - params.Set("instId", instID) - default: - return nil, errors.New("missing required variable instType (instrument type) or instId( Instrument ID )") + params.Set("type", kind) + if status != "" { + params.Set("status", status) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []PurchaseRedeemHistory + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPurchaseRedeemHistoryEPL, http.MethodGet, + common.EncodeURLValues("finance/staking-defi/eth/purchase-redeem-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetAPYHistory retrieves Annual percentage yield(APY) history +func (ok *Okx) GetAPYHistory(ctx context.Context, days int64) ([]APYItem, error) { + if days == 0 || days > 365 { + return nil, errors.New("field days is required; possible values from 1 to 365") + } + var resp []APYItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAPYHistoryEPL, http.MethodGet, fmt.Sprintf("finance/staking-defi/eth/apy-history?days=%d", days), nil, &resp, request.UnauthenticatedRequest) +} + +// GetTickers retrieves the latest price snapshots best bid/ ask price, and trading volume in the last 24 hours +func (ok *Okx) GetTickers(ctx context.Context, instType, uly, instFamily string) ([]TickerResponse, error) { + if instType == "" { + return nil, errInvalidInstrumentType + } + params := url.Values{} + params.Set("instType", instType) + if instFamily != "" { + params.Set("instFamily", instFamily) + } + if uly != "" { + params.Set("uly", uly) } var response []TickerResponse - return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTickersEPL, http.MethodGet, common.EncodeURLValues(marketTickers, params), nil, &response, false) + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTickersEPL, http.MethodGet, common.EncodeURLValues("market/tickers", params), nil, &response, request.UnauthenticatedRequest) } -// GetTicker retrieves the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours. +// GetTicker retrieves the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours func (ok *Okx) GetTicker(ctx context.Context, instrumentID string) (*TickerResponse, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } params := url.Values{} - if instrumentID != "" { - params.Set("instId", instrumentID) - } else { - return nil, errors.New("missing required variable instType(instruction type) or instId( Instrument ID )") + params.Set("instId", instrumentID) + var resp *TickerResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTickerEPL, http.MethodGet, common.EncodeURLValues("market/ticker", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetPremiumHistory returns premium data in the past 6 months +func (ok *Okx) GetPremiumHistory(ctx context.Context, instrumentID string, after, before time.Time, limit int64) ([]PremiumInfo, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID } - var response []TickerResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getTickersEPL, http.MethodGet, common.EncodeURLValues(marketTicker, params), nil, &response, false) - if err != nil { - return nil, err + params := url.Values{} + params.Set("instId", instrumentID) + if !after.IsZero() && !before.IsZero() { + err := common.StartEndTimeCheck(after, before) + if err != nil { + return nil, err + } + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) } - if len(response) == 1 { - return &response[0], nil + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return nil, errNoValidResponseFromServer + var resp []PremiumInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPremiumHistoryEPL, http.MethodGet, common.EncodeURLValues("public/premium-history", params), nil, &resp, request.UnauthenticatedRequest) } -// GetIndexTickers Retrieves index tickers. -func (ok *Okx) GetIndexTickers(ctx context.Context, quoteCurrency, instID string) ([]IndexTicker, error) { - response := []IndexTicker{} - if instID == "" && quoteCurrency == "" { - return nil, errors.New("missing required variable! param quoteCcy or instId has to be set") +// GetIndexTickers Retrieves index tickers +func (ok *Okx) GetIndexTickers(ctx context.Context, quoteCurrency currency.Code, instID string) ([]IndexTicker, error) { + if instID == "" && quoteCurrency.IsEmpty() { + return nil, fmt.Errorf("%w, QuoteCurrency or InstrumentID is required", errEitherInstIDOrCcyIsRequired) } params := url.Values{} - if quoteCurrency != "" { - params.Set("quoteCcy", quoteCurrency) - } else if instID != "" { + if !quoteCurrency.IsEmpty() { + params.Set("quoteCcy", quoteCurrency.String()) + } + if instID != "" { params.Set("instId", instID) } - return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getIndexTickersEPL, http.MethodGet, common.EncodeURLValues(indexTickers, params), nil, &response, false) + var resp []IndexTicker + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getIndexTickersEPL, http.MethodGet, common.EncodeURLValues("market/index-tickers", params), nil, &resp, request.UnauthenticatedRequest) } -// GetInstrumentTypeFromAssetItem returns a string representation of asset.Item; which is an equivalent term for InstrumentType in Okx exchange. -func (ok *Okx) GetInstrumentTypeFromAssetItem(a asset.Item) string { +// GetInstrumentTypeFromAssetItem returns a string representation of asset.Item; which is an equivalent term for InstrumentType in Okx exchange +func GetInstrumentTypeFromAssetItem(a asset.Item) string { switch a { case asset.PerpetualSwap: - return okxInstTypeSwap + return instTypeSwap case asset.Options: - return okxInstTypeOption + return instTypeOption default: return strings.ToUpper(a.String()) } @@ -3055,35 +4294,41 @@ func (ok *Okx) GetInstrumentTypeFromAssetItem(a asset.Item) string { // GetUnderlying returns the instrument ID for the corresponding asset pairs and asset type( Instrument Type ) func (ok *Okx) GetUnderlying(pair currency.Pair, a asset.Item) (string, error) { + if !pair.IsPopulated() { + return "", currency.ErrCurrencyPairsEmpty + } format, err := ok.GetPairFormat(a, false) if err != nil { return "", err } - if !pair.IsPopulated() { - return "", errIncompleteCurrencyPair - } return pair.Base.String() + format.Delimiter + pair.Quote.String(), nil } -// GetOrderBookDepth returns the recent order asks and bids before specified timestamp. -func (ok *Okx) GetOrderBookDepth(ctx context.Context, instrumentID string, depth int64) (*OrderBookResponse, error) { +// GetPairFromInstrumentID returns a currency pair give an instrument ID and asset Item, which represents the instrument type +func (ok *Okx) GetPairFromInstrumentID(instrumentID string) (currency.Pair, error) { + codes := strings.Split(instrumentID, currency.DashDelimiter) + if len(codes) >= 2 { + instrumentID = codes[0] + currency.DashDelimiter + strings.Join(codes[1:], currency.DashDelimiter) + } + return currency.NewPairFromString(instrumentID) +} + +// GetOrderBookDepth returns the recent order asks and bids before specified timestamp +func (ok *Okx) GetOrderBookDepth(ctx context.Context, instrumentID string, depth int64) (*OrderBookResponseDetail, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } params := url.Values{} params.Set("instId", instrumentID) if depth > 0 { params.Set("sz", strconv.FormatInt(depth, 10)) } - var resp []OrderBookResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getOrderBookEPL, http.MethodGet, common.EncodeURLValues(marketBooks, params), nil, &resp, false) - if err != nil { - return nil, err - } else if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *OrderBookResponseDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOrderBookEPL, http.MethodGet, common.EncodeURLValues("market/books", params), nil, &resp, request.UnauthenticatedRequest) } -// GetIntervalEnum allowed interval params by Okx Exchange -func (ok *Okx) GetIntervalEnum(interval kline.Interval, appendUTC bool) string { +// IntervalFromString returns a kline.Interval instance from string +func IntervalFromString(interval kline.Interval, appendUTC bool) string { switch interval { case kline.OneMin: return "1m" @@ -3130,41 +4375,94 @@ func (ok *Okx) GetIntervalEnum(interval kline.Interval, appendUTC bool) string { default: return duration } - if appendUTC { duration += "utc" } - return duration } -// GetCandlesticks Retrieve the candlestick charts. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar. +// GetCandlesticks retrieve the candlestick charts. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar func (ok *Okx) GetCandlesticks(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) { - return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, marketCandles, getCandlestickEPL) + return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/candles", getCandlesticksEPL) } -// GetCandlesticksHistory Retrieve history candlestick charts from recent years. +// GetCandlesticksHistory retrieve history candlestick charts from recent years func (ok *Okx) GetCandlesticksHistory(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) { - return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, marketCandlesHistory, getCandlestickHistoryEPL) + return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/history-candles", getCandlestickHistoryEPL) } -// GetIndexCandlesticks Retrieve the candlestick charts of the index. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar. -// the response is a list of Candlestick data. +// GetIndexCandlesticks retrieve the candlestick charts of the index. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar. +// the response is a list of Candlestick data func (ok *Okx) GetIndexCandlesticks(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) { - return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, marketCandlesIndex, getIndexCandlestickEPL) + return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/index-candles", getIndexCandlesticksEPL) } -// GetMarkPriceCandlesticks Retrieve the candlestick charts of mark price. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar. +// GetMarkPriceCandlesticks retrieve the candlestick charts of mark price. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar func (ok *Okx) GetMarkPriceCandlesticks(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) { - return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, marketPriceCandles, getCandlestickHistoryEPL) + return ok.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/mark-price-candles", getCandlestickHistoryEPL) } -// GetCandlestickData handles fetching the data for both the default GetCandlesticks, GetCandlesticksHistory, and GetIndexCandlesticks() methods. -func (ok *Okx) GetCandlestickData(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64, route string, rateLimit request.EndpointLimit) ([]CandleStick, error) { +// GetHistoricIndexCandlesticksHistory retrieve the candlestick charts of the index from recent years +func (ok *Okx) GetHistoricIndexCandlesticksHistory(ctx context.Context, instrumentID string, after, before time.Time, bar kline.Interval, limit int64) ([]CandlestickHistoryItem, error) { + return ok.getHistoricCandlesticks(ctx, instrumentID, "market/history-index-candles", after, before, bar, limit, getIndexCandlesticksHistoryEPL) +} + +// GetMarkPriceCandlestickHistory retrieve the candlestick charts of the mark price from recent years +func (ok *Okx) GetMarkPriceCandlestickHistory(ctx context.Context, instrumentID string, after, before time.Time, bar kline.Interval, limit int64) ([]CandlestickHistoryItem, error) { + return ok.getHistoricCandlesticks(ctx, instrumentID, "market/history-mark-price-candles", after, before, bar, limit, getMarkPriceCandlesticksHistoryEPL) +} + +func (ok *Okx) getHistoricCandlesticks(ctx context.Context, instrumentID, path string, after, before time.Time, bar kline.Interval, limit int64, epl request.EndpointLimit) ([]CandlestickHistoryItem, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } + params := url.Values{} + params.Set("instId", instrumentID) + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + barString := IntervalFromString(bar, false) + if barString != "" { + params.Set("bar", barString) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []CandlestickHistoryItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodGet, common.EncodeURLValues(path, params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetEconomicCalendarData retrieves the macro-economic calendar data within 3 months. Historical data from 3 months ago is only available to users with trading fee tier VIP1 and above +func (ok *Okx) GetEconomicCalendarData(ctx context.Context, region, importance string, before, after time.Time, limit int64) ([]EconomicCalendar, error) { params := url.Values{} + if region != "" { + params.Set("region", region) + } + if importance != "" { + params.Set("importance", importance) + } + if !before.IsZero() { + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if !after.IsZero() { + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []EconomicCalendar + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEconomicCalendarEPL, http.MethodGet, common.EncodeURLValues("public/economic-calendar", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetCandlestickData handles fetching the data for both the default GetCandlesticks, GetCandlesticksHistory, and GetIndexCandlesticks() methods +func (ok *Okx) GetCandlestickData(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64, route string, rateLimit request.EndpointLimit) ([]CandleStick, error) { if instrumentID == "" { return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) params.Set("limit", strconv.FormatInt(limit, 10)) if !before.IsZero() { @@ -3173,68 +4471,34 @@ func (ok *Okx) GetCandlestickData(ctx context.Context, instrumentID string, inte if !after.IsZero() { params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) } - bar := ok.GetIntervalEnum(interval, true) + bar := IntervalFromString(interval, true) if bar != "" { params.Set("bar", bar) } - var resp [][7]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, false) - if err != nil { - return nil, err - } - klineData := make([]CandleStick, len(resp)) - for x := range resp { - var candleData CandleStick - var err error - timestamp, err := strconv.ParseInt(resp[x][0], 10, 64) - if err != nil { - return nil, err - } - candleData.OpenTime = time.UnixMilli(timestamp) - if candleData.OpenPrice, err = convert.FloatFromString(resp[x][1]); err != nil { - return nil, err - } - if candleData.HighestPrice, err = convert.FloatFromString(resp[x][2]); err != nil { - return nil, err - } - if candleData.LowestPrice, err = convert.FloatFromString(resp[x][3]); err != nil { - return nil, err - } - if candleData.ClosePrice, err = convert.FloatFromString(resp[x][4]); err != nil { - return nil, err - } - if candleData.Volume, err = convert.FloatFromString(resp[x][5]); err != nil { - return nil, err - } - if candleData.QuoteAssetVolume, err = convert.FloatFromString(resp[x][6]); err != nil { - return nil, err - } - klineData[x] = candleData - } - return klineData, nil + var resp []CandleStick + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.UnauthenticatedRequest) } -// GetTrades Retrieve the recent transactions of an instrument. +// GetTrades retrieve the recent transactions of an instrument func (ok *Okx) GetTrades(ctx context.Context, instrumentID string, limit int64) ([]TradeResponse, error) { - var resp []TradeResponse - params := url.Values{} if instrumentID == "" { return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTradesRequestEPL, http.MethodGet, common.EncodeURLValues(marketTrades, params), nil, &resp, false) + var resp []TradeResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTradesRequestEPL, http.MethodGet, common.EncodeURLValues("market/trades", params), nil, &resp, request.UnauthenticatedRequest) } -// GetTradesHistory retrieves the recent transactions of an instrument from the last 3 months with pagination. +// GetTradesHistory retrieves the recent transactions of an instrument from the last 3 months with pagination func (ok *Okx) GetTradesHistory(ctx context.Context, instrumentID, before, after string, limit int64) ([]TradeResponse, error) { - var resp []TradeResponse - params := url.Values{} if instrumentID == "" { return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) if before != "" { params.Set("before", before) @@ -3245,55 +4509,67 @@ func (ok *Okx) GetTradesHistory(ctx context.Context, instrumentID, before, after if limit > 0 { params.Set("limit", strconv.FormatInt(limit, 10)) } - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTradesHistoryEPL, http.MethodGet, common.EncodeURLValues(marketTradesHistory, params), nil, &resp, false) + var resp []TradeResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTradesHistoryEPL, http.MethodGet, common.EncodeURLValues("market/history-trades", params), nil, &resp, request.UnauthenticatedRequest) } -// Get24HTotalVolume The 24-hour trading volume is calculated on a rolling basis, using USD as the pricing unit. -func (ok *Okx) Get24HTotalVolume(ctx context.Context) (*TradingVolumeIn24HR, error) { - var resp []TradingVolumeIn24HR - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, get24HTotalVolumeEPL, http.MethodGet, marketPlatformVolumeIn24Hour, nil, &resp, false) - if err != nil { - return nil, err +// GetOptionTradesByInstrumentFamily retrieve the recent transactions of an instrument under same instFamily. The maximum is 100 +func (ok *Okx) GetOptionTradesByInstrumentFamily(ctx context.Context, instrumentFamily string) ([]InstrumentFamilyTrade, error) { + if instrumentFamily == "" { + return nil, errInstrumentFamilyRequired } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNo24HrTradeVolumeFound + var resp []InstrumentFamilyTrade + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, optionInstrumentTradeFamilyEPL, http.MethodGet, "market/option/instrument-family-trades?instFamily="+instrumentFamily, nil, &resp, request.UnauthenticatedRequest) } -// GetOracle Get the crypto price of signing using Open Oracle smart contract. -func (ok *Okx) GetOracle(ctx context.Context) (*OracleSmartContractResponse, error) { - var resp []OracleSmartContractResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getOracleEPL, http.MethodGet, marketOpenOracles, nil, &resp, false) - if err != nil { - return nil, err +// GetOptionTrades retrieves option trades +// Option type, 'C': Call 'P': put +func (ok *Okx) GetOptionTrades(ctx context.Context, instrumentID, instrumentFamily, optionType string) ([]OptionTrade, error) { + if instrumentID == "" && instrumentFamily == "" { + return nil, errInstrumentIDorFamilyRequired + } + params := url.Values{} + if instrumentID != "" { + params.Set("instId", instrumentID) } - if len(resp) == 1 { - return &resp[0], nil + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) } - return nil, errOracleInformationNotFound + if optionType != "" { + params.Set("optType", optionType) + } + var resp []OptionTrade + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, optionTradesEPL, http.MethodGet, common.EncodeURLValues("public/option-trades", params), nil, &resp, request.UnauthenticatedRequest) +} + +// Get24HTotalVolume The 24-hour trading volume is calculated on a rolling basis, using USD as the pricing unit +func (ok *Okx) Get24HTotalVolume(ctx context.Context) (*TradingVolumeIn24HR, error) { + var resp *TradingVolumeIn24HR + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, get24HTotalVolumeEPL, http.MethodGet, "market/platform-24-volume", nil, &resp, request.UnauthenticatedRequest) +} + +// GetOracle Get the crypto price of signing using Open Oracle smart contract +func (ok *Okx) GetOracle(ctx context.Context) (*OracleSmartContractResponse, error) { + var resp *OracleSmartContractResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOracleEPL, http.MethodGet, "market/open-oracle", nil, &resp, request.UnauthenticatedRequest) } // GetExchangeRate this interface provides the average exchange rate data for 2 weeks // from USD to CNY func (ok *Okx) GetExchangeRate(ctx context.Context) (*UsdCnyExchangeRate, error) { - var resp []UsdCnyExchangeRate - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getExchangeRateRequestEPL, http.MethodGet, marketExchangeRate, nil, &resp, false) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errExchangeInfoNotFound + var resp *UsdCnyExchangeRate + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getExchangeRateRequestEPL, http.MethodGet, "market/exchange-rate", nil, &resp, request.UnauthenticatedRequest) } // GetIndexComponents returns the index component information data on the market func (ok *Okx) GetIndexComponents(ctx context.Context, index string) (*IndexComponent, error) { + if index == "" { + return nil, errIndexComponentNotFound + } params := url.Values{} params.Set("index", index) var resp *IndexComponent - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getIndexComponentsEPL, http.MethodGet, common.EncodeURLValues(marketIndexComponents, params), nil, &resp, false) + err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getIndexComponentsEPL, http.MethodGet, common.EncodeURLValues("market/index-components", params), nil, &resp, request.UnauthenticatedRequest, true) if err != nil { return nil, err } @@ -3304,137 +4580,403 @@ func (ok *Okx) GetIndexComponents(ctx context.Context, index string) (*IndexComp } // GetBlockTickers retrieves the latest block trading volume in the last 24 hours. -// Instrument Type Is Mandatory, and Underlying is Optional. +// Instrument Type Is Mandatory, and Underlying is Optional func (ok *Okx) GetBlockTickers(ctx context.Context, instrumentType, underlying string) ([]BlockTicker, error) { - params := url.Values{} instrumentType = strings.ToUpper(instrumentType) if instrumentType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} params.Set("instType", instrumentType) if underlying != "" { params.Set("uly", underlying) } var resp []BlockTicker - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTickersEPL, http.MethodGet, common.EncodeURLValues(marketBlockTickers, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTickersEPL, http.MethodGet, common.EncodeURLValues("market/block-tickers", params), nil, &resp, request.UnauthenticatedRequest) } -// GetBlockTicker retrieves the latest block trading volume in the last 24 hours. +// GetBlockTicker retrieves the latest block trading volume in the last 24 hours func (ok *Okx) GetBlockTicker(ctx context.Context, instrumentID string) (*BlockTicker, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } params := url.Values{} + params.Set("instId", instrumentID) + var resp *BlockTicker + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTickersEPL, http.MethodGet, common.EncodeURLValues("market/block-ticker", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetPublicBlockTrades retrieves the recent block trading transactions of an instrument. Descending order by tradeId +func (ok *Okx) GetPublicBlockTrades(ctx context.Context, instrumentID string) ([]BlockTrade, error) { if instrumentID == "" { return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) - var resp []BlockTicker - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTickersEPL, http.MethodGet, common.EncodeURLValues(marketBlockTicker, params), nil, &resp, false) + var resp []BlockTrade + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTradesEPL, http.MethodGet, common.EncodeURLValues("public/block-trades", params), nil, &resp, request.UnauthenticatedRequest) +} + +// ********************************************* Spread Trading *********************************************** + +// Spread Trading: As Introduced in the Okx exchange. URL: https://www.okx.com/docs-v5/en/#spread-trading-introduction + +// PlaceSpreadOrder places new spread order +func (ok *Okx) PlaceSpreadOrder(ctx context.Context, arg *SpreadOrderParam) (*SpreadOrderResponse, error) { + err := ok.validatePlaceSpreadOrderParam(arg) if err != nil { return nil, err } - if len(resp) == 1 { - return &resp[0], nil + var resp *SpreadOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeSpreadOrderEPL, http.MethodPost, "sprd/order", arg, &resp, request.AuthenticatedRequest) +} + +func (ok *Okx) validatePlaceSpreadOrderParam(arg *SpreadOrderParam) error { + if *arg == (SpreadOrderParam{}) { + return common.ErrEmptyParams + } + if arg.SpreadID == "" { + return fmt.Errorf("%w, spread ID missing", errMissingInstrumentID) + } + if arg.OrderType == "" { + return fmt.Errorf("%w spread order type is required", order.ErrTypeIsInvalid) + } + if arg.Size <= 0 { + return order.ErrAmountBelowMin + } + if arg.Price <= 0 { + return order.ErrPriceBelowMin + } + arg.Side = strings.ToLower(arg.Side) + switch arg.Side { + case order.Buy.Lower(), order.Sell.Lower(): + default: + return fmt.Errorf("%w %s", order.ErrSideIsInvalid, arg.Side) + } + return nil +} + +// CancelSpreadOrder cancels an incomplete spread order +func (ok *Okx) CancelSpreadOrder(ctx context.Context, orderID, clientOrderID string) (*SpreadOrderResponse, error) { + if orderID == "" && clientOrderID == "" { + return nil, order.ErrOrderIDNotSet + } + arg := make(map[string]string) + if orderID != "" { + arg["ordId"] = orderID + } + if clientOrderID != "" { + arg["clOrdId"] = clientOrderID + } + var resp *SpreadOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelSpreadOrderEPL, http.MethodPost, "sprd/cancel-order", arg, &resp, request.AuthenticatedRequest) +} + +// CancelAllSpreadOrders cancels all spread orders and return success message +// spreadID is optional +// the function returns success status and error message +func (ok *Okx) CancelAllSpreadOrders(ctx context.Context, spreadID string) (bool, error) { + arg := make(map[string]string, 1) + if spreadID != "" { + arg["sprdId"] = spreadID + } + resp := &struct { + Result bool `json:"result"` + }{} + return resp.Result, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllSpreadOrderEPL, http.MethodPost, "sprd/mass-cancel", arg, resp, request.AuthenticatedRequest) +} + +// AmendSpreadOrder amends incomplete spread order +func (ok *Okx) AmendSpreadOrder(ctx context.Context, arg *AmendSpreadOrderParam) (*SpreadOrderResponse, error) { + if *arg == (AmendSpreadOrderParam{}) { + return nil, common.ErrEmptyParams + } + if arg.OrderID == "" && arg.ClientOrderID == "" { + return nil, order.ErrOrderIDNotSet + } + if arg.NewPrice == 0 && arg.NewSize == 0 { + return nil, errSizeOrPriceIsRequired + } + var resp *SpreadOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendSpreadOrderEPL, http.MethodPost, "sprd/amend-order", arg, &resp, request.AuthenticatedRequest) +} + +// GetSpreadOrderDetails retrieves spread order details +func (ok *Okx) GetSpreadOrderDetails(ctx context.Context, orderID, clientOrderID string) (*SpreadOrder, error) { + if orderID == "" && clientOrderID == "" { + return nil, order.ErrOrderIDNotSet + } + params := url.Values{} + if orderID != "" { + params.Set("ordId", orderID) + } + if clientOrderID != "" { + params.Set("clOrdId", clientOrderID) + } + var resp *SpreadOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrderDetailsEPL, http.MethodGet, common.EncodeURLValues("sprd/order", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetActiveSpreadOrders retrieves list of incomplete spread orders +func (ok *Okx) GetActiveSpreadOrders(ctx context.Context, spreadID, orderType, state, beginID, endID string, limit int64) ([]SpreadOrder, error) { + params := url.Values{} + if spreadID != "" { + params.Set("sprdId", spreadID) + } + if orderType != "" { + params.Set("ordType", orderType) + } + if state != "" { + params.Set("state", state) + } + if beginID != "" { + params.Set("beginId", beginID) + } + if endID != "" { + params.Set("endId", endID) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []SpreadOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getActiveSpreadOrdersEPL, http.MethodGet, common.EncodeURLValues("sprd/orders-pending", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetCompletedSpreadOrdersLast7Days retrieve the completed order data for the last 7 days, and the incomplete orders (filledSz =0 & state = canceled) that have been canceled are only reserved for 2 hours. Results are returned in counter chronological order +func (ok *Okx) GetCompletedSpreadOrdersLast7Days(ctx context.Context, spreadID, orderType, state, beginID, endID string, begin, end time.Time, limit int64) ([]SpreadOrder, error) { + params := url.Values{} + if spreadID != "" { + params.Set("sprdId", spreadID) + } + if orderType != "" { + params.Set("ordType", orderType) + } + if state != "" { + params.Set("state", state) + } + if beginID != "" { + params.Set("beginId", beginID) + } + if endID != "" { + params.Set("endId", endID) + } + if !begin.IsZero() { + params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) + } + if !end.IsZero() { + params.Set("end", strconv.FormatInt(end.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []SpreadOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrders7DaysEPL, http.MethodGet, common.EncodeURLValues("sprd/orders-history", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetSpreadTradesOfLast7Days retrieve historical transaction details for the last 7 days. Results are returned in counter chronological order +func (ok *Okx) GetSpreadTradesOfLast7Days(ctx context.Context, spreadID, tradeID, orderID, beginID, endID string, begin, end time.Time, limit int64) ([]SpreadTrade, error) { + params := url.Values{} + if spreadID != "" { + params.Set("sprdId", spreadID) + } + if tradeID != "" { + params.Set("tradeId", tradeID) + } + if orderID != "" { + params.Set("ordId", orderID) + } + if beginID != "" { + params.Set("beginId", beginID) + } + if endID != "" { + params.Set("endId", endID) + } + if !begin.IsZero() { + params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) + } + if !end.IsZero() { + params.Set("end", strconv.FormatInt(end.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []SpreadTrade + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrderTradesEPL, http.MethodGet, common.EncodeURLValues("sprd/trades", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetPublicSpreads retrieve all available spreads based on the request parameters +func (ok *Okx) GetPublicSpreads(ctx context.Context, baseCurrency, instrumentID, spreadID, state string) ([]SpreadInstrument, error) { + params := url.Values{} + if baseCurrency != "" { + params.Set("baseCcy", baseCurrency) + } + if instrumentID != "" { + params.Set("instId", instrumentID) + } + if spreadID != "" { + params.Set("sprdId", spreadID) + } + if state != "" { + params.Set("state", state) + } + var resp []SpreadInstrument + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadsEPL, http.MethodGet, common.EncodeURLValues("sprd/spreads", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetPublicSpreadOrderBooks retrieve the order book of the spread +func (ok *Okx) GetPublicSpreadOrderBooks(ctx context.Context, spreadID string, orderbookSize int64) ([]SpreadOrderbook, error) { + if spreadID == "" { + return nil, fmt.Errorf("%w, spread ID missing", errMissingInstrumentID) + } + params := url.Values{} + params.Set("sprdId", spreadID) + if orderbookSize != 0 { + params.Set("size", strconv.FormatInt(orderbookSize, 10)) } - return nil, errNoValidResponseFromServer + var resp []SpreadOrderbook + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrderbookEPL, http.MethodGet, common.EncodeURLValues("sprd/books", params), nil, &resp, request.UnauthenticatedRequest) } -// GetBlockTrades retrieves the recent block trading transactions of an instrument. Descending order by tradeId. -func (ok *Okx) GetBlockTrades(ctx context.Context, instrumentID string) ([]BlockTrade, error) { +// GetPublicSpreadTickers retrieve the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours +func (ok *Okx) GetPublicSpreadTickers(ctx context.Context, spreadID string) ([]SpreadTicker, error) { + if spreadID == "" { + return nil, fmt.Errorf("%w, spread ID is required", errMissingInstrumentID) + } params := url.Values{} - if instrumentID == "" { - return nil, errMissingInstrumentID + params.Set("sprdId", spreadID) + var resp []SpreadTicker + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadTickerEPL, http.MethodGet, common.EncodeURLValues("sprd/ticker", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetPublicSpreadTrades retrieve the recent transactions of an instrument (at most 500 records per request). Results are returned in counter chronological order +func (ok *Okx) GetPublicSpreadTrades(ctx context.Context, spreadID string) ([]SpreadPublicTradeItem, error) { + params := url.Values{} + if spreadID != "" { + params.Set("sprdId", spreadID) } - params.Set("instId", instrumentID) - var resp []BlockTrade - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTradesEPL, http.MethodGet, common.EncodeURLValues(publicBlockTrades, params), nil, &resp, false) + var resp []SpreadPublicTradeItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadPublicTradesEPL, http.MethodGet, common.EncodeURLValues("sprd/public-trades", params), nil, &resp, request.UnauthenticatedRequest) +} + +// CancelAllSpreadOrdersAfterCountdown cancel all pending orders after the countdown timeout. Only applicable to spread trading +func (ok *Okx) CancelAllSpreadOrdersAfterCountdown(ctx context.Context, timeoutDuration int64) (*SpreadOrderCancellationResponse, error) { + if (timeoutDuration != 0) && (timeoutDuration < 10 || timeoutDuration > 120) { + return nil, fmt.Errorf("%w, range of value can be 0, [10, 120]", errCountdownTimeoutRequired) + } + arg := &struct { + TimeOut int64 `json:"timeOut,string"` + }{ + TimeOut: timeoutDuration, + } + var resp *SpreadOrderCancellationResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllSpreadOrdersAfterEPL, http.MethodPost, "sprd/cancel-all-after", arg, &resp, request.AuthenticatedRequest) } -/************************************ Public Data Endpoinst *************************************************/ +/************************************ Public Data Endpoints *************************************************/ -// GetInstruments Retrieve a list of instruments with open contracts. +// GetInstruments retrieve a list of instruments with open contracts func (ok *Okx) GetInstruments(ctx context.Context, arg *InstrumentsFetchParams) ([]Instrument, error) { - params := url.Values{} - arg.InstrumentType = strings.ToUpper(arg.InstrumentType) if arg.InstrumentType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) + } + if arg.InstrumentType == instTypeOption && + arg.InstrumentFamily == "" && arg.Underlying == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired } + params := url.Values{} + arg.InstrumentType = strings.ToUpper(arg.InstrumentType) params.Set("instType", arg.InstrumentType) if arg.Underlying != "" { params.Set("uly", arg.Underlying) } + if arg.InstrumentFamily != "" { + params.Set("instFamily", arg.InstrumentFamily) + } if arg.InstrumentID != "" { params.Set("instId", arg.InstrumentID) } var resp []Instrument - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInstrumentsEPL, http.MethodGet, common.EncodeURLValues(publicInstruments, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInstrumentsEPL, http.MethodGet, + common.EncodeURLValues("public/instruments", params), nil, &resp, request.UnauthenticatedRequest) } -// GetDeliveryHistory retrieves the estimated delivery price of the last 3 months, which will only have a return value one hour before the delivery/exercise. -func (ok *Okx) GetDeliveryHistory(ctx context.Context, instrumentType, underlying string, after, before time.Time, limit int64) ([]DeliveryHistory, error) { +// GetDeliveryHistory retrieves the estimated delivery price of the last 3 months, which will only have a return value one hour before the delivery/exercise +func (ok *Okx) GetDeliveryHistory(ctx context.Context, instrumentType, underlying, instrumentFamily string, after, before time.Time, limit int64) ([]DeliveryHistory, error) { + if instrumentType == "" { + return nil, errInvalidInstrumentType + } + switch instrumentType { + case instTypeFutures, instTypeOption: + if underlying == "" && instrumentFamily == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired + } + } + if limit > 100 { + return nil, errLimitValueExceedsMaxOf100 + } params := url.Values{} - instrumentType = strings.ToUpper(instrumentType) - if instrumentType != "" { - params.Set("instType", instrumentType) + params.Set("instType", instrumentType) + if underlying != "" { + params.Set("uly", underlying) } - if underlying == "" { - return nil, errMissingRequiredUnderlying + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) } - params.Set("uly", underlying) if !after.IsZero() { params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) } if !before.IsZero() { params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) } - if limit <= 0 || limit > 100 { - return nil, errLimitValueExceedsMaxOf100 + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - params.Set("limit", strconv.FormatInt(limit, 10)) var resp []DeliveryHistory - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDeliveryExerciseHistoryEPL, http.MethodGet, common.EncodeURLValues(publicDeliveryExerciseHistory, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDeliveryExerciseHistoryEPL, http.MethodGet, + common.EncodeURLValues("public/delivery-exercise-history", params), nil, &resp, request.UnauthenticatedRequest) } // GetOpenInterestData retrieves the total open interest for contracts on OKX -func (ok *Okx) GetOpenInterestData(ctx context.Context, instType, uly, instID string) ([]OpenInterest, error) { - params := url.Values{} - instType = strings.ToUpper(instType) +func (ok *Okx) GetOpenInterestData(ctx context.Context, instType, uly, instrumentFamily, instID string) ([]OpenInterest, error) { if instType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) + } + if instType == instTypeOption && uly == "" && instrumentFamily == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired } + params := url.Values{} + instType = strings.ToUpper(instType) params.Set("instType", instType) if uly != "" { params.Set("uly", uly) } + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } if instID != "" { params.Set("instId", instID) } var resp []OpenInterest - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestEPL, http.MethodGet, common.EncodeURLValues(publicOpenInterestValues, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestEPL, http.MethodGet, common.EncodeURLValues("public/open-interest", params), nil, &resp, request.UnauthenticatedRequest) } // GetSingleFundingRate returns the latest funding rate func (ok *Okx) GetSingleFundingRate(ctx context.Context, instrumentID string) (*FundingRateResponse, error) { - params := url.Values{} if instrumentID == "" { return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) - var resp []FundingRateResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundingEPL, http.MethodGet, common.EncodeURLValues(publicFundingRate, params), nil, &resp, false) - if err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errNoValidResponseFromServer + var resp *FundingRateResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundingEPL, http.MethodGet, common.EncodeURLValues("public/funding-rate", params), nil, &resp, request.UnauthenticatedRequest) } -// GetFundingRateHistory retrieves funding rate history. This endpoint can retrieve data from the last 3 months. +// GetFundingRateHistory retrieves funding rate history. This endpoint can retrieve data from the last 3 months func (ok *Okx) GetFundingRateHistory(ctx context.Context, instrumentID string, before, after time.Time, limit int64) ([]FundingRateResponse, error) { - params := url.Values{} if instrumentID == "" { return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) if !before.IsZero() { params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) @@ -3446,100 +4988,93 @@ func (ok *Okx) GetFundingRateHistory(ctx context.Context, instrumentID string, b params.Set("limit", strconv.FormatInt(limit, 10)) } var resp []FundingRateResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundingRateHistoryEPL, http.MethodGet, common.EncodeURLValues(publicFundingRateHistory, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFundingRateHistoryEPL, http.MethodGet, common.EncodeURLValues("public/funding-rate-history", params), nil, &resp, request.UnauthenticatedRequest) } -// GetLimitPrice retrieves the highest buy limit and lowest sell limit of the instrument. +// GetLimitPrice retrieves the highest buy limit and lowest sell limit of the instrument func (ok *Okx) GetLimitPrice(ctx context.Context, instrumentID string) (*LimitPriceResponse, error) { - params := url.Values{} if instrumentID == "" { return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) - var resp []LimitPriceResponse - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getLimitPriceEPL, http.MethodGet, common.EncodeURLValues(publicLimitPath, params), nil, &resp, false); err != nil { - return nil, err - } - if len(resp) == 1 { - return &resp[0], nil - } - return nil, errFundingRateHistoryNotFound + var resp *LimitPriceResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLimitPriceEPL, http.MethodGet, common.EncodeURLValues("public/price-limit", params), nil, &resp, request.UnauthenticatedRequest) } -// GetOptionMarketData retrieves option market data. -func (ok *Okx) GetOptionMarketData(ctx context.Context, underlying string, expTime time.Time) ([]OptionMarketDataResponse, error) { +// GetOptionMarketData retrieves option market data +func (ok *Okx) GetOptionMarketData(ctx context.Context, underlying, instrumentFamily string, expTime time.Time) ([]OptionMarketDataResponse, error) { + if underlying == "" && instrumentFamily == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired + } params := url.Values{} - if underlying == "" { - return nil, errMissingRequiredUnderlying + if underlying != "" { + params.Set("uly", underlying) + } + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) } - params.Set("uly", underlying) if !expTime.IsZero() { params.Set("expTime", fmt.Sprintf("%d%d%d", expTime.Year(), expTime.Month(), expTime.Day())) } var resp []OptionMarketDataResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOptionMarketDateEPL, http.MethodGet, common.EncodeURLValues(publicOptionalData, params), nil, &resp, false) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOptionMarketDateEPL, http.MethodGet, common.EncodeURLValues("public/opt-summary", params), nil, &resp, request.UnauthenticatedRequest) } -// GetEstimatedDeliveryPrice retrieves the estimated delivery price which will only have a return value one hour before the delivery/exercise. +// GetEstimatedDeliveryPrice retrieves the estimated delivery price which will only have a return value one hour before the delivery/exercise func (ok *Okx) GetEstimatedDeliveryPrice(ctx context.Context, instrumentID string) ([]DeliveryEstimatedPrice, error) { - var resp []DeliveryEstimatedPrice - params := url.Values{} if instrumentID == "" { - return nil, errMissingRequiredParamInstID + return nil, errMissingInstrumentID } + params := url.Values{} params.Set("instId", instrumentID) - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEstimatedDeliveryExercisePriceEPL, http.MethodGet, common.EncodeURLValues(publicEstimatedPrice, params), nil, &resp, false) + var resp []DeliveryEstimatedPrice + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEstimatedDeliveryPriceEPL, http.MethodGet, common.EncodeURLValues("public/estimated-price", params), nil, &resp, request.UnauthenticatedRequest) } -// GetDiscountRateAndInterestFreeQuota retrieves discount rate level and interest-free quota. -func (ok *Okx) GetDiscountRateAndInterestFreeQuota(ctx context.Context, currency string, discountLevel int8) ([]DiscountRate, error) { - var response []DiscountRate +// GetDiscountRateAndInterestFreeQuota retrieves discount rate level and interest-free quota +func (ok *Okx) GetDiscountRateAndInterestFreeQuota(ctx context.Context, ccy currency.Code, discountLevel int8) ([]DiscountRate, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if discountLevel > 0 { - params.Set("discountLv", strconv.Itoa(int(discountLevel))) + params.Set("discountLv", strconv.FormatInt(int64(discountLevel), 10)) } - return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDiscountRateAndInterestFreeQuotaEPL, http.MethodGet, common.EncodeURLValues(publicDiscountRate, params), nil, &response, false) + var response []DiscountRate + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDiscountRateAndInterestFreeQuotaEPL, http.MethodGet, common.EncodeURLValues("public/discount-rate-interest-free-quota", params), nil, &response, request.UnauthenticatedRequest) } -// GetSystemTime Retrieve API server time. -func (ok *Okx) GetSystemTime(ctx context.Context) (time.Time, error) { - var resp []ServerTime - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getSystemTimeEPL, http.MethodGet, publicTime, nil, &resp, false); err != nil { - return time.Time{}, err - } - if len(resp) == 1 { - return resp[0].Timestamp.Time(), nil - } - return time.Time{}, errNoValidResponseFromServer +// GetSystemTime retrieve API server time +func (ok *Okx) GetSystemTime(ctx context.Context) (types.Time, error) { + resp := &ServerTime{} + return resp.Timestamp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSystemTimeEPL, http.MethodGet, "public/time", nil, &resp, request.UnauthenticatedRequest) } -// GetLiquidationOrders retrieves information on liquidation orders in the last day. +// GetLiquidationOrders retrieves information on liquidation orders in the last day func (ok *Okx) GetLiquidationOrders(ctx context.Context, arg *LiquidationOrderRequestParams) (*LiquidationOrder, error) { - params := url.Values{} arg.InstrumentType = strings.ToUpper(arg.InstrumentType) if arg.InstrumentType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} params.Set("instType", arg.InstrumentType) arg.MarginMode = strings.ToLower(arg.MarginMode) if arg.MarginMode != "" { params.Set("mgnMode", arg.MarginMode) } switch { - case arg.InstrumentType == okxInstTypeMargin && arg.InstrumentID != "": + case arg.InstrumentType == instTypeMargin && arg.InstrumentID != "": params.Set("instId", arg.InstrumentID) - case arg.InstrumentType == okxInstTypeMargin && arg.Currency.String() != "": + case arg.InstrumentType == instTypeMargin && arg.Currency.String() != "": params.Set("ccy", arg.Currency.String()) default: return nil, errEitherInstIDOrCcyIsRequired } - if arg.InstrumentType != okxInstTypeMargin && arg.Underlying != "" { + if arg.InstrumentType != instTypeMargin && arg.Underlying != "" { params.Set("uly", arg.Underlying) } - if arg.InstrumentType == okxInstTypeFutures && arg.Alias != "" { + if arg.InstrumentType == instTypeFutures && arg.Alias != "" { params.Set("alias", arg.Alias) } if !arg.Before.IsZero() { @@ -3551,119 +5086,129 @@ func (ok *Okx) GetLiquidationOrders(ctx context.Context, arg *LiquidationOrderRe if arg.Limit > 0 && arg.Limit < 100 { params.Set("limit", strconv.FormatInt(arg.Limit, 10)) } - var response []LiquidationOrder - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getLiquidationOrdersEPL, http.MethodGet, common.EncodeURLValues(publicLiquidationOrders, params), nil, &response, false) - if err != nil { - return nil, err - } - if len(response) == 1 { - return &response[0], nil - } - return nil, errLiquidationOrderResponseNotFound + var resp *LiquidationOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLiquidationOrdersEPL, http.MethodGet, common.EncodeURLValues("public/liquidation-orders", params), nil, &resp, request.UnauthenticatedRequest) } -// GetMarkPrice Retrieve mark price. -func (ok *Okx) GetMarkPrice(ctx context.Context, instrumentType, underlying, instrumentID string) ([]MarkPrice, error) { - params := url.Values{} +// GetMarkPrice retrieve mark price +func (ok *Okx) GetMarkPrice(ctx context.Context, instrumentType, underlying, instrumentFamily, instrumentID string) ([]MarkPrice, error) { if instrumentType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} params.Set("instType", strings.ToUpper(instrumentType)) if underlying != "" { params.Set("uly", underlying) } + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } if instrumentID != "" { params.Set("instId", instrumentID) } var response []MarkPrice - return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMarkPriceEPL, http.MethodGet, common.EncodeURLValues(publicMarkPrice, params), nil, &response, false) + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMarkPriceEPL, http.MethodGet, common.EncodeURLValues("public/mark-price", params), nil, &response, request.UnauthenticatedRequest) } -// GetPositionTiers retrieves position tiers information,maximum leverage depends on your borrowings and margin ratio. -func (ok *Okx) GetPositionTiers(ctx context.Context, instrumentType, tradeMode, underlying, instrumentID, tiers string) ([]PositionTiers, error) { - params := url.Values{} +// GetPositionTiers retrieves position tiers information,maximum leverage depends on your borrowings and margin ratio +func (ok *Okx) GetPositionTiers(ctx context.Context, instrumentType, tradeMode, underlying, instrumentFamily, instrumentID, tiers string, ccy currency.Code) ([]PositionTiers, error) { instrumentType = strings.ToUpper(instrumentType) if instrumentType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } - params.Set("instType", strings.ToUpper(instrumentType)) tradeMode = strings.ToLower(tradeMode) - if tradeMode != TradeModeCross && tradeMode != TradeModeIsolated { - return nil, errIncorrectRequiredParameterTradeMode + switch tradeMode { + case TradeModeCross, TradeModeIsolated: + default: + return nil, errInvalidTradeMode } + params := url.Values{} + params.Set("instType", strings.ToUpper(instrumentType)) params.Set("tdMode", tradeMode) if underlying != "" { params.Set("uly", underlying) } - if instrumentType == okxInstTypeMargin && instrumentID != "" { + + switch instrumentType { + case instTypeSwap, instTypeFutures, instTypeOption: + if instrumentFamily == "" && underlying == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired + } + if ccy.IsEmpty() && instrumentID == "" { + return nil, errEitherInstIDOrCcyIsRequired + } + } + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } + if underlying != "" { + params.Set("uly", underlying) + } + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if instrumentID != "" { params.Set("instId", instrumentID) - } else if instrumentType == okxInstTypeMargin { - return nil, errMissingInstrumentID } if tiers != "" { params.Set("tiers", tiers) } var response []PositionTiers - return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPositionTiersEPL, http.MethodGet, common.EncodeURLValues(publicPositionTiers, params), nil, &response, false) + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPositionTiersEPL, http.MethodGet, common.EncodeURLValues("public/position-tiers", params), nil, &response, request.UnauthenticatedRequest) } -// GetInterestRateAndLoanQuota retrieves an interest rate and loan quota information for various currencies. -func (ok *Okx) GetInterestRateAndLoanQuota(ctx context.Context) (map[string][]InterestRateLoanQuotaItem, error) { - var response []map[string][]InterestRateLoanQuotaItem - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateAndLoanQuotaEPL, http.MethodGet, publicInterestRateAndLoanQuota, nil, &response, false) - if err != nil { - return nil, err - } else if len(response) == 1 { - return response[0], nil - } - return nil, errInterestRateAndLoanQuotaNotFound +// GetInterestRateAndLoanQuota retrieves an interest rate and loan quota information for various currencies +func (ok *Okx) GetInterestRateAndLoanQuota(ctx context.Context) ([]InterestRateLoanQuotaItem, error) { + var resp []InterestRateLoanQuotaItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateAndLoanQuotaEPL, http.MethodGet, "public/interest-rate-loan-quota", nil, &resp, request.UnauthenticatedRequest) } -// GetInterestRateAndLoanQuotaForVIPLoans retrieves an interest rate and loan quota information for VIP users of various currencies. +// GetInterestRateAndLoanQuotaForVIPLoans retrieves an interest rate and loan quota information for VIP users of various currencies func (ok *Okx) GetInterestRateAndLoanQuotaForVIPLoans(ctx context.Context) ([]VIPInterestRateAndLoanQuotaInformation, error) { var response []VIPInterestRateAndLoanQuotaInformation - return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateAndLoanQuoteForVIPLoansEPL, http.MethodGet, publicVIPInterestRateAndLoanQuota, nil, &response, false) + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateAndLoanQuoteForVIPLoansEPL, http.MethodGet, "public/vip-interest-rate-loan-quota", nil, &response, request.UnauthenticatedRequest) } -// GetPublicUnderlyings returns list of underlyings for various instrument types. +// GetPublicUnderlyings returns list of underlyings for various instrument types func (ok *Okx) GetPublicUnderlyings(ctx context.Context, instrumentType string) ([]string, error) { - params := url.Values{} instrumentType = strings.ToUpper(instrumentType) if instrumentType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} params.Set("instType", strings.ToUpper(instrumentType)) - var resp [][]string - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getUnderlyingEPL, http.MethodGet, common.EncodeURLValues(publicUnderlyings, params), nil, &resp, false); err != nil { - return nil, err - } - if len(resp) == 1 { - return resp[0], nil - } - return nil, errUnderlyingsForSpecifiedInstTypeNofFound + var resp []string + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getUnderlyingEPL, http.MethodGet, common.EncodeURLValues("public/underlying", params), nil, &resp, request.UnauthenticatedRequest, false) } -// GetInsuranceFundInformation returns insurance fund balance information. +// GetInsuranceFundInformation returns insurance fund balance information func (ok *Okx) GetInsuranceFundInformation(ctx context.Context, arg *InsuranceFundInformationRequestParams) (*InsuranceFundInformation, error) { - if arg == nil { - return nil, errNilArgument + if *arg == (InsuranceFundInformationRequestParams{}) { + return nil, common.ErrEmptyParams } - params := url.Values{} if arg.InstrumentType == "" { - return nil, errMissingRequiredArgInstType + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} params.Set("instType", strings.ToUpper(arg.InstrumentType)) - arg.Type = strings.ToLower(arg.Type) - if arg.Type != "" { - params.Set("type", arg.Type) + arg.InsuranceType = strings.ToLower(arg.InsuranceType) + if arg.InsuranceType != "" { + params.Set("type", arg.InsuranceType) + } + switch arg.InstrumentType { + case instTypeFutures, instTypeSwap, instTypeOption: + if arg.Underlying == "" && arg.InstrumentFamily == "" { + return nil, errInstrumentFamilyOrUnderlyingRequired + } } if arg.Underlying != "" { params.Set("uly", arg.Underlying) - } else if arg.InstrumentType != okxInstTypeMargin { - return nil, errMissingRequiredUnderlying } - if arg.Currency != "" { - params.Set("ccy", arg.Currency) + if arg.InstrumentFamily != "" { + params.Set("instFamily", arg.InstrumentFamily) + } + if !arg.Currency.IsEmpty() { + params.Set("ccy", arg.Currency.String()) } if !arg.Before.IsZero() { params.Set("before", strconv.FormatInt(arg.Before.UnixMilli(), 10)) @@ -3674,25 +5219,19 @@ func (ok *Okx) GetInsuranceFundInformation(ctx context.Context, arg *InsuranceFu if arg.Limit > 0 { params.Set("limit", strconv.FormatInt(arg.Limit, 10)) } - var response []InsuranceFundInformation - if err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getInsuranceFundEPL, http.MethodGet, common.EncodeURLValues(publicInsuranceFunds, params), nil, &response, false); err != nil { - return nil, err - } - if len(response) == 1 { - return &response[0], nil - } - return nil, errInsuranceFundInformationNotFound + var resp *InsuranceFundInformation + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getInsuranceFundEPL, http.MethodGet, common.EncodeURLValues("public/insurance-fund", params), nil, &resp, request.UnauthenticatedRequest) } -// CurrencyUnitConvert convert currency to contract, or contract to currency. -func (ok *Okx) CurrencyUnitConvert(ctx context.Context, instrumentID string, quantity, orderPrice float64, convertType uint, unitOfCurrency string) (*UnitConvertResponse, error) { - params := url.Values{} +// CurrencyUnitConvert convert currency to contract, or contract to currency +func (ok *Okx) CurrencyUnitConvert(ctx context.Context, instrumentID string, quantity, orderPrice float64, convertType uint, unitOfCcy currency.Code, operationTypeOpen bool) (*UnitConvertResponse, error) { if instrumentID == "" { return nil, errMissingInstrumentID } if quantity <= 0 { return nil, errMissingQuantity } + params := url.Values{} params.Set("instId", instrumentID) params.Set("sz", strconv.FormatFloat(quantity, 'f', 0, 64)) if orderPrice > 0 { @@ -3701,41 +5240,63 @@ func (ok *Okx) CurrencyUnitConvert(ctx context.Context, instrumentID string, qua if convertType > 0 { params.Set("type", strconv.Itoa(int(convertType))) } - if unitOfCurrency != "" { - params.Set("unit", unitOfCurrency) + switch unitOfCcy { + case currency.USDC, currency.USDT: + params.Set("unit", "usds") + default: + params.Set("unit", "coin") } - var resp []UnitConvertResponse - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, unitConvertEPL, http.MethodGet, common.EncodeURLValues(publicCurrencyConvertContract, params), nil, &resp, false) - if err != nil { - return nil, err + // Applicable to FUTURES and SWAP orders + if operationTypeOpen { + params.Set("opType", "open") + } else { + params.Set("opType", "close") + } + var resp *UnitConvertResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, unitConvertEPL, http.MethodGet, + common.EncodeURLValues("public/convert-contract-coin", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetOptionsTickBands retrieves option tick bands information. +// Instrument type OPTION +func (ok *Okx) GetOptionsTickBands(ctx context.Context, instrumentType, instrumentFamily string) ([]OptionTickBand, error) { + if instrumentType == "" { + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } - if len(resp) == 1 { - return &resp[0], nil + params := url.Values{} + params.Set("instType", instrumentType) + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) } - return nil, errNoValidResponseFromServer + var resp []OptionTickBand + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, optionTickBandsEPL, http.MethodGet, + common.EncodeURLValues("public/instrument-tick-bands", params), nil, &resp, request.UnauthenticatedRequest) } // Trading Data Endpoints // GetSupportCoins retrieves the currencies supported by the trading data endpoints func (ok *Okx) GetSupportCoins(ctx context.Context) (*SupportedCoinsData, error) { - var response SupportedCoinsData - return &response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSupportCoinEPL, http.MethodGet, tradingDataSupportedCoins, nil, &response, false) + var response *SupportedCoinsData + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getSupportCoinEPL, http.MethodGet, "rubik/stat/trading-data/support-coin", nil, &response, request.UnauthenticatedRequest, true) } -// GetTakerVolume retrieves the taker volume for both buyers and sellers. -func (ok *Okx) GetTakerVolume(ctx context.Context, currency, instrumentType string, begin, end time.Time, period kline.Interval) ([]TakerVolume, error) { - params := url.Values{} +// GetTakerVolume retrieves the taker volume for both buyers and sellers +func (ok *Okx) GetTakerVolume(ctx context.Context, ccy currency.Code, instrumentType, instrumentFamily string, begin, end time.Time, period kline.Interval) ([]TakerVolume, error) { if instrumentType == "" { - return nil, errInstrumentTypeRequired + return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType) } + params := url.Values{} params.Set("instType", strings.ToUpper(instrumentType)) - interval := ok.GetIntervalEnum(period, false) + if instrumentFamily != "" { + params.Set("instFamily", instrumentFamily) + } + interval := IntervalFromString(period, false) if interval != "" { params.Set("period", interval) } - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if !begin.IsZero() { params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) @@ -3743,40 +5304,35 @@ func (ok *Okx) GetTakerVolume(ctx context.Context, currency, instrumentType stri if !end.IsZero() { params.Set("end", strconv.FormatInt(end.UnixMilli(), 10)) } - var response [][3]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getTakerVolumeEPL, http.MethodGet, common.EncodeURLValues(tradingTakerVolume, params), nil, &response, false) - if err != nil { - return nil, err + var response []TakerVolume + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTakerVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/taker-volume", params), nil, &response, request.UnauthenticatedRequest) +} + +// GetMarginLendingRatio retrieves the ratio of cumulative amount between currency margin quote currency and base currency +func (ok *Okx) GetMarginLendingRatio(ctx context.Context, ccy currency.Code, begin, end time.Time, period kline.Interval) ([]MarginLendRatioItem, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - takerVolumes := []TakerVolume{} - for x := range response { - timestamp, err := strconv.ParseInt(response[x][0], 10, 64) - if err != nil { - return nil, err - } - sellVolume, err := strconv.ParseFloat(response[x][1], 64) - if err != nil { - return nil, err - } - buyVolume, err := strconv.ParseFloat(response[x][2], 64) - if err != nil { - return nil, err - } - takerVolume := TakerVolume{ - Timestamp: time.UnixMilli(timestamp), - SellVolume: sellVolume, - BuyVolume: buyVolume, - } - takerVolumes = append(takerVolumes, takerVolume) + if !begin.IsZero() { + params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) } - return takerVolumes, nil + if !end.IsZero() { + params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10)) + } + interval := IntervalFromString(period, false) + if interval != "" { + params.Set("period", interval) + } + var response []MarginLendRatioItem + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getMarginLendingRatioEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/margin/loan-ratio", params), nil, &response, request.UnauthenticatedRequest) } -// GetMarginLendingRatio retrieves the ratio of cumulative amount between currency margin quote currency and base currency. -func (ok *Okx) GetMarginLendingRatio(ctx context.Context, currency string, begin, end time.Time, period kline.Interval) ([]MarginLendRatioItem, error) { +// GetLongShortRatio retrieves the ratio of users with net long vs net short positions for futures and perpetual swaps +func (ok *Okx) GetLongShortRatio(ctx context.Context, ccy currency.Code, begin, end time.Time, period kline.Interval) ([]LongShortRatio, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } if !begin.IsZero() { params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) @@ -3784,429 +5340,543 @@ func (ok *Okx) GetMarginLendingRatio(ctx context.Context, currency string, begin if !end.IsZero() { params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10)) } - interval := ok.GetIntervalEnum(period, false) + interval := IntervalFromString(period, false) if interval != "" { params.Set("period", interval) } - var response [][2]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getMarginLendingRatioEPL, http.MethodGet, common.EncodeURLValues(tradingMarginLoanRatio, params), nil, &response, false) - if err != nil { - return nil, err + var response []LongShortRatio + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getLongShortRatioEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/contracts/long-short-account-ratio", params), nil, &response, request.UnauthenticatedRequest) +} + +// GetContractsOpenInterestAndVolume retrieves the open interest and trading volume for futures and perpetual swaps +func (ok *Okx) GetContractsOpenInterestAndVolume(ctx context.Context, ccy currency.Code, begin, end time.Time, period kline.Interval) ([]OpenInterestVolume, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if !begin.IsZero() { + params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) + } + if !end.IsZero() { + params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10)) + } + interval := IntervalFromString(period, false) + if interval != "" { + params.Set("period", interval) + } + var response []OpenInterestVolume + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getContractsOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/contracts/open-interest-volume", params), nil, &response, request.UnauthenticatedRequest) +} + +// GetOptionsOpenInterestAndVolume retrieves the open interest and trading volume for options +func (ok *Okx) GetOptionsOpenInterestAndVolume(ctx context.Context, ccy currency.Code, period kline.Interval) ([]OpenInterestVolume, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + interval := IntervalFromString(period, false) + if interval != "" { + params.Set("period", interval) + } + var response []OpenInterestVolume + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOptionsOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume", params), nil, &response, request.UnauthenticatedRequest) +} + +// GetPutCallRatio retrieves the open interest ration and trading volume ratio of calls vs puts +func (ok *Okx) GetPutCallRatio(ctx context.Context, ccy currency.Code, period kline.Interval) ([]OpenInterestVolumeRatio, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + interval := IntervalFromString(period, false) + if interval != "" { + params.Set("period", interval) + } + var response []OpenInterestVolumeRatio + return response, ok.SendHTTPRequest(ctx, exchange.RestSpot, getPutCallRatioEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume-ratio", params), nil, &response, request.UnauthenticatedRequest) +} + +// GetOpenInterestAndVolumeExpiry retrieves the open interest and trading volume of calls and puts for each upcoming expiration +func (ok *Okx) GetOpenInterestAndVolumeExpiry(ctx context.Context, ccy currency.Code, period kline.Interval) ([]ExpiryOpenInterestAndVolume, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + interval := IntervalFromString(period, false) + if interval != "" { + params.Set("period", interval) + } + var resp []ExpiryOpenInterestAndVolume + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume-expiry", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetOpenInterestAndVolumeStrike retrieves the taker volume for both buyers and sellers of calls and puts +func (ok *Okx) GetOpenInterestAndVolumeStrike(ctx context.Context, ccy currency.Code, + expTime time.Time, period kline.Interval) ([]StrikeOpenInterestAndVolume, error) { + if expTime.IsZero() { + return nil, errMissingExpiryTimeParameter + } + params := url.Values{} + params.Set("expTime", expTime.Format("20060102")) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - lendingRatios := []MarginLendRatioItem{} - for x := range response { - timestamp, err := strconv.ParseInt(response[x][0], 10, 64) + interval := IntervalFromString(period, false) + if interval != "" { + params.Set("period", interval) + } + var resp []StrikeOpenInterestAndVolume + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume-strike", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetTakerFlow shows the relative buy/sell volume for calls and puts +// It shows whether traders are bullish or bearish on price and volatility +func (ok *Okx) GetTakerFlow(ctx context.Context, ccy currency.Code, period kline.Interval) (*CurrencyTakerFlow, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + interval := IntervalFromString(period, false) + if interval != "" { + params.Set("period", interval) + } + var resp *CurrencyTakerFlow + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getTakerFlowEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/taker-block-volume", params), nil, &resp, request.UnauthenticatedRequest, true) +} + +// ********************************************************** Affiliate ********************************************************************** + +// The Affiliate API offers affiliate users a flexible function to query the invitee information +// Simply enter the UID of your direct invitee to access their relevant information, empowering your affiliate business growth and day-to-day business operation +// If you have additional data requirements regarding the Affiliate API, please don't hesitate to contact your BD +// We will reach out to you through your BD to provide more comprehensive API support + +// GetInviteesDetail retrieves affiliate invitees details +func (ok *Okx) GetInviteesDetail(ctx context.Context, uid string) (*AffilateInviteesDetail, error) { + if uid == "" { + return nil, errUserIDRequired + } + var resp *AffilateInviteesDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAffilateInviteesDetailEPL, http.MethodGet, "affiliate/invitee/detail?uid="+uid, nil, &resp, request.AuthenticatedRequest) +} + +// GetUserAffiliateRebateInformation this endpoint is used to get the user's affiliate rebate information for affiliate +func (ok *Okx) GetUserAffiliateRebateInformation(ctx context.Context, apiKey string) (*AffilateRebateInfo, error) { + if apiKey == "" { + return nil, errInvalidAPIKey + } + var resp *AffilateRebateInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getUserAffiliateRebateInformationEPL, http.MethodGet, "users/partner/if-rebate?apiKey="+apiKey, nil, &resp, request.AuthenticatedRequest) +} + +// Status + +// SystemStatusResponse retrieves the system status. +// state supports valid values 'scheduled', 'ongoing', 'pre_open', 'completed', and 'canceled' +func (ok *Okx) SystemStatusResponse(ctx context.Context, state string) ([]SystemStatusResponse, error) { + params := url.Values{} + if state != "" { + params.Set("state", state) + } + var resp []SystemStatusResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEventStatusEPL, http.MethodGet, common.EncodeURLValues("system/status", params), nil, &resp, request.UnauthenticatedRequest) +} + +// ------------------------------------------------------- Lending Orders ------------------------------------------------------ + +// PlaceLendingOrder places a lending order +func (ok *Okx) PlaceLendingOrder(ctx context.Context, arg *LendingOrderParam) (*LendingOrderResponse, error) { + if *arg == (LendingOrderParam{}) { + return nil, common.ErrEmptyParams + } + if arg.Currency.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty + } + if arg.Amount <= 0 { + return nil, order.ErrAmountBelowMin + } + if arg.Rate <= 0 { + return nil, errRateRequired + } + if arg.Term == "" { + return nil, errLendingTermIsRequired + } + var resp *LendingOrderResponse + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, placeLendingOrderEPL, http.MethodPost, "finance/fixed-loan/lending-order", arg, &resp, request.AuthenticatedRequest, false) +} + +// AmendLendingOrder amends a lending order +func (ok *Okx) AmendLendingOrder(ctx context.Context, orderID string, changeAmount, rate float64, autoRenewal bool) (string, error) { + if orderID == "" { + return "", order.ErrOrderIDNotSet + } + arg := &struct { + OrderID string `json:"ordId"` + ChangeAmount float64 `json:"changeAmt,omitempty,string"` + Rate float64 `json:"rate,omitempty,string"` + AutoRenewal bool `json:"autoRenewal,omitempty"` + }{ + OrderID: orderID, + ChangeAmount: changeAmount, + Rate: rate, + AutoRenewal: autoRenewal, + } + var resp OrderIDResponse + return resp.OrderID, ok.SendHTTPRequest(ctx, exchange.RestSpot, amendLendingOrderEPL, http.MethodPost, "finance/fixed-loan/amend-lending-order", arg, &resp, request.AuthenticatedRequest) +} + +// Note: the documentation for Amending lending order has similar url, request method, and parameters to the placing order. Therefore, the implementation is skipped for now. + +// GetLendingOrders retrieves list of lending orders. +// State: possible values are 'pending', 'earning', 'expired', 'settled' +func (ok *Okx) GetLendingOrders(ctx context.Context, orderID, state string, ccy currency.Code, startAt, endAt time.Time, limit int64) ([]LendingOrderDetail, error) { + params := url.Values{} + if orderID != "" { + params.Set("ordId", orderID) + } + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) + } + if !startAt.IsZero() && !endAt.IsZero() { + err := common.StartEndTimeCheck(startAt, endAt) if err != nil { return nil, err } - ratio, err := strconv.ParseFloat(response[x][1], 64) + params.Set("after", strconv.FormatInt(startAt.UnixMilli(), 10)) + params.Set("before", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + if state != "" { + params.Set("state", state) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) + } + var resp []LendingOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, lendingOrderListEPL, http.MethodGet, + common.EncodeURLValues("finance/fixed-loan/lending-orders-list", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetLendingSubOrderList retrieves a lending sub-orders list +func (ok *Okx) GetLendingSubOrderList(ctx context.Context, orderID, state string, startAt, endAt time.Time, limit int64) ([]LendingSubOrder, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + params := url.Values{} + params.Set("ordId", orderID) + if state != "" { + params.Set("state", state) + } + if !startAt.IsZero() && !endAt.IsZero() { + err := common.StartEndTimeCheck(startAt, endAt) if err != nil { return nil, err } - lendRatio := MarginLendRatioItem{ - Timestamp: time.UnixMilli(timestamp), - MarginLendRatio: ratio, - } - lendingRatios = append(lendingRatios, lendRatio) + params.Set("after", strconv.FormatInt(startAt.UnixMilli(), 10)) + params.Set("before", strconv.FormatInt(endAt.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return lendingRatios, nil + var resp []LendingSubOrder + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, lendingSubOrderListEPL, http.MethodGet, common.EncodeURLValues("finance/fixed-loan/lending-sub-orders", params), nil, &resp, request.AuthenticatedRequest) } -// GetLongShortRatio retrieves the ratio of users with net long vs net short positions for futures and perpetual swaps. -func (ok *Okx) GetLongShortRatio(ctx context.Context, currency string, begin, end time.Time, period kline.Interval) ([]LongShortRatio, error) { - params := url.Values{} - if currency != "" { - params.Set("ccy", currency) - } - if !begin.IsZero() { - params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) +// Trading Statistics endpoints + +// GetFuturesContractsOpenInterestHistory retrieve the contract open interest statistics of futures and perp. This endpoint returns a maximum of 1440 records +func (ok *Okx) GetFuturesContractsOpenInterestHistory(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]ContractOpenInterestHistoryItem, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID } - if !end.IsZero() { - params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10)) + params := url.Values{} + params.Set("instId", instrumentID) + if period != kline.Interval(0) { + params.Set("period", IntervalFromString(period, true)) } - interval := ok.GetIntervalEnum(period, false) - if interval != "" { - params.Set("period", interval) + if !startAt.IsZero() { + params.Set("begin", strconv.FormatInt(startAt.UnixMilli(), 10)) } - var response [][2]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getLongShortRatioEPL, http.MethodGet, common.EncodeURLValues(longShortAccountRatio, params), nil, &response, false) - if err != nil { - return nil, err + if !endAt.IsZero() { + params.Set("end", strconv.FormatInt(endAt.UnixMilli(), 10)) } - ratios := []LongShortRatio{} - for x := range response { - timestamp, err := strconv.ParseInt(response[x][0], 10, 64) - if err != nil { - return nil, err - } else if timestamp <= 0 { - return nil, fmt.Errorf("%w, expecting non zero timestamp, but found %d", errMalformedData, timestamp) - } - ratio, err := strconv.ParseFloat(response[x][1], 64) - if err != nil { - return nil, err - } - dratio := LongShortRatio{ - Timestamp: time.UnixMilli(timestamp), - MarginLendRatio: ratio, - } - ratios = append(ratios, dratio) + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return ratios, nil + var resp []ContractOpenInterestHistoryItem + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rubikGetContractOpenInterestHistoryEPL, http.MethodGet, + common.EncodeURLValues("rubik/stat/contracts/open-interest-history", params), nil, &resp, request.UnauthenticatedRequest) } -// GetContractsOpenInterestAndVolume retrieves the open interest and trading volume for futures and perpetual swaps. -func (ok *Okx) GetContractsOpenInterestAndVolume(ctx context.Context, currency string, begin, end time.Time, period kline.Interval) ([]OpenInterestVolume, error) { - params := url.Values{} - if currency != "" { - params.Set("ccy", currency) +// GetFuturesContractTakerVolume retrieve the contract taker volume for both buyers and sellers. This endpoint returns a maximum of 1440 records. +// The unit of buy/sell volume, the default is 1. '0': Crypto '1': Contracts '2': U +func (ok *Okx) GetFuturesContractTakerVolume(ctx context.Context, instrumentID string, period kline.Interval, unit, limit int64, startAt, endAt time.Time) ([]ContractTakerVolume, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID } - if !begin.IsZero() { - params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10)) + params := url.Values{} + params.Set("instId", instrumentID) + if period != kline.Interval(0) { + params.Set("period", IntervalFromString(period, true)) } - if !end.IsZero() { - params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10)) + if !startAt.IsZero() { + params.Set("begin", strconv.FormatInt(startAt.UnixMilli(), 10)) } - interval := ok.GetIntervalEnum(period, false) - if interval != "" { - params.Set("period", interval) + if !endAt.IsZero() { + params.Set("end", strconv.FormatInt(endAt.UnixMilli(), 10)) } - openInterestVolumes := []OpenInterestVolume{} - var response [][3]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getContractsOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues(contractOpenInterestVolume, params), nil, &response, false) - if err != nil { - return nil, err + if unit != 1 { + params.Set("unit", strconv.FormatInt(unit, 10)) } - for x := range response { - timestamp, err := strconv.ParseFloat(response[x][0], 64) - if err != nil { - return nil, err - } else if timestamp <= 0 { - return nil, fmt.Errorf("%w, invalid Timestamp value %f", errMalformedData, timestamp) - } - openInterest, err := strconv.ParseFloat(response[x][1], 64) - if err != nil { - return nil, err - } else if openInterest <= 0 { - return nil, fmt.Errorf("%w, OpendInterest has to be non-zero positive value, but found %f", errMalformedData, openInterest) - } - volume, err := strconv.ParseFloat(response[x][2], 64) - if err != nil { - return nil, err - } - openInterestVolume := OpenInterestVolume{ - Timestamp: time.UnixMilli(int64(timestamp)), - Volume: volume, - OpenInterest: openInterest, - } - openInterestVolumes = append(openInterestVolumes, openInterestVolume) + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return openInterestVolumes, nil + var resp []ContractTakerVolume + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rubikContractTakerVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/taker-volume-contract", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetFuturesContractLongShortAccountRatio retrieve the account long/short ratio of a contract. This endpoint returns a maximum of 1440 records +func (ok *Okx) GetFuturesContractLongShortAccountRatio(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) { + return ok.getTopTradersFuturesContractLongShortRatio(ctx, instrumentID, "rubik/stat/contracts/long-short-account-ratio-contract", period, startAt, endAt, limit) } -// GetOptionsOpenInterestAndVolume retrieves the open interest and trading volume for options. -func (ok *Okx) GetOptionsOpenInterestAndVolume(ctx context.Context, currency string, period kline.Interval) ([]OpenInterestVolume, error) { +// GetTopTradersFuturesContractLongShortAccountRatio retrieve the account net long/short ratio of a contract for top traders. +// Top traders refer to the top 5% of traders with the largest open position value. +// This endpoint returns a maximum of 1440 records +func (ok *Okx) GetTopTradersFuturesContractLongShortAccountRatio(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) { + return ok.getTopTradersFuturesContractLongShortRatio(ctx, instrumentID, "rubik/stat/contracts/long-short-account-ratio-contract-top-trader", period, startAt, endAt, limit) +} + +// GetTopTradersFuturesContractLongShortPositionRatio retrieve the position long/short ratio of a contract for top traders. Top traders refer to the top 5% of traders with the largest open position value. This endpoint returns a maximum of 1440 records +func (ok *Okx) GetTopTradersFuturesContractLongShortPositionRatio(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) { + return ok.getTopTradersFuturesContractLongShortRatio(ctx, instrumentID, "rubik/stat/contracts/long-short-position-ratio-contract-top-trader", period, startAt, endAt, limit) +} + +func (ok *Okx) getTopTradersFuturesContractLongShortRatio(ctx context.Context, instrumentID, path string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) { + if instrumentID == "" { + return nil, errMissingInstrumentID + } params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + params.Set("instId", instrumentID) + if period != kline.Interval(0) { + params.Set("period", IntervalFromString(period, true)) } - interval := ok.GetIntervalEnum(period, false) - if interval != "" { - params.Set("period", interval) + if !startAt.IsZero() { + params.Set("begin", strconv.FormatInt(startAt.UnixMilli(), 10)) } - openInterestVolumes := []OpenInterestVolume{} - var response [][3]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getOptionsOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues(optionOpenInterestVolume, params), nil, &response, false) - if err != nil { - return nil, err + if !endAt.IsZero() { + params.Set("end", strconv.FormatInt(endAt.UnixMilli(), 10)) } - for x := range response { - timestamp, err := strconv.ParseInt(response[x][0], 10, 64) - if err != nil { - return nil, errors.New("invalid timestamp information") - } else if timestamp <= 0 { - return nil, fmt.Errorf("%w, expecting non zero timestamp, but found %d", errMalformedData, timestamp) - } - openInterest, err := strconv.ParseFloat(response[x][1], 64) - if err != nil { - return nil, err - } else if openInterest <= 0 { - return nil, fmt.Errorf("%w, OpendInterest has to be non-zero positive value, but found %f", errMalformedData, openInterest) - } - volume, err := strconv.ParseFloat(response[x][2], 64) - if err != nil { - return nil, err - } - openInterestVolume := OpenInterestVolume{ - Timestamp: time.UnixMilli(timestamp), - Volume: volume, - OpenInterest: openInterest, - } - openInterestVolumes = append(openInterestVolumes, openInterestVolume) + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return openInterestVolumes, nil + var resp []TopTraderContractsLongShortRatio + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, rubikTopTradersContractLongShortRatioEPL, http.MethodGet, common.EncodeURLValues(path, params), nil, &resp, request.UnauthenticatedRequest) } -// GetPutCallRatio retrieves the open interest ration and trading volume ratio of calls vs puts. -func (ok *Okx) GetPutCallRatio(ctx context.Context, currency string, - period kline.Interval) ([]OpenInterestVolumeRatio, error) { +// GetAnnouncements get announcements, the response is sorted by pTime with the most recent first. The sort will not be affected if the announcement is updated. Every page has 20 records +// +// There are differences between public endpoint and private endpoint. +// For public endpoint, the response is restricted based on your request IP. +// For private endpoint, the response is restricted based on your country of residence +func (ok *Okx) GetAnnouncements(ctx context.Context, announcementType string, page int64) (*AnnouncementDetail, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if announcementType != "" { + params.Set("annType", announcementType) } - interval := ok.GetIntervalEnum(period, false) - if interval != "" { - params.Set("period", interval) + if page > 0 { + params.Set("page", strconv.FormatInt(page, 10)) } - openInterestVolumeRatios := []OpenInterestVolumeRatio{} - var response [][3]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getPutCallRatioEPL, http.MethodGet, common.EncodeURLValues(optionOpenInterestVolumeRatio, params), nil, &response, false) - if err != nil { - return nil, err + var resp *AnnouncementDetail + if ok.AreCredentialsValid(ctx) { + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAnnouncementsEPL, http.MethodGet, common.EncodeURLValues("support/announcements", params), nil, &resp, request.AuthenticatedRequest) } - for x := range response { - timestamp, err := strconv.ParseInt(response[x][0], 10, 64) - if err != nil { - return nil, err - } - openInterest, err := strconv.ParseFloat(response[x][1], 64) - if err != nil { - return nil, err - } - volume, err := strconv.ParseFloat(response[x][2], 64) - if err != nil { - return nil, err - } - openInterestVolume := OpenInterestVolumeRatio{ - Timestamp: time.UnixMilli(timestamp), - VolumeRatio: volume, - OpenInterestRatio: openInterest, - } - openInterestVolumeRatios = append(openInterestVolumeRatios, openInterestVolume) + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAnnouncementsEPL, http.MethodGet, common.EncodeURLValues("support/announcements", params), nil, &resp, request.UnauthenticatedRequest) +} + +// GetAnnouncementTypes represents a list of announcement types +func (ok *Okx) GetAnnouncementTypes(ctx context.Context) ([]AnnouncementTypeInfo, error) { + var resp []AnnouncementTypeInfo + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getAnnouncementTypeEPL, http.MethodGet, + "support/announcement-types", nil, &resp, request.UnauthenticatedRequest) +} + +// Fiat endpoints + +// GetDepositOrderDetail retrieves fiat deposit order detail +func (ok *Okx) GetDepositOrderDetail(ctx context.Context, orderID string) (*FiatOrderDetail, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet } - return openInterestVolumeRatios, nil + params := url.Values{} + params.Set("ordID", orderID) + var resp *FiatOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDepositOrderDetailEPL, http.MethodGet, common.EncodeURLValues("fiat/deposit", params), nil, &resp, request.AuthenticatedRequest) } -// GetOpenInterestAndVolumeExpiry retrieves the open interest and trading volume of calls and puts for each upcoming expiration. -func (ok *Okx) GetOpenInterestAndVolumeExpiry(ctx context.Context, currency string, period kline.Interval) ([]ExpiryOpenInterestAndVolume, error) { +// GetFiatDepositOrderHistory retrieves fiat deposit order history +func (ok *Okx) GetFiatDepositOrderHistory(ctx context.Context, ccy currency.Code, paymentMethod, state string, after, before time.Time, limit int64) ([]FiatOrderDetail, error) { params := url.Values{} - if currency != "" { - params.Set("ccy", currency) + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - interval := ok.GetIntervalEnum(period, false) - if interval != "" { - params.Set("period", interval) + if paymentMethod != "" { + params.Set("paymentMethod", paymentMethod) } - var resp [][6]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues(optionOpenInterestVolumeExpiry, params), nil, &resp, false) - if err != nil { - return nil, err + if state != "" { + params.Set("state", state) } - volumes := []ExpiryOpenInterestAndVolume{} - for x := range resp { - var timestamp int64 - timestamp, err = strconv.ParseInt(resp[x][0], 10, 64) - if err != nil { - return nil, err - } - var expiryTime time.Time - expTime := resp[x][1] - if expTime != "" && len(expTime) == 8 { - year, err := strconv.ParseInt(expTime[0:4], 10, 64) - if err != nil { - return nil, err - } - month, err := strconv.ParseInt(expTime[4:6], 10, 64) - if err != nil { - return nil, err - } - var months string - var days string - if month <= 9 { - months = "0" + strconv.FormatInt(month, 10) - } else { - months = strconv.FormatInt(month, 10) - } - day, err := strconv.ParseInt(expTime[6:], 10, 64) - if err != nil { - return nil, err - } - if day <= 9 { - days = "0" + strconv.FormatInt(day, 10) - } else { - days = strconv.FormatInt(day, 10) - } - expiryTime, err = time.Parse("2006-01-02", strconv.FormatInt(year, 10)+"-"+months+"-"+days) - if err != nil { - return nil, err - } - } - callOpenInterest, err := strconv.ParseFloat(resp[x][2], 64) - if err != nil { - return nil, err - } - putOpenInterest, err := strconv.ParseFloat(resp[x][3], 64) - if err != nil { - return nil, err - } - callVol, err := strconv.ParseFloat(resp[x][4], 64) - if err != nil { - return nil, err - } - putVol, err := strconv.ParseFloat(resp[x][5], 64) + if !after.IsZero() && !before.IsZero() { + err := common.StartEndTimeCheck(after, before) if err != nil { return nil, err } - volume := ExpiryOpenInterestAndVolume{ - Timestamp: time.UnixMilli(timestamp), - ExpiryTime: expiryTime, - CallOpenInterest: callOpenInterest, - PutOpenInterest: putOpenInterest, - CallVolume: callVol, - PutVolume: putVol, - } - volumes = append(volumes, volume) + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return volumes, nil + var resp []FiatOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getDepositOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("fiat/deposit-order-history", params), nil, &resp, request.AuthenticatedRequest) } -// GetOpenInterestAndVolumeStrike retrieves the taker volume for both buyers and sellers of calls and puts. -func (ok *Okx) GetOpenInterestAndVolumeStrike(ctx context.Context, currency string, - expTime time.Time, period kline.Interval) ([]StrikeOpenInterestAndVolume, error) { - params := url.Values{} - if currency != "" { - params.Set("ccy", currency) +// GetWithdrawalOrderDetail retrieves fiat withdrawal order detail +func (ok *Okx) GetWithdrawalOrderDetail(ctx context.Context, orderID string) (*FiatOrderDetail, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet } - interval := ok.GetIntervalEnum(period, false) - if interval != "" { - params.Set("period", interval) + params := url.Values{} + params.Set("ordId", orderID) + var resp *FiatOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getWithdrawalOrderDetailEPL, http.MethodGet, + common.EncodeURLValues("fiat/withdrawal", params), &map[string]string{"ordId": orderID}, &resp, request.AuthenticatedRequest) +} + +// GetFiatWithdrawalOrderHistory retrieves fiat withdrawal order history +func (ok *Okx) GetFiatWithdrawalOrderHistory(ctx context.Context, ccy currency.Code, paymentMethod, state string, after, before time.Time, limit int64) ([]FiatOrderDetail, error) { + params := url.Values{} + if !ccy.IsEmpty() { + params.Set("ccy", ccy.String()) } - if !expTime.IsZero() { - params.Set("expTime", expTime.Format("20060102")) - } else { - return nil, errMissingExpiryTimeParameter + if paymentMethod != "" { + params.Set("paymentMethod", paymentMethod) } - var resp [][6]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues(optionOpenInterestVolumeStrike, params), nil, &resp, false) - if err != nil { - return nil, err + if state != "" { + params.Set("state", state) } - volumes := []StrikeOpenInterestAndVolume{} - for x := range resp { - timestamp, err := strconv.ParseInt(resp[x][0], 10, 64) - if err != nil { - return nil, err - } - strike, err := strconv.ParseInt(resp[x][1], 10, 64) - if err != nil { - return nil, err - } - calloi, err := strconv.ParseFloat(resp[x][2], 64) - if err != nil { - return nil, err - } - putoi, err := strconv.ParseFloat(resp[x][3], 64) - if err != nil { - return nil, err - } - callvol, err := strconv.ParseFloat(resp[x][4], 64) - if err != nil { - return nil, err - } - putvol, err := strconv.ParseFloat(resp[x][5], 64) + if !after.IsZero() && !before.IsZero() { + err := common.StartEndTimeCheck(after, before) if err != nil { return nil, err } - volume := StrikeOpenInterestAndVolume{ - Timestamp: time.UnixMilli(timestamp), - Strike: strike, - CallOpenInterest: calloi, - PutOpenInterest: putoi, - CallVolume: callvol, - PutVolume: putvol, - } - volumes = append(volumes, volume) + params.Set("after", strconv.FormatInt(after.UnixMilli(), 10)) + params.Set("before", strconv.FormatInt(before.UnixMilli(), 10)) + } + if limit > 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } - return volumes, nil + var resp []FiatOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFiatWithdrawalOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("fiat/withdrawal-order-history", params), nil, &resp, request.AuthenticatedRequest) } -// GetTakerFlow shows the relative buy/sell volume for calls and puts. -// It shows whether traders are bullish or bearish on price and volatility -func (ok *Okx) GetTakerFlow(ctx context.Context, currency string, period kline.Interval) (*CurrencyTakerFlow, error) { - params := url.Values{} - if currency != "" { - params.Set("ccy", currency) +// CancelWithdrawalOrder cancel a pending fiat withdrawal order, currently only applicable to TRY +func (ok *Okx) CancelWithdrawalOrder(ctx context.Context, orderID string) (*OrderIDAndState, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet } - interval := ok.GetIntervalEnum(period, false) - if interval != "" { - params.Set("period", interval) - } - var resp [7]string - err := ok.SendHTTPRequest(ctx, exchange.RestSpot, getTakerFlowEPL, http.MethodGet, common.EncodeURLValues(takerBlockVolume, params), nil, &resp, false) - if err != nil { - return nil, err + var resp *OrderIDAndState + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, cancelWithdrawalOrderEPL, http.MethodPost, "fiat/cancel-withdrawal", &map[string]string{"ordId": orderID}, &resp, request.AuthenticatedRequest) +} + +// CreateWithdrawalOrder initiate a fiat withdrawal request (Authenticated endpoint, Only for API keys with "Withdrawal" access) +func (ok *Okx) CreateWithdrawalOrder(ctx context.Context, ccy currency.Code, paymentAccountID, paymentMethod, clientID string, amount float64) (*FiatOrderDetail, error) { + if paymentAccountID == "" { + return nil, fmt.Errorf("%w, payment account ID is required", errIDNotSet) } - timestamp, err := strconv.ParseInt(resp[0], 10, 64) - if err != nil { - return nil, err + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } - callbuyvol, err := strconv.ParseFloat(resp[1], 64) - if err != nil { - return nil, err + if amount <= 0 { + return nil, order.ErrAmountBelowMin } - callselvol, err := strconv.ParseFloat(resp[2], 64) - if err != nil { - return nil, err + if paymentMethod == "" { + return nil, errPaymentMethodRequired } - putbutvol, err := strconv.ParseFloat(resp[3], 64) - if err != nil { - return nil, err + if clientID == "" { + return nil, fmt.Errorf("%w, client ID is required", errIDNotSet) } - putsellvol, err := strconv.ParseFloat(resp[4], 64) - if err != nil { - return nil, err + arg := &struct { + PaymentMethod string `json:"paymentMethod"` + PaymentAcctID string `json:"paymentAcctId"` + ClientID string `json:"clientId"` + Amount float64 `json:"amt,string"` + Currency string `json:"ccy"` + }{ + PaymentMethod: paymentMethod, + PaymentAcctID: paymentAccountID, + ClientID: clientID, + Amount: amount, + Currency: ccy.String(), } - callblockvol, err := strconv.ParseFloat(resp[5], 64) - if err != nil { - return nil, err + var resp *FiatOrderDetail + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, createWithdrawalOrderEPL, http.MethodPost, "fiat/create-withdrawal", arg, &resp, request.AuthenticatedRequest) +} + +// GetFiatWithdrawalPaymentMethods to display all the available fiat withdrawal payment methods +func (ok *Okx) GetFiatWithdrawalPaymentMethods(ctx context.Context, ccy currency.Code) (*FiatWithdrawalPaymentMethods, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } - putblockvol, err := strconv.ParseFloat(resp[6], 64) - if err != nil { - return nil, err + params := url.Values{} + params.Set("ccy", ccy.String()) + var resp *FiatWithdrawalPaymentMethods + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getWithdrawalPaymentMethodsEPL, http.MethodGet, + common.EncodeURLValues("fiat/withdrawal-payment-methods", params), nil, &resp, request.AuthenticatedRequest) +} + +// GetFiatDepositPaymentMethods to display all the available fiat deposit payment methods +func (ok *Okx) GetFiatDepositPaymentMethods(ctx context.Context, ccy currency.Code) (*FiatDepositPaymentMethods, error) { + if ccy.IsEmpty() { + return nil, currency.ErrCurrencyCodeEmpty } - return &CurrencyTakerFlow{ - Timestamp: time.UnixMilli(timestamp), - CallBuyVolume: callbuyvol, - CallSellVolume: callselvol, - PutBuyVolume: putbutvol, - PutSellVolume: putsellvol, - CallBlockVolume: callblockvol, - PutBlockVolume: putblockvol, - }, nil + params := url.Values{} + params.Set("ccy", ccy.String()) + var resp *FiatDepositPaymentMethods + return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getFiatDepositPaymentMethodsEPL, http.MethodGet, + common.EncodeURLValues("fiat/deposit-payment-methods", params), nil, &resp, request.AuthenticatedRequest) } // SendHTTPRequest sends an authenticated http request to a desired // path with a JSON payload (of present) // URL arguments must be in the request path and not as url.URL values -func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.EndpointLimit, httpMethod, requestPath string, data, result interface{}, authenticated bool) (err error) { +func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.EndpointLimit, httpMethod, requestPath string, data, result any, authenticated request.AuthType, useAsItIs ...bool) (err error) { rv := reflect.ValueOf(result) - if rv.Kind() != reflect.Pointer || rv.IsNil() { + if rv.Kind() != reflect.Pointer { return errInvalidResponseParam } endpoint, err := ok.API.Endpoints.GetURL(ep) if err != nil { return err } + var respResult interface{} + switch { + case rv.Elem().Kind() == reflect.Slice && len(useAsItIs) > 0 && !useAsItIs[0]: + respResult = &[]interface{}{&result} + case rv.Elem().Kind() == reflect.Slice || + // When needed to use the result as it is. + len(useAsItIs) > 0 && useAsItIs[0]: + respResult = result + default: + respResult = &[]interface{}{result} + } resp := struct { - Code string `json:"code"` - Msg string `json:"msg"` - Data interface{} `json:"data"` + Code types.Number `json:"code"` + Msg string `json:"msg"` + Data any `json:"data"` }{ - Data: result, + Data: respResult, } requestType := request.AuthType(request.UnauthenticatedRequest) - if authenticated { - requestType = request.AuthenticatedRequest - } newRequest := func() (*request.Item, error) { utcTime := time.Now().UTC().Format(time.RFC3339) payload := []byte("") @@ -4223,13 +5893,13 @@ func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.E if _, okay := ctx.Value(testNetVal).(bool); okay { headers["x-simulated-trading"] = "1" } - if authenticated { + if authenticated == request.AuthenticatedRequest { var creds *account.Credentials creds, err = ok.GetCredentials(ctx) if err != nil { return nil, err } - signPath := "/" + okxAPIPath + requestPath + signPath := "/" + apiPath + requestPath var hmac []byte hmac, err = crypto.GetHMAC(crypto.HashSHA256, []byte(utcTime+httpMethod+signPath+string(payload)), @@ -4255,121 +5925,26 @@ func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.E } err = ok.SendPayload(ctx, f, newRequest, requestType) if err != nil { + if authenticated == request.AuthenticatedRequest { + return fmt.Errorf("%w %w", request.ErrAuthRequestFailed, err) + } return err } - code, err := strconv.ParseInt(resp.Code, 10, 64) - if err == nil && code != 0 { + if rv.Kind() == reflect.Slice { + value, okay := result.([]interface{}) + if !okay || result == nil || len(value) == 0 { + return fmt.Errorf("%w, received invalid response", common.ErrNoResponse) + } + } + if err == nil && resp.Code.Int64() != 0 { if resp.Msg != "" { - return fmt.Errorf("error code: %d message: %s", code, resp.Msg) + return fmt.Errorf("%w error code: %d message: %s", request.ErrAuthRequestFailed, resp.Code.Int64(), resp.Msg) } - err, okay := ErrorCodes[strconv.FormatInt(code, 10)] + err, okay := ErrorCodes[resp.Code.String()] if okay { return err } - return fmt.Errorf("error code: %d", code) + return fmt.Errorf("%w error code: %d", request.ErrAuthRequestFailed, resp.Code.Int64()) } return nil } - -// Status - -// SystemStatusResponse retrieves the system status. -// state supports valid values 'scheduled', 'ongoing', 'pre_open', 'completed', and 'canceled'. -func (ok *Okx) SystemStatusResponse(ctx context.Context, state string) ([]SystemStatusResponse, error) { - params := url.Values{} - params.Set("state", state) - var resp []SystemStatusResponse - return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getEventStatusEPL, http.MethodGet, common.EncodeURLValues(systemStatus, params), nil, &resp, false) -} - -// GetAssetTypeFromInstrumentType returns an asset Item instance given and Instrument Type string. -func GetAssetTypeFromInstrumentType(instrumentType string) asset.Item { - switch strings.ToUpper(instrumentType) { - case okxInstTypeSwap, okxInstTypeContract: - return asset.PerpetualSwap - case okxInstTypeSpot: - return asset.Spot - case okxInstTypeMargin: - return asset.Margin - case okxInstTypeFutures: - return asset.Futures - case okxInstTypeOption: - return asset.Options - } - return asset.Empty -} - -// GetAssetsFromInstrumentTypeOrID parses an instrument type and instrument ID and returns a list of assets -// that the currency pair is associated with. -func (ok *Okx) GetAssetsFromInstrumentTypeOrID(instType, instrumentID string) ([]asset.Item, error) { - if instType != "" { - a := GetAssetTypeFromInstrumentType(instType) - if a != asset.Empty { - return []asset.Item{a}, nil - } - } - if instrumentID == "" { - return nil, fmt.Errorf("%w instrumentID", errEmptyArgument) - } - pf, err := ok.CurrencyPairs.GetFormat(asset.Spot, true) - if err != nil { - return nil, err - } - splitSymbol := strings.Split(instrumentID, pf.Delimiter) - if len(splitSymbol) <= 1 { - return nil, fmt.Errorf("%w %v", currency.ErrCurrencyNotSupported, instrumentID) - } - pair, err := currency.NewPairDelimiter(instrumentID, pf.Delimiter) - if err != nil { - return nil, err - } - switch { - case len(splitSymbol) == 2: - resp := make([]asset.Item, 0, 2) - enabled, err := ok.IsPairEnabled(pair, asset.Spot) - if err != nil { - return nil, err - } - if enabled { - resp = append(resp, asset.Spot) - } - enabled, err = ok.IsPairEnabled(pair, asset.Margin) - if err != nil { - return nil, err - } - if enabled { - resp = append(resp, asset.Margin) - } - if len(resp) > 0 { - return resp, nil - } - case len(splitSymbol) > 2: - switch splitSymbol[len(splitSymbol)-1] { - case "SWAP", "swap": - enabled, err := ok.IsPairEnabled(pair, asset.PerpetualSwap) - if err != nil { - return nil, err - } - if enabled { - return []asset.Item{asset.PerpetualSwap}, nil - } - case "C", "P", "c", "p": - enabled, err := ok.IsPairEnabled(pair, asset.Options) - if err != nil { - return nil, err - } - if enabled { - return []asset.Item{asset.Options}, nil - } - default: - enabled, err := ok.IsPairEnabled(pair, asset.Futures) - if err != nil { - return nil, err - } - if enabled { - return []asset.Item{asset.Futures}, nil - } - } - } - return nil, fmt.Errorf("%w '%v' or currency not enabled '%v'", asset.ErrNotSupported, instType, instrumentID) -} diff --git a/exchanges/okx/okx_business_websocket.go b/exchanges/okx/okx_business_websocket.go new file mode 100644 index 00000000000..5e52467f99e --- /dev/null +++ b/exchanges/okx/okx_business_websocket.go @@ -0,0 +1,286 @@ +package okx + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" + "github.com/thrasher-corp/gocryptotrader/log" +) + +const ( + // okxBusinessWebsocketURL + okxBusinessWebsocketURL = "wss://ws.okx.com:8443/ws/v5/business" +) + +var ( + // defaultBusinessSubscribedChannels list of channels which are subscribed by default + defaultBusinessSubscribedChannels = []string{ + okxSpreadPublicTrades, + okxSpreadOrderbook, + okxSpreadPublicTicker, + + channelPublicStrucBlockTrades, + channelPublicBlockTrades, + channelBlockTickers, + } + + // defaultBusinessAuthChannels list of authenticated channels + defaultBusinessAuthChannels = []string{ + okxSpreadOrders, + okxSpreadTrades, + } +) + +// WsConnectBusiness connects to a business wbesocket channel. +func (ok *Okx) WsConnectBusiness() error { + if !ok.Websocket.IsEnabled() || !ok.IsEnabled() { + return stream.ErrWebsocketNotEnabled + } + var dialer websocket.Dialer + dialer.ReadBufferSize = 8192 + dialer.WriteBufferSize = 8192 + + ok.Websocket.Conn.SetURL(okxBusinessWebsocketURL) + err := ok.Websocket.Conn.Dial(&dialer, http.Header{}) + if err != nil { + return err + } + ok.Websocket.Wg.Add(1) + go ok.wsReadData(ok.Websocket.Conn) + if ok.Verbose { + log.Debugf(log.ExchangeSys, "Successful connection to %v\n", + ok.Websocket.GetWebsocketURL()) + } + ok.Websocket.Conn.SetupPingHandler(request.UnAuth, stream.PingHandler{ + MessageType: websocket.TextMessage, + Message: pingMsg, + Delay: time.Second * 20, + }) + if ok.Websocket.CanUseAuthenticatedEndpoints() { + err = ok.WsSpreadAuth(context.TODO()) + if err != nil { + log.Errorf(log.ExchangeSys, "Error connecting auth socket: %s\n", err.Error()) + ok.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + } + return nil +} + +// WsSpreadAuth will connect to Okx's Private websocket connection and Authenticate with a login payload. +func (ok *Okx) WsSpreadAuth(ctx context.Context) error { + if !ok.Websocket.CanUseAuthenticatedEndpoints() { + return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", ok.Name) + } + creds, err := ok.GetCredentials(ctx) + if err != nil { + return err + } + ok.Websocket.SetCanUseAuthenticatedEndpoints(true) + timeUnix := time.Now() + signPath := "/users/self/verify" + hmac, err := crypto.GetHMAC(crypto.HashSHA256, + []byte(strconv.FormatInt(timeUnix.Unix(), 10)+http.MethodGet+signPath), + []byte(creds.Secret), + ) + if err != nil { + return err + } + base64Sign := crypto.Base64Encode(hmac) + wsReq := WebsocketEventRequest{ + Operation: operationLogin, + Arguments: []WebsocketLoginData{ + { + APIKey: creds.Key, + Passphrase: creds.ClientID, + Timestamp: timeUnix.Unix(), + Sign: base64Sign, + }, + }, + } + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, wsReq) + if err != nil { + return err + } + timer := time.NewTimer(ok.WebsocketResponseCheckTimeout) + randomID, err := common.GenerateRandomString(16) + if err != nil { + return fmt.Errorf("%w, generating random string for incoming websocket response failed", err) + } + wsResponse := make(chan *wsIncomingData) + ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ + ID: randomID, + Chan: wsResponse, + Event: operationLogin, + } + ok.WsRequestSemaphore <- 1 + defer func() { + <-ok.WsRequestSemaphore + }() + defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() + for { + select { + case data := <-wsResponse: + if data.Event == operationLogin && data.StatusCode == "0" { + ok.Websocket.SetCanUseAuthenticatedEndpoints(true) + return nil + } else if data.Event == "error" && + (data.StatusCode == "60022" || data.StatusCode == "60009") { + ok.Websocket.SetCanUseAuthenticatedEndpoints(false) + return fmt.Errorf("authentication failed with error: %v", ErrorCodes[data.StatusCode]) + } + continue + case <-timer.C: + timer.Stop() + return fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", + ok.Name, + wsReq.Operation) + } + } +} + +// GenerateDefaultBusinessSubscriptions returns a list of default subscriptions to business stream. +func (ok *Okx) GenerateDefaultBusinessSubscriptions() ([]subscription.Subscription, error) { + var subs []string + var subscriptions []subscription.Subscription + subs = append(subs, defaultBusinessSubscribedChannels...) + if ok.Websocket.CanUseAuthenticatedEndpoints() { + subs = append(subs, defaultBusinessAuthChannels...) + } + for c := range subs { + switch subs[c] { + case okxSpreadOrders, + okxSpreadTrades, + okxSpreadOrderbookLevel1, + okxSpreadOrderbook, + okxSpreadPublicTrades, + okxSpreadPublicTicker: + pairs, err := ok.GetEnabledPairs(asset.Spread) + if err != nil { + return nil, err + } + for p := range pairs { + subscriptions = append(subscriptions, subscription.Subscription{ + Channel: subs[c], + Asset: asset.Spread, + Pairs: []currency.Pair{pairs[p]}, + }) + } + case channelPublicBlockTrades, + channelBlockTickers: + pairs, err := ok.GetEnabledPairs(asset.PerpetualSwap) + if err != nil { + return nil, err + } + for p := range pairs { + subscriptions = append(subscriptions, subscription.Subscription{ + Channel: subs[c], + Asset: asset.PerpetualSwap, + Pairs: []currency.Pair{pairs[p]}, + }) + } + default: + subscriptions = append(subscriptions, subscription.Subscription{ + Channel: subs[c], + }) + } + } + return subscriptions, nil +} + +// BusinessSubscribe sends a websocket subscription request to several channels to receive data. +func (ok *Okx) BusinessSubscribe(channelsToSubscribe subscription.List) error { + return ok.handleBusinessSubscription(operationSubscribe, channelsToSubscribe) +} + +// BusinessUnsubscribe sends a websocket unsubscription request to several channels to receive data. +func (ok *Okx) BusinessUnsubscribe(channelsToUnsubscribe subscription.List) error { + return ok.handleBusinessSubscription(operationUnsubscribe, channelsToUnsubscribe) +} + +// handleBusinessSubscription sends a subscription and unsubscription information thought the business websocket endpoint. +// as of the okx, exchange this endpoint sends subscription and unsubscription messages but with a list of json objects. +func (ok *Okx) handleBusinessSubscription(operation string, subscriptions subscription.List) error { + wsSubscriptionReq := WSSubscriptionInformationList{Operation: operation} + ok.WsRequestSemaphore <- 1 + defer func() { <-ok.WsRequestSemaphore }() + var channels subscription.List + var authChannels subscription.List + var err error + for i := 0; i < len(subscriptions); i++ { + arg := SubscriptionInfo{ + Channel: subscriptions[i].Channel, + } + var instrumentID, instrumentFamily, spreadID string + switch arg.Channel { + case okxSpreadOrders, + okxSpreadTrades, + okxSpreadOrderbookLevel1, + okxSpreadOrderbook, + okxSpreadPublicTrades, + okxSpreadPublicTicker: + spreadID = subscriptions[i].Pairs[0].String() + case channelPublicBlockTrades, + channelBlockTickers: + instrumentID = subscriptions[i].Pairs[0].String() + } + instrumentFamilyInterface, okay := subscriptions[i].Params["instFamily"] + if okay { + instrumentFamily, _ = instrumentFamilyInterface.(string) + } + + arg.InstrumentFamily = instrumentFamily + arg.SpreadID = spreadID + arg.InstrumentID = instrumentID + + var chunk []byte + channels = append(channels, subscriptions[i]) + wsSubscriptionReq.Arguments = append(wsSubscriptionReq.Arguments, arg) + chunk, err = json.Marshal(wsSubscriptionReq) + if err != nil { + return err + } + if len(chunk) > maxConnByteLen { + i-- + err = ok.Websocket.Conn.SendJSONMessage(context.Background(), request.UnAuth, wsSubscriptionReq) + if err != nil { + return err + } + if operation == operationUnsubscribe { + err = ok.Websocket.RemoveSubscriptions(ok.Websocket.Conn, channels...) + } else { + err = ok.Websocket.AddSuccessfulSubscriptions(ok.Websocket.Conn, channels...) + } + if err != nil { + return err + } + channels = subscription.List{} + wsSubscriptionReq.Arguments = []SubscriptionInfo{} + continue + } + } + err = ok.Websocket.Conn.SendJSONMessage(context.Background(), request.UnAuth, wsSubscriptionReq) + if err != nil { + return err + } + + if operation == operationUnsubscribe { + channels = append(channels, authChannels...) + err = ok.Websocket.RemoveSubscriptions(ok.Websocket.Conn, channels...) + } else { + channels = append(channels, authChannels...) + err = ok.Websocket.AddSuccessfulSubscriptions(ok.Websocket.Conn, channels...) + } + return err +} diff --git a/exchanges/okx/okx_err_codes.go b/exchanges/okx/okx_err_codes.go index f9d7f30387b..29ad9ab1ad1 100644 --- a/exchanges/okx/okx_err_codes.go +++ b/exchanges/okx/okx_err_codes.go @@ -130,7 +130,7 @@ var ( "59508": errors.New("the sub account of {0} is suspended"), "59509": errors.New("account does not have permission to reset MMP status"), "59510": errors.New("sub-account does not exist"), - "59512": errors.New("unable to set up this permission for ND brokers sub accounts. by default, all ND sub accounts can transfer funds out"), + "59512": errors.New("unable to set up this permission for 'ND' brokers sub accounts"), "59601": errors.New("this sub-account name already exists, try another name"), "59602": errors.New("number of API keys exceeds the limit"), "59603": errors.New("number of sub accounts exceeds the limit"), diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 14138bbea33..97263770002 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -7,7 +7,7 @@ import ( "fmt" "log" "os" - "strings" + "sync" "testing" "time" @@ -27,6 +27,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/margin" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" @@ -43,7 +44,14 @@ const ( useTestNet = false ) -var ok = &Okx{} +var ( + ok = &Okx{} + + leadTraderUniqueID string + loadLeadTraderOnce sync.Once + + spotTP, marginTP, futuresTP, perpetualSwapTP, optionsTP, spreadTP currency.Pair +) func TestMain(m *testing.M) { cfg := config.GetConfig() @@ -59,9 +67,13 @@ func TestMain(m *testing.M) { exchCfg.API.Credentials.Secret = apiSecret exchCfg.API.Credentials.ClientID = passphrase ok.SetDefaults() + if apiKey != "" && apiSecret != "" && passphrase != "" { exchCfg.API.AuthenticatedSupport = true exchCfg.API.AuthenticatedWebsocketSupport = true + ok.API.CredentialsValidator.RequiresBase64DecodeSecret = false + ok.SetCredentials(apiKey, apiSecret, passphrase, "", "", "") + ok.Websocket.SetCanUseAuthenticatedEndpoints(true) } if !useTestNet { ok.Websocket = sharedtestvalues.NewTestWebsocket() @@ -70,14 +82,80 @@ func TestMain(m *testing.M) { if err != nil { log.Fatal(err) } + err = ok.UpdateTradablePairs(contextGenerate(), true) + if err != nil { + log.Fatal(err) + } if !useTestNet { ok.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() ok.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() setupWS() } + err = populateTradablePairs() + if err != nil { + log.Fatal(err) + } + syncLeadTraderUniqueID() os.Exit(m.Run()) } +func populateTradablePairs() error { + errNoEnabledPair := errors.New("no enabled pair found") + err := ok.UpdateTradablePairs(contextGenerate(), true) + if err != nil { + return err + } + + assetToTradablePairMap := map[asset.Item]currency.Pair{ + asset.Spot: spotTP, + asset.Margin: marginTP, + asset.Futures: futuresTP, + asset.Options: optionsTP, + asset.PerpetualSwap: perpetualSwapTP, + asset.Spread: spreadTP, + } + for a := range assetToTradablePairMap { + tradablePairs, err := ok.GetEnabledPairs(a) + if err != nil { + return err + } + if len(tradablePairs) == 0 { + return fmt.Errorf("%w %v", errNoEnabledPair, a) + } + switch a { + case asset.Spot: + spotTP = tradablePairs[0] + case asset.Margin: + marginTP = tradablePairs[0] + case asset.Futures: + futuresTP = tradablePairs[0] + case asset.Options: + optionsTP = tradablePairs[0] + case asset.PerpetualSwap: + perpetualSwapTP = tradablePairs[0] + case asset.Spread: + spreadTP = tradablePairs[0] + } + } + return nil +} + +func syncLeadTraderUniqueID() { + loadLeadTraderOnce.Do(func() { + result, err := ok.GetLeadTradersRanks(contextGenerate(), "SWAP", "pnl_ratio", "1", "", "", "", "", "", "", "", 10) + if err != nil { + log.Fatal(err) + } + if len(result) == 0 { + log.Fatal("No lead trader found") + } + if len(result[0].Ranks) == 0 { + log.Fatal("could not load lead traders ranks") + } + leadTraderUniqueID = result[0].Ranks[0].UniqueCode + }) +} + // contextGenerate sends an optional value to allow test requests // named this way, so it shows up in auto-complete and reminds you to use it func contextGenerate() context.Context { @@ -90,116 +168,182 @@ func contextGenerate() context.Context { func TestGetTickers(t *testing.T) { t.Parallel() - _, err := ok.GetTickers(contextGenerate(), "OPTION", "", "SOL-USD") - if err != nil { - t.Error("Okx GetTickers() error", err) - } + instFamily, err := ok.instrumentFamilyFromInstID(instTypeOption, optionsTP.String()) + require.NoError(t, err) + + _, err = ok.GetTickers(contextGenerate(), "", "", instFamily) + require.ErrorIs(t, err, errInvalidInstrumentType) + + result, err := ok.GetTickers(contextGenerate(), instTypeOption, "", instFamily) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetIndexTicker(t *testing.T) { t.Parallel() - _, err := ok.GetIndexTickers(contextGenerate(), "USDT", "NEAR-USDT-SWAP") - if err != nil { - t.Error("OKX GetIndexTicker() error", err) - } + _, err := ok.GetIndexTickers(contextGenerate(), currency.EMPTYCODE, "") + require.ErrorIs(t, err, errEitherInstIDOrCcyIsRequired) + + result, err := ok.GetIndexTickers(contextGenerate(), currency.USDT, "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetTicker(t *testing.T) { t.Parallel() - if _, err := ok.GetTicker(contextGenerate(), "NEAR-USDT-SWAP"); err != nil { - t.Error("Okx GetTicker() error", err) - } + _, err := ok.GetTicker(contextGenerate(), "") + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetTicker(contextGenerate(), perpetualSwapTP.String()) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetPremiumHistory(t *testing.T) { + t.Parallel() + _, err := ok.GetPremiumHistory(contextGenerate(), "", time.Time{}, time.Time{}, 10) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetPremiumHistory(contextGenerate(), perpetualSwapTP.String(), time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOrderBookDepth(t *testing.T) { t.Parallel() - _, err := ok.GetOrderBookDepth(contextGenerate(), "BTC-USDT", 400) - if err != nil { - t.Error("OKX GetOrderBookDepth() error", err) - } + _, err := ok.GetOrderBookDepth(contextGenerate(), "", 400) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetOrderBookDepth(contextGenerate(), spotTP.String(), 400) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetCandlesticks(t *testing.T) { t.Parallel() - _, err := ok.GetCandlesticks(contextGenerate(), "BTC-USDT", kline.OneHour, time.Now().Add(-time.Minute*2), time.Now(), 2) - if err != nil { - t.Error("Okx GetCandlesticks() error", err) - } + _, err := ok.GetCandlesticks(contextGenerate(), "", kline.OneHour, time.Now().Add(-time.Minute*2), time.Now(), 2) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetCandlesticks(contextGenerate(), spotTP.String(), kline.OneHour, time.Now().Add(-time.Minute*2), time.Now(), 2) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetCandlesticksHistory(t *testing.T) { t.Parallel() - _, err := ok.GetCandlesticksHistory(contextGenerate(), "BTC-USDT", kline.OneHour, time.Unix(time.Now().Unix()-int64(time.Minute), 3), time.Now(), 3) - if err != nil { - t.Error("Okx GetCandlesticksHistory() error", err) - } + _, err := ok.GetCandlesticksHistory(contextGenerate(), "", kline.OneHour, time.Unix(time.Now().Unix()-int64(time.Minute), 3), time.Now(), 3) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetCandlesticksHistory(contextGenerate(), spotTP.String(), kline.OneHour, time.Unix(time.Now().Unix()-int64(time.Minute), 3), time.Now(), 3) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetTrades(t *testing.T) { t.Parallel() - _, err := ok.GetTrades(contextGenerate(), "BTC-USDT", 3) - if err != nil { - t.Error("Okx GetTrades() error", err) - } + _, err := ok.GetTrades(contextGenerate(), "", 3) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetTrades(contextGenerate(), spotTP.String(), 3) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetTradeHistory(t *testing.T) { t.Parallel() - if _, err := ok.GetTradesHistory(contextGenerate(), "BTC-USDT", "", "", 2); err != nil { - t.Error("Okx GetTradeHistory() error", err) - } + _, err := ok.GetTradesHistory(contextGenerate(), "", "", "", 2) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetTradesHistory(contextGenerate(), spotTP.String(), "", "", 2) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetoptionTradesByInstrumentFamily(t *testing.T) { + t.Parallel() + _, err := ok.GetOptionTradesByInstrumentFamily(contextGenerate(), "") + require.ErrorIs(t, err, errInstrumentFamilyRequired) + + instFamily, err := ok.instrumentFamilyFromInstID(instTypeOption, optionsTP.String()) + require.NoError(t, err) + + result, err := ok.GetOptionTradesByInstrumentFamily(contextGenerate(), instFamily) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetOptionTrades(t *testing.T) { + t.Parallel() + _, err := ok.GetOptionTrades(contextGenerate(), "", "", "C") + require.ErrorIs(t, err, errInstrumentIDorFamilyRequired) + + instFamily, err := ok.instrumentFamilyFromInstID(instTypeOption, optionsTP.String()) + require.NoError(t, err) + + result, err := ok.GetOptionTrades(contextGenerate(), "", instFamily, "C") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGet24HTotalVolume(t *testing.T) { t.Parallel() - _, err := ok.Get24HTotalVolume(contextGenerate()) - if err != nil { - t.Error("Okx Get24HTotalVolume() error", err) - } + result, err := ok.Get24HTotalVolume(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOracle(t *testing.T) { t.Parallel() - _, err := ok.GetOracle(contextGenerate()) - if err != nil { - t.Error("Okx GetOracle() error", err) - } + t.Skip("Skipping test: The server endpoint has a rate-limiting issue that needs to be fixed.") + result, err := ok.GetOracle(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetExchangeRate(t *testing.T) { t.Parallel() - _, err := ok.GetExchangeRate(contextGenerate()) - if err != nil { - t.Error("Okx GetExchangeRate() error", err) - } + result, err := ok.GetExchangeRate(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetIndexComponents(t *testing.T) { t.Parallel() - _, err := ok.GetIndexComponents(contextGenerate(), "ETH-USDT") - if err != nil { - t.Error("Okx GetIndexComponents() error", err) - } + _, err := ok.GetIndexComponents(contextGenerate(), "") + require.ErrorIs(t, err, errIndexComponentNotFound) + + result, err := ok.GetIndexComponents(contextGenerate(), "ETH-USDT") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetBlockTickers(t *testing.T) { t.Parallel() - if _, err := ok.GetBlockTickers(contextGenerate(), "SWAP", ""); err != nil { - t.Error("Okx GetBlockTickers() error", err) - } + _, err := ok.GetBlockTickers(contextGenerate(), "", "") + require.ErrorIs(t, err, errInvalidInstrumentType) + + result, err := ok.GetBlockTickers(contextGenerate(), "SWAP", "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetBlockTicker(t *testing.T) { t.Parallel() - if _, err := ok.GetBlockTicker(contextGenerate(), "BTC-USDT"); err != nil { - t.Error("Okx GetBlockTicker() error", err) - } + _, err := ok.GetBlockTicker(contextGenerate(), "") + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetBlockTicker(contextGenerate(), "BTC-USDT") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetBlockTrade(t *testing.T) { t.Parallel() - trades, err := ok.GetBlockTrades(contextGenerate(), "BTC-USDT") - assert.NoError(t, err, "GetBlockTrades should not error") + _, err := ok.GetPublicBlockTrades(contextGenerate(), "") + require.ErrorIs(t, err, errMissingInstrumentID) + + trades, err := ok.GetPublicBlockTrades(contextGenerate(), "BTC-USDT") + require.NoError(t, err) if assert.NotEmpty(t, trades, "Should get some block trades") { trade := trades[0] assert.Equal(t, "BTC-USDT", trade.InstrumentID, "InstrumentID should have correct value") @@ -213,11 +357,11 @@ func TestGetBlockTrade(t *testing.T) { testexch.UpdatePairsOnce(t, ok) pairs, err := ok.GetAvailablePairs(asset.Options) - assert.NoError(t, err, "GetAvailablePairs should not error") - assert.NotEmpty(t, pairs, "Should get some Option pairs") + require.NoError(t, err) + require.NotEmpty(t, pairs) - publicTrades, err := ok.GetPublicBlockTrades(contextGenerate(), "", "", 100) - assert.NoError(t, err, "GetPublicBlockTrades should not error") + publicTrades, err := ok.GetPublicRFQTrades(contextGenerate(), "", "", 100) + require.NoError(t, err) tested := false LOOP: @@ -228,8 +372,8 @@ LOOP: continue } - trades, err = ok.GetBlockTrades(contextGenerate(), p.String()) - assert.NoError(t, err, "GetBlockTrades should not error on Options") + trades, err = ok.GetPublicBlockTrades(contextGenerate(), p.String()) + require.NoError(t, err, "GetBlockTrades should not error on Options") for _, trade := range trades { assert.Equal(t, p.String(), trade.InstrumentID, "InstrumentID should have correct value") assert.NotEmpty(t, trade.TradeID, "TradeID should not be empty") @@ -251,649 +395,1154 @@ LOOP: func TestGetInstrument(t *testing.T) { t.Parallel() - _, err := ok.GetInstruments(contextGenerate(), &InstrumentsFetchParams{ - InstrumentType: "OPTION", + _, err := ok.GetInstruments(contextGenerate(), &InstrumentsFetchParams{Underlying: "SOL-USD"}) + assert.ErrorIs(t, err, errInvalidInstrumentType) + + _, err = ok.GetInstruments(contextGenerate(), &InstrumentsFetchParams{ + InstrumentType: instTypeOption, Underlying: ""}) + assert.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) + + result, err := ok.GetInstruments(contextGenerate(), &InstrumentsFetchParams{ + InstrumentType: instTypeFutures, Underlying: "SOL-USD", }) - if err != nil { - t.Error("Okx GetInstruments() error", err) - } + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.GetInstruments(contextGenerate(), &InstrumentsFetchParams{ + InstrumentType: instTypeSpot, + }) + require.NoError(t, err) + assert.NotNil(t, result) + _, err = ok.GetInstruments(contextGenerate(), &InstrumentsFetchParams{ - InstrumentType: "OPTION", + InstrumentType: instTypeSwap, Underlying: "SOL-USD", }) - if err != nil { - t.Error("Okx GetInstruments() error", err) - } + assert.NoError(t, err) + assert.NotNil(t, result) } func TestGetDeliveryHistory(t *testing.T) { t.Parallel() - _, err := ok.GetDeliveryHistory(contextGenerate(), "FUTURES", "BTC-USDT", time.Time{}, time.Time{}, 3) - if err != nil { - t.Error("okx GetDeliveryHistory() error", err) - } + _, err := ok.GetDeliveryHistory(contextGenerate(), "", "BTC-USDT", "", time.Time{}, time.Time{}, 3) + require.ErrorIs(t, err, errInvalidInstrumentType) + + _, err = ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, "", "", time.Time{}, time.Time{}, 3) + require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) + + _, err = ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, "BTC-USDT", "", time.Time{}, time.Time{}, 345) + require.ErrorIs(t, err, errLimitValueExceedsMaxOf100) + + result, err := ok.GetDeliveryHistory(contextGenerate(), instTypeFutures, "BTC-USDT", "", time.Time{}, time.Time{}, 3) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOpenInterestData(t *testing.T) { t.Parallel() - if _, err := ok.GetOpenInterestData(contextGenerate(), "FUTURES", "BTC-USDT", ""); err != nil { - t.Error("Okx GetOpenInterestData() error", err) - } + _, err := ok.GetOpenInterestData(contextGenerate(), "", "BTC-USDT", "", "") + require.ErrorIs(t, err, errInvalidInstrumentType) + + _, err = ok.GetOpenInterestData(contextGenerate(), instTypeOption, "", "", "") + require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) + + uly, err := ok.underlyingFromInstID(instTypeFutures, futuresTP.String()) + require.NoError(t, err) + + result, err := ok.GetOpenInterestData(contextGenerate(), instTypeFutures, uly, "", futuresTP.String()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetSingleFundingRate(t *testing.T) { t.Parallel() - if _, err := ok.GetSingleFundingRate(context.Background(), "BTC-USD-SWAP"); err != nil { - t.Error("okx GetSingleFundingRate() error", err) - } + _, err := ok.GetSingleFundingRate(contextGenerate(), "") + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetSingleFundingRate(contextGenerate(), "BTC-USD-SWAP") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetFundingRateHistory(t *testing.T) { t.Parallel() - if _, err := ok.GetFundingRateHistory(contextGenerate(), "BTC-USD-SWAP", time.Time{}, time.Time{}, 2); err != nil { - t.Error("Okx GetFundingRateHistory() error", err) - } + _, err := ok.GetFundingRateHistory(contextGenerate(), "", time.Time{}, time.Time{}, 2) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetFundingRateHistory(contextGenerate(), "BTC-USD-SWAP", time.Time{}, time.Time{}, 2) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetLimitPrice(t *testing.T) { t.Parallel() - if _, err := ok.GetLimitPrice(contextGenerate(), "BTC-USD-SWAP"); err != nil { - t.Error("okx GetLimitPrice() error", err) - } + _, err := ok.GetLimitPrice(contextGenerate(), "") + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetLimitPrice(contextGenerate(), "BTC-USD-SWAP") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOptionMarketData(t *testing.T) { t.Parallel() - if _, err := ok.GetOptionMarketData(contextGenerate(), "BTC-USD", time.Time{}); err != nil { - t.Error("Okx GetOptionMarketData() error", err) - } + _, err := ok.GetOptionMarketData(contextGenerate(), "", "", time.Time{}) + require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) + + result, err := ok.GetOptionMarketData(contextGenerate(), "BTC-USD", "", time.Time{}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetEstimatedDeliveryPrice(t *testing.T) { t.Parallel() + _, err := ok.GetEstimatedDeliveryPrice(contextGenerate(), "") + require.ErrorIs(t, err, errMissingInstrumentID) + r, err := ok.FetchTradablePairs(contextGenerate(), asset.Futures) - if err != nil { - t.Fatal(err) - } - if _, err := ok.GetEstimatedDeliveryPrice(contextGenerate(), r[0].String()); err != nil { - t.Error("Okx GetEstimatedDeliveryPrice() error", err) - } + require.NoError(t, err) + + result, err := ok.GetEstimatedDeliveryPrice(contextGenerate(), r[0].String()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetDiscountRateAndInterestFreeQuota(t *testing.T) { t.Parallel() - _, err := ok.GetDiscountRateAndInterestFreeQuota(contextGenerate(), "", 0) - if err != nil { - t.Error("Okx GetDiscountRateAndInterestFreeQuota() error", err) - } + result, err := ok.GetDiscountRateAndInterestFreeQuota(contextGenerate(), currency.EMPTYCODE, 0) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetSystemTime(t *testing.T) { t.Parallel() - if _, err := ok.GetSystemTime(contextGenerate()); err != nil { - t.Error("Okx GetSystemTime() error", err) - } + result, err := ok.GetSystemTime(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetLiquidationOrders(t *testing.T) { t.Parallel() insts, err := ok.FetchTradablePairs(contextGenerate(), asset.Margin) - if err != nil { - t.Skip(err) - } - if _, err := ok.GetLiquidationOrders(contextGenerate(), &LiquidationOrderRequestParams{ - InstrumentType: okxInstTypeMargin, + require.NoError(t, err) + + result, err := ok.GetLiquidationOrders(contextGenerate(), &LiquidationOrderRequestParams{ + InstrumentType: instTypeMargin, Underlying: insts[0].String(), Currency: currency.BTC, Limit: 2, - }); err != nil { - t.Error("Okx GetLiquidationOrders() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetMarkPrice(t *testing.T) { t.Parallel() - if _, err := ok.GetMarkPrice(contextGenerate(), "MARGIN", "", ""); err != nil { - t.Error("Okx GetMarkPrice() error", err) - } + _, err := ok.GetMarkPrice(contextGenerate(), "", "", "", "BTC-USDT") + require.ErrorIs(t, err, errInvalidInstrumentType) + + result, err := ok.GetMarkPrice(contextGenerate(), "MARGIN", "", "", "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetPositionTiers(t *testing.T) { t.Parallel() - if _, err := ok.GetPositionTiers(contextGenerate(), "FUTURES", "cross", "BTC-USDT", "", ""); err != nil { - t.Error("Okx GetPositionTiers() error", err) - } + _, err := ok.GetPositionTiers(contextGenerate(), "", "cross", "BTC-USDT", "", "", "", currency.ETH) + require.ErrorIs(t, err, errInvalidInstrumentType) + + _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "", "BTC-USDT", "", "", "", currency.ETH) + require.ErrorIs(t, err, errInvalidTradeMode) + + _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", "", "", "", "", currency.EMPTYCODE) + require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) + + _, err = ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", "BTC-USDT", "", "", "", currency.EMPTYCODE) + require.ErrorIs(t, err, errEitherInstIDOrCcyIsRequired) + + result, err := ok.GetPositionTiers(contextGenerate(), instTypeFutures, "cross", "BTC-USDT", "", "", "", currency.ETH) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetInterestRateAndLoanQuota(t *testing.T) { t.Parallel() - if _, err := ok.GetInterestRateAndLoanQuota(contextGenerate()); err != nil { - t.Error("Okx GetInterestRateAndLoanQuota() error", err) - } + result, err := ok.GetInterestRateAndLoanQuota(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetInterestRateAndLoanQuotaForVIPLoans(t *testing.T) { t.Parallel() - if _, err := ok.GetInterestRateAndLoanQuotaForVIPLoans(contextGenerate()); err != nil { - t.Error("Okx GetInterestRateAndLoanQuotaForVIPLoans() error", err) - } + result, err := ok.GetInterestRateAndLoanQuotaForVIPLoans(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetPublicUnderlyings(t *testing.T) { t.Parallel() - if _, err := ok.GetPublicUnderlyings(contextGenerate(), "swap"); err != nil { - t.Error("Okx GetPublicUnderlyings() error", err) - } + _, err := ok.GetPublicUnderlyings(contextGenerate(), "") + require.ErrorIs(t, err, errInvalidInstrumentType) + + result, err := ok.GetPublicUnderlyings(contextGenerate(), instTypeFutures) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetInsuranceFundInformation(t *testing.T) { t.Parallel() - r, err := ok.GetInsuranceFundInformation(contextGenerate(), &InsuranceFundInformationRequestParams{ - InstrumentType: "FUTURES", + _, err := ok.GetInsuranceFundInformation(contextGenerate(), &InsuranceFundInformationRequestParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg := &InsuranceFundInformationRequestParams{Limit: 2} + _, err = ok.GetInsuranceFundInformation(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidInstrumentType) + + arg.InstrumentType = instTypeSwap + _, err = ok.GetInsuranceFundInformation(contextGenerate(), arg) + require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) + + arg.Underlying = "BTC-USDT" + r, err := ok.GetInsuranceFundInformation(contextGenerate(), arg) + assert.NoError(t, err) + assert.Positive(t, r.Total, "Total should be positive") + assert.NotEmpty(t, r.Details, "Should have some details") + for _, d := range r.Details { + assert.Positive(t, d.Balance, "Balance should be positive") + assert.NotEmpty(t, d.InsuranceType, "Type should not be empty") + assert.Positive(t, d.Timestamp, "Timestamp should be positive") + } + + r, err = ok.GetInsuranceFundInformation(contextGenerate(), &InsuranceFundInformationRequestParams{ + InstrumentType: instTypeFutures, Underlying: "BTC-USDT", Limit: 2, }) - assert.NoError(t, err, "GetInsuranceFundInformation should not error") + assert.NoError(t, err) assert.Positive(t, r.Total, "Total should be positive") assert.NotEmpty(t, r.Details, "Should have some details") for _, d := range r.Details { assert.Positive(t, d.Balance, "Balance should be positive") - assert.NotEmpty(t, d.Type, "Type should not be empty") + assert.NotEmpty(t, d.InsuranceType, "Type should not be empty") assert.Positive(t, d.Timestamp, "Timestamp should be positive") } } func TestCurrencyUnitConvert(t *testing.T) { t.Parallel() - if _, err := ok.CurrencyUnitConvert(contextGenerate(), "BTC-USD-SWAP", 1, 3500, 1, ""); err != nil { - t.Error("Okx CurrencyUnitConvert() error", err) - } + _, err := ok.CurrencyUnitConvert(contextGenerate(), "", 1, 3500, 1, currency.EMPTYCODE, false) + require.ErrorIs(t, err, errMissingInstrumentID) + + _, err = ok.CurrencyUnitConvert(contextGenerate(), "BTC-USD-SWAP", 0, 3500, 1, currency.EMPTYCODE, false) + require.ErrorIs(t, err, errMissingQuantity) + + result, err := ok.CurrencyUnitConvert(contextGenerate(), perpetualSwapTP.String(), 1, 3500, 1, currency.EMPTYCODE, false) + require.NoError(t, err) + assert.NotNil(t, result) } // Trading related endpoints test functions. func TestGetSupportCoins(t *testing.T) { t.Parallel() - if _, err := ok.GetSupportCoins(contextGenerate()); err != nil { - t.Error("Okx GetSupportCoins() error", err) - } + result, err := ok.GetSupportCoins(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetTakerVolume(t *testing.T) { t.Parallel() - if _, err := ok.GetTakerVolume(contextGenerate(), "BTC", "SPOT", time.Time{}, time.Time{}, kline.OneDay); err != nil { - t.Error("Okx GetTakerVolume() error", err) - } + _, err := ok.GetTakerVolume(contextGenerate(), currency.BTC, "", "", time.Time{}, time.Time{}, kline.OneDay) + require.ErrorIs(t, err, errInvalidInstrumentType) + + result, err := ok.GetTakerVolume(contextGenerate(), currency.BTC, instTypeSpot, "", time.Time{}, time.Time{}, kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetMarginLendingRatio(t *testing.T) { t.Parallel() - if _, err := ok.GetMarginLendingRatio(contextGenerate(), "BTC", time.Time{}, time.Time{}, kline.FiveMin); err != nil { - t.Error("Okx GetMarginLendingRatio() error", err) - } + result, err := ok.GetMarginLendingRatio(contextGenerate(), currency.BTC, time.Time{}, time.Time{}, kline.FiveMin) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetLongShortRatio(t *testing.T) { t.Parallel() - if _, err := ok.GetLongShortRatio(contextGenerate(), "BTC", time.Time{}, time.Time{}, kline.OneDay); err != nil { - t.Error("Okx GetLongShortRatio() error", err) - } + result, err := ok.GetLongShortRatio(contextGenerate(), currency.BTC, time.Time{}, time.Time{}, kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetContractsOpenInterestAndVolume(t *testing.T) { t.Parallel() - if _, err := ok.GetContractsOpenInterestAndVolume(contextGenerate(), "BTC", time.Time{}, time.Time{}, kline.OneDay); err != nil { - t.Error("Okx GetContractsOpenInterestAndVolume() error", err) - } + result, err := ok.GetContractsOpenInterestAndVolume(contextGenerate(), currency.BTC, time.Time{}, time.Time{}, kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOptionsOpenInterestAndVolume(t *testing.T) { t.Parallel() - if _, err := ok.GetOptionsOpenInterestAndVolume(contextGenerate(), "BTC", kline.OneDay); err != nil { - t.Error("Okx GetOptionsOpenInterestAndVolume() error", err) - } + result, err := ok.GetOptionsOpenInterestAndVolume(contextGenerate(), currency.BTC, kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetPutCallRatio(t *testing.T) { t.Parallel() - if _, err := ok.GetPutCallRatio(contextGenerate(), "BTC", kline.OneDay); err != nil { - t.Error("Okx GetPutCallRatio() error", err) - } + result, err := ok.GetPutCallRatio(contextGenerate(), currency.BTC, kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOpenInterestAndVolumeExpiry(t *testing.T) { t.Parallel() - if _, err := ok.GetOpenInterestAndVolumeExpiry(contextGenerate(), "BTC", kline.OneDay); err != nil { - t.Error("Okx GetOpenInterestAndVolume() error", err) - } + result, err := ok.GetOpenInterestAndVolumeExpiry(contextGenerate(), currency.BTC, kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOpenInterestAndVolumeStrike(t *testing.T) { t.Parallel() - if _, err := ok.GetOpenInterestAndVolumeStrike(contextGenerate(), "BTC", time.Now(), kline.OneDay); err != nil { - t.Error("Okx GetOpenInterestAndVolumeStrike() error", err) - } + _, err := ok.GetOpenInterestAndVolumeStrike(contextGenerate(), currency.BTC, time.Time{}, kline.OneDay) + require.ErrorIs(t, err, errMissingExpiryTimeParameter) + + result, err := ok.GetOpenInterestAndVolumeStrike(contextGenerate(), currency.BTC, time.Now(), kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetTakerFlow(t *testing.T) { t.Parallel() - if _, err := ok.GetTakerFlow(contextGenerate(), "BTC", kline.OneDay); err != nil { - t.Error("Okx GetTakerFlow() error", err) - } + result, err := ok.GetTakerFlow(contextGenerate(), currency.BTC, kline.OneDay) + require.NoError(t, err) + assert.NotNil(t, result) } func TestPlaceOrder(t *testing.T) { t.Parallel() + _, err := ok.PlaceOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) + + arg := &PlaceOrderRequestParam{ + ReduceOnly: true, + AssetType: asset.Margin, + } + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = order.Buy.Lower() + arg.TradeMode = "abc" + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = "cross" + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.OrderType = order.Limit.String() + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.AssetType = asset.Futures + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.PositionSide = "long" + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.Amount = 1 + arg.QuantityType = "abcd" + _, err = ok.PlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errCurrencyQuantityTypeRequired) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceOrder(contextGenerate(), &PlaceOrderRequestParam{ + InstrumentID: "BTC-USDC", + TradeMode: "cross", + Side: order.Buy.String(), + OrderType: "limit", + Amount: 2.6, + Price: 2.1, + Currency: "BTC", + AssetType: asset.Margin, + }) + assert.NoError(t, err) + assert.NotNil(t, result) - if _, err := ok.PlaceOrder(contextGenerate(), &PlaceOrderRequestParam{ + result, err = ok.PlaceOrder(contextGenerate(), &PlaceOrderRequestParam{ InstrumentID: "BTC-USDC", TradeMode: "cross", - Side: "Buy", + Side: order.Buy.Lower(), + PositionSide: "long", OrderType: "limit", Amount: 2.6, Price: 2.1, Currency: "BTC", - }, asset.Margin); err != nil { - t.Error("Okx PlaceOrder() error", err) - } + AssetType: asset.Futures, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -const placeMultipleOrderParamsJSON = `[{"instId":"BTC-USDT","tdMode":"cash","clOrdId":"b159","side":"buy","ordType":"limit","px":"2.15","sz":"2"},{"instId":"BTC-USDT","tdMode":"cash","clOrdId":"b15","side":"buy","ordType":"limit","px":"2.15","sz":"2"}]` +const ( + instrumentJSON = `{"alias":"","baseCcy":"","category":"1","ctMult":"1","ctType":"linear","ctVal":"0.0001","ctValCcy":"BTC","expTime":"","instFamily":"BTC-USDC","instId":"BTC-USDC-SWAP","instType":"SWAP","lever":"125","listTime":"1666076190000","lotSz":"1","maxIcebergSz":"100000000.0000000000000000","maxLmtSz":"100000000","maxMktSz":"85000","maxStopSz":"85000","maxTriggerSz":"100000000.0000000000000000","maxTwapSz":"","minSz":"1","optType":"","quoteCcy":"","settleCcy":"USDC","state":"live","stk":"","tickSz":"0.1","uly":"BTC-USDC"}` + placeOrderArgs = `[{"side": "buy","instId": "BTC-USDT","tdMode": "cash","ordType": "market","sz": "100"},{"side": "buy","instId": "LTC-USDT","tdMode": "cash","ordType": "market","sz": "1"}]` + calculateOrderbookChecksumUpdateOrderbookJSON = `{"Bids":[{"Amount":56,"Price":0.07014,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":608,"Price":0.07011,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":110,"Price":0.07009,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1264,"Price":0.07006,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":2347,"Price":0.07004,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":279,"Price":0.07003,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":52,"Price":0.07001,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":91,"Price":0.06997,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":4242,"Price":0.06996,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":486,"Price":0.06995,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":161,"Price":0.06992,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":63,"Price":0.06991,"ID":0,"Period":0,"LiquidationOrders":0, + "OrderCount":0},{"Amount":7518,"Price":0.06988,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":186,"Price":0.06976,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":71,"Price":0.06975,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1086,"Price":0.06973,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":513,"Price":0.06961,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":4603,"Price":0.06959,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":186,"Price":0.0695,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":3043,"Price":0.06946,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":103,"Price":0.06939,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5053,"Price":0.0693,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5039,"Price":0.06909,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5037,"Price":0.06888,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1526,"Price":0.06886,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5008,"Price":0.06867,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5065,"Price":0.06846,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1572,"Price":0.06826,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1565,"Price":0.06801,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":67,"Price":0.06748,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":111,"Price":0.0674,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10038,"Price":0.0672,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.06652,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1526,"Price":0.06625,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10924,"Price":0.06619,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.05986,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.05387,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.04848,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.04363,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0}],"Asks":[{"Amount":5,"Price":0.07026,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":765,"Price":0.07027,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":110,"Price":0.07028,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1264,"Price":0.0703,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":280,"Price":0.07034,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":2255,"Price":0.07035,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":28,"Price":0.07036,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":63,"Price":0.07037,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":137,"Price":0.07039,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":48,"Price":0.0704,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":32,"Price":0.07041,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":3985,"Price":0.07043,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":257,"Price":0.07057,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":7870,"Price":0.07058,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":161,"Price":0.07059,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":4539,"Price":0.07061,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1438,"Price":0.07068,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":3162,"Price":0.07088,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":99,"Price":0.07104,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5018,"Price":0.07108,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1540,"Price":0.07115,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5080,"Price":0.07129,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1512,"Price":0.07145,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5016,"Price":0.0715,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5026,"Price":0.07171,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5062,"Price":0.07192,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1517,"Price":0.07197,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1511,"Price":0.0726,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10376,"Price":0.07314,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.07354,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10277,"Price":0.07466,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":269,"Price":0.07626,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":269,"Price":0.07636,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.0809,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.08899,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.09789,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.10768,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0}],"Exchange":"Okx","Pair":"BTC-USDT","Asset":"spot","LastUpdated":"0001-01-01T00:00:00Z","LastUpdateID":0,"PriceDuplication":false,"IsFundingRate":false,"RestSnapshot":false,"IDAlignment":false}` + placeMultipleOrderParamsJSON = `[{"instId":"BTC-USDT","tdMode":"cash","clOrdId":"b159","side":"buy","ordType":"limit","px":"2.15","sz":"2"},{"instId":"BTC-USDT","tdMode":"cash","clOrdId":"b15","side":"buy","ordType":"limit","px":"2.15","sz":"2"}]` +) func TestPlaceMultipleOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - var params []PlaceOrderRequestParam err := json.Unmarshal([]byte(placeMultipleOrderParamsJSON), ¶ms) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) + + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{}) + require.ErrorIs(t, err, order.ErrSubmissionIsNil) - if _, err = ok.PlaceMultipleOrders(contextGenerate(), - params); err != nil { - t.Error("Okx PlaceMultipleOrders() error", err) + arg := PlaceOrderRequestParam{ + ReduceOnly: true, } + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = "buy" + arg.TradeMode = "abc" + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = "cross" + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.OrderType = orderLimit + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.AssetType = asset.Futures + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.PositionSide = "long" + _, err = ok.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceMultipleOrders(contextGenerate(), params) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelSingleOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelSingleOrder(contextGenerate(), &CancelOrderRequestParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.CancelSingleOrder(contextGenerate(), &CancelOrderRequestParam{OrderID: "12321312312"}) + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.CancelSingleOrder(contextGenerate(), &CancelOrderRequestParam{InstrumentID: "BTC-USDT"}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.CancelSingleOrder(contextGenerate(), - CancelOrderRequestParam{ - InstrumentID: "BTC-USDT", + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelSingleOrder(contextGenerate(), + &CancelOrderRequestParam{ + InstrumentID: spotTP.String(), OrderID: "2510789768709120", - }); err != nil { - t.Error("Okx CancelOrder() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelMultipleOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelMultipleOrders(contextGenerate(), []CancelOrderRequestParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + arg := CancelOrderRequestParam{} + _, err = ok.CancelMultipleOrders(contextGenerate(), []CancelOrderRequestParam{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) - if _, err := ok.CancelMultipleOrders(contextGenerate(), []CancelOrderRequestParam{{ - InstrumentID: "DCR-BTC", - OrderID: "2510789768709120", - }}); err != nil { - t.Error("Okx CancelMultipleOrders() error", err) - } + arg.InstrumentID = spotTP.String() + _, err = ok.CancelMultipleOrders(contextGenerate(), []CancelOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelMultipleOrders(contextGenerate(), []CancelOrderRequestParam{ + { + InstrumentID: spotTP.String(), + OrderID: "2510789768709120", + }, + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestAmendOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.AmendOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) - if _, err := ok.AmendOrder(contextGenerate(), &AmendOrderRequestParams{ - InstrumentID: "DCR-BTC", + arg := &AmendOrderRequestParams{} + _, err = ok.AmendOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.AmendOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + arg.OrderID = "1234" + _, err = ok.AmendOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendOrder(contextGenerate(), &AmendOrderRequestParams{ + InstrumentID: spotTP.String(), OrderID: "2510789768709120", NewPrice: 1233324.332, - }); err != nil { - t.Error("Okx AmendOrder() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestAmendMultipleOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.AmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - if _, err := ok.AmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{{ - InstrumentID: "BTC-USDT", + arg := AmendOrderRequestParams{ + NewPriceInUSD: 1233, + } + _, err = ok.AmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.AmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + arg.ClientOrderID = "123212" + _, err = ok.AmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) + require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{{ + InstrumentID: spotTP.String(), OrderID: "2510789768709120", NewPrice: 1233324.332, - }}); err != nil { - t.Error("Okx AmendMultipleOrders() error", err) - } + }}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestClosePositions(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{MarginMode: "cross"}) + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{InstrumentID: "BTC-USDT", MarginMode: "abc"}) + require.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) - if _, err := ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{ - InstrumentID: "BTC-USDT", + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ClosePositions(contextGenerate(), &ClosePositionsRequestParams{ + InstrumentID: spotTP.String(), MarginMode: "cross", Currency: "BTC", - }); err != nil { - t.Error("Okc ClosePositions() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOrderDetail(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{}) + assert.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{OrderID: "1234"}) + assert.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{InstrumentID: "BTC-USDT"}) + assert.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{ - InstrumentID: "BTC-USDT", - OrderID: "2510789768709120", - }); !strings.Contains(err.Error(), "Order does not exist") { - t.Error("Okx GetOrderDetail() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetOrderDetail(contextGenerate(), &OrderDetailRequestParam{InstrumentID: "SUI-USDT", OrderID: "1974857619964870656"}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOrderList(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetOrderList(contextGenerate(), &OrderListRequestParams{}) + assert.ErrorIs(t, err, common.ErrEmptyParams) - if _, err := ok.GetOrderList(contextGenerate(), &OrderListRequestParams{ - Limit: 1, - }); err != nil { - t.Error("Okx GetOrderList() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetOrderList(contextGenerate(), &OrderListRequestParams{Limit: 1}) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGet7And3MonthDayOrderHistory(t *testing.T) { +func TestGet7DayOrderHistory(t *testing.T) { t.Parallel() + _, err := ok.getOrderHistory(contextGenerate(), &OrderHistoryRequestParams{}, "", request.UnAuth) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.getOrderHistory(contextGenerate(), &OrderHistoryRequestParams{Category: "abc"}, "", request.UnAuth) + require.ErrorIs(t, err, errInvalidInstrumentType) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.Get7DayOrderHistory(contextGenerate(), &OrderHistoryRequestParams{OrderListRequestParams: OrderListRequestParams{InstrumentType: "MARGIN"}}) + require.NoError(t, err) + require.NotNil(t, result) +} - if _, err := ok.Get7DayOrderHistory(contextGenerate(), &OrderHistoryRequestParams{ - OrderListRequestParams: OrderListRequestParams{InstrumentType: "MARGIN"}, - }); err != nil { - t.Error("Okx Get7DayOrderHistory() error", err) - } - if _, err := ok.Get3MonthOrderHistory(contextGenerate(), &OrderHistoryRequestParams{ - OrderListRequestParams: OrderListRequestParams{InstrumentType: "MARGIN"}, - }); err != nil { - t.Error("Okx Get3MonthOrderHistory() error", err) - } +func TestGet3MonthOrderHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.Get3MonthOrderHistory(contextGenerate(), &OrderHistoryRequestParams{OrderListRequestParams: OrderListRequestParams{InstrumentType: "MARGIN"}}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestTransactionHistory(t *testing.T) { t.Parallel() + _, err := ok.getTransactionDetails(contextGenerate(), &TransactionDetailRequestParams{}, "", request.UnAuth) + require.ErrorIs(t, err, common.ErrEmptyParams) + + _, err = ok.getTransactionDetails(contextGenerate(), &TransactionDetailRequestParams{Limit: 10}, "", request.UnAuth) + require.ErrorIs(t, err, errInvalidInstrumentType) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetTransactionDetailsLast3Days(contextGenerate(), &TransactionDetailRequestParams{InstrumentType: "MARGIN", Limit: 1}) + require.NoError(t, err) + require.NotNil(t, result) +} - if _, err := ok.GetTransactionDetailsLast3Days(contextGenerate(), &TransactionDetailRequestParams{ - InstrumentType: "MARGIN", - Limit: 1, - }); err != nil { - t.Error("Okx GetTransactionDetailsLast3Days() error", err) - } - if _, err := ok.GetTransactionDetailsLast3Months(contextGenerate(), &TransactionDetailRequestParams{ - InstrumentType: "MARGIN", - }); err != nil { - t.Error("Okx GetTransactionDetailsLast3Days() error", err) - } +func TestGetTransactionDetailsLast3Months(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetTransactionDetailsLast3Months(contextGenerate(), &TransactionDetailRequestParams{InstrumentType: "MARGIN"}) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestStopOrder(t *testing.T) { +func TestPlaceAlgoOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.PlaceAlgoOrder(contextGenerate(), &AlgoOrderParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + arg := &AlgoOrderParams{ + ReduceOnly: true, + } + arg.OrderType = "conditional" + _, err = ok.PlaceAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) - if _, err := ok.PlaceStopOrder(contextGenerate(), &AlgoOrderParams{ - TakeProfitTriggerPriceType: "index", + arg.InstrumentID = spotTP.String() + _, err = ok.PlaceAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = TradeModeCross + _, err = ok.PlaceAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = order.Sell.Lower() + arg.OrderType = "" + _, err = ok.PlaceAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.OrderType = "limit" + _, err = ok.PlaceAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) +} + +func TestStopOrder(t *testing.T) { + t.Parallel() + _, err := ok.PlaceStopOrder(contextGenerate(), &AlgoOrderParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + arg := &AlgoOrderParams{ + ReduceOnly: true, + } + arg.OrderType = "conditional" + _, err = ok.PlaceStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.TakeProfitTriggerPrice = 123 + _, err = ok.PlaceStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrUnknownPriceType) + + arg.TakeProfitTriggerPriceType = "last_price" + arg.AlgoClientOrderID = "12345" + _, err = ok.PlaceStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.PlaceStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = TradeModeCross + _, err = ok.PlaceStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = order.Sell.Lower() + arg.OrderType = "" + _, err = ok.PlaceStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.OrderType = "limit" + _, err = ok.PlaceStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceStopOrder(contextGenerate(), &AlgoOrderParams{ + AlgoClientOrderID: "681096944655273984", + TakeProfitTriggerPriceType: "index", InstrumentID: "BTC-USDT", OrderType: "conditional", - Side: order.Sell, + Side: order.Sell.Lower(), TradeMode: "isolated", Size: 12, + TakeProfitTriggerPrice: 12335, + TakeProfitOrderPrice: 1234, + }) + assert.NoError(t, err) + assert.NotNil(t, result) +} - TakeProfitTriggerPrice: 12335, - TakeProfitOrderPrice: 1234, - }); err != nil { - t.Errorf("Okx StopOrderParams() error %v", err) - } - if _, err := ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{ - CallbackRatio: 0.01, - InstrumentID: "BTC-USDT", - OrderType: "move_order_stop", - Side: order.Buy, - TradeMode: "isolated", - Size: 2, - ActivePrice: 1234, - }); err != nil { - t.Error("Okx PlaceTrailingStopOrder error", err) - } - if _, err := ok.PlaceIcebergOrder(contextGenerate(), &AlgoOrderParams{ - PriceLimit: 100.22, - SizeLimit: 9999.9, - PriceSpread: "0.04", +func TestPlaceIcebergOrder(t *testing.T) { + t.Parallel() + _, err := ok.PlaceIcebergOrder(contextGenerate(), &AlgoOrderParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.PlaceIcebergOrder(contextGenerate(), &AlgoOrderParams{ReduceOnly: true}) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + _, err = ok.PlaceIcebergOrder(contextGenerate(), &AlgoOrderParams{OrderType: "iceberg"}) + require.ErrorIs(t, err, errMissingSizeLimit) + _, err = ok.PlaceIcebergOrder(contextGenerate(), &AlgoOrderParams{OrderType: "iceberg", SizeLimit: 123}) + require.ErrorIs(t, err, errInvalidPriceLimit) - InstrumentID: "BTC-USDT", - OrderType: "iceberg", - Side: order.Buy, + // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceIcebergOrder(contextGenerate(), &AlgoOrderParams{ + AlgoClientOrderID: "681096944655273984", + LimitPrice: 100.22, SizeLimit: 9999.9, + PriceSpread: 0.04, InstrumentID: "BTC-USDT", + OrderType: "iceberg", Side: order.Buy.Lower(), + TradeMode: "isolated", Size: 6, + }) + assert.NoError(t, err) + assert.NotNil(t, result) +} - TradeMode: "isolated", - Size: 6, - }); err != nil { - t.Error("Okx PlaceIceburgOrder() error", err) - } - if _, err := ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{ - InstrumentID: "BTC-USDT", - PriceLimit: 100.22, - SizeLimit: 9999.9, - OrderType: "twap", - PriceSpread: "0.4", - TradeMode: "cross", - Side: order.Sell, - Size: 6, - TimeInterval: kline.ThreeDay, - }); err != nil { - t.Error("Okx PlaceTWAPOrder() error", err) - } - if _, err := ok.TriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{ - TriggerPriceType: "mark", - TriggerPrice: 1234, +func TestPlaceTWAPOrder(t *testing.T) { + t.Parallel() + _, err := ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - InstrumentID: "BTC-USDT", - OrderType: "trigger", - Side: order.Buy, - TradeMode: "cross", - Size: 5, - }); err != nil { - t.Error("Okx TriggerAlogOrder() error", err) + _, err = ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{ReduceOnly: true}) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + _, err = ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{OrderType: "twap"}) + require.ErrorIs(t, err, errMissingSizeLimit) + + _, err = ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{SizeLimit: 2, OrderType: "twap"}) + require.ErrorIs(t, err, errInvalidPriceLimit) + + _, err = ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{SizeLimit: 2, OrderType: "twap", LimitPrice: 1234.5}) + require.ErrorIs(t, err, errMissingIntervalValue) + + // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceTWAPOrder(contextGenerate(), &AlgoOrderParams{ + AlgoClientOrderID: "681096944655273984", + InstrumentID: "BTC-USDT", + LimitPrice: 100.22, + SizeLimit: 9999.9, + OrderType: "twap", + PriceSpread: 0.4, + TradeMode: "cross", + Side: order.Sell.Lower(), + Size: 6, + TimeInterval: kline.ThreeDay, + }) + assert.NoError(t, err) + assert.NotNil(t, result) +} + +func TestPlaceTakeProfitStopLossOrder(t *testing.T) { + t.Parallel() + _, err := ok.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{ReduceOnly: true}) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + _, err = ok.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{OrderType: "conditional"}) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + _, err = ok.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{ + OrderType: "conditional", + StopLossTriggerPrice: 1234, + StopLossTriggerPriceType: "abcd"}) + require.ErrorIs(t, err, order.ErrUnknownPriceType) + + // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{ + OrderType: "conditional", + StopLossTriggerPrice: 1234, + StopLossTriggerPriceType: "last", + AlgoClientOrderID: "681096944655273984", + InstrumentID: "BTC-USDT", + LimitPrice: 100.22, + SizeLimit: 9999.9, + PriceSpread: 0.4, + TradeMode: "cross", + Side: order.Sell.Lower(), + Size: 6, + TimeInterval: kline.ThreeDay, + }) + assert.NoError(t, err) + assert.NotNil(t, result) +} + +func TestPlaceChaseAlgoOrder(t *testing.T) { + t.Parallel() + _, err := ok.PlaceChaseAlgoOrder(contextGenerate(), &AlgoOrderParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + arg := &AlgoOrderParams{ + ReduceOnly: true, } + _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.OrderType = "chase" + arg.MaxChaseType = "percentage" + _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errPriceTrackingNotSet) + + arg.MaxChaseType = "percentage" + arg.MaxChaseValue = .5 + _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = "BTC-USDT" + _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = "cross" + _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = order.Sell.Lower() + _, err = ok.PlaceChaseAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceChaseAlgoOrder(contextGenerate(), &AlgoOrderParams{ + AlgoClientOrderID: "681096944655273984", + InstrumentID: "BTC-USDT", + LimitPrice: 100.22, + OrderType: "chase", + TradeMode: "cross", + Side: order.Sell.Lower(), + MaxChaseType: "distance", + MaxChaseValue: .5, + Size: 6, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestCancelAlgoOrder(t *testing.T) { +func TestTriggerAlgoOrder(t *testing.T) { + t.Parallel() + _, err := ok.PlaceTriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + _, err = ok.PlaceTriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{AlgoClientOrderID: "1234"}) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + _, err = ok.PlaceTriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{AlgoClientOrderID: "1234", OrderType: "trigger"}) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + _, err = ok.PlaceTriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{AlgoClientOrderID: "1234", OrderType: "trigger", TriggerPrice: 123., TriggerPriceType: "abcd"}) + require.ErrorIs(t, err, order.ErrUnknownPriceType) + + // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceTriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{ + AlgoClientOrderID: "681096944655273984", + TriggerPriceType: "mark", + TriggerPrice: 1234, + InstrumentID: "BTC-USDT", + OrderType: "trigger", + Side: order.Buy.Lower(), + TradeMode: "cross", + Size: 5, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestPlaceTrailingStopOrder(t *testing.T) { t.Parallel() + _, err := ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{}) + assert.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{Size: 2}) + assert.ErrorIs(t, err, order.ErrTypeIsInvalid) + _, err = ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{Size: 2, OrderType: "move_order_stop"}) + assert.ErrorIs(t, err, errPriceTrackingNotSet) + + // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceTrailingStopOrder(contextGenerate(), &AlgoOrderParams{ + AlgoClientOrderID: "681096944655273984", CallbackRatio: 0.01, + InstrumentID: "BTC-USDT", OrderType: "move_order_stop", + Side: order.Buy.Lower(), TradeMode: "isolated", + Size: 2, ActivePrice: 1234}) + assert.NoError(t, err) + assert.NotNil(t, result) +} + +func TestCancelAlgoOrder(t *testing.T) { + t.Parallel() + arg := AlgoOrderCancelParams{} + _, err := ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{arg}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg.AlgoOrderID = "90994943" + _, err = ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = "BTC-USDT" + arg.AlgoOrderID = "" + _, err = ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{arg}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{ { InstrumentID: "BTC-USDT", AlgoOrderID: "90994943", }, - }); err != nil { - t.Error("Okx CancelAlgoOrder() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelAdvanceAlgoOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelAdvanceAlgoOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.CancelAdvanceAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{{}}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.CancelAdvanceAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{{InstrumentID: "90994943"}}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = ok.CancelAdvanceAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{{AlgoOrderID: "90994943"}}) + require.ErrorIs(t, err, errMissingInstrumentID) - if _, err := ok.CancelAdvanceAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelAdvanceAlgoOrder(contextGenerate(), []AlgoOrderCancelParams{{ InstrumentID: "BTC-USDT", AlgoOrderID: "90994943", - }}); err != nil { - t.Error("Okx CancelAdvanceAlgoOrder() error", err) - } + }}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetAlgoOrderList(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetAlgoOrderList(contextGenerate(), "", "", "", "", "", time.Time{}, time.Time{}, 1) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) - if _, err := ok.GetAlgoOrderList(contextGenerate(), "conditional", "", "", "", "", time.Time{}, time.Time{}, 1); err != nil { - t.Error("Okx GetAlgoOrderList() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetAlgoOrderList(contextGenerate(), "conditional", "", "", "", "", time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetAlgoOrderHistory(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetAlgoOrderHistory(contextGenerate(), "", "effective", "", "", "", time.Time{}, time.Time{}, 1) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + _, err = ok.GetAlgoOrderHistory(contextGenerate(), "conditional", "", "", "", "", time.Time{}, time.Time{}, 1) + require.ErrorIs(t, err, errMissingEitherAlgoIDOrState) - if _, err := ok.GetAlgoOrderHistory(contextGenerate(), "conditional", "effective", "", "", "", time.Time{}, time.Time{}, 1); err != nil { - t.Error("Okx GetAlgoOrderList() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetAlgoOrderHistory(contextGenerate(), "conditional", "effective", "", "", "", time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetEasyConvertCurrencyList(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetEasyConvertCurrencyList(contextGenerate()); err != nil { - t.Errorf("%s GetEasyConvertCurrencyList() error %v", ok.Name, err) - } + result, err := ok.GetEasyConvertCurrencyList(contextGenerate(), "1") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOneClickRepayCurrencyList(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetOneClickRepayCurrencyList(contextGenerate(), "cross"); err != nil && !strings.Contains(err.Error(), "Parameter acctLv error") { - t.Error(err) - } + result, err := ok.GetOneClickRepayCurrencyList(contextGenerate(), "cross") + require.NoError(t, err) + assert.NotNil(t, result) } func TestPlaceEasyConvert(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.PlaceEasyConvert(contextGenerate(), PlaceEasyConvertParam{}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) - if _, err := ok.PlaceEasyConvert(contextGenerate(), - PlaceEasyConvertParam{ - FromCurrency: []string{"BTC"}, - ToCurrency: "USDT"}); err != nil { - t.Errorf("%s PlaceEasyConvert() error %v", ok.Name, err) - } + _, err = ok.PlaceEasyConvert(contextGenerate(), PlaceEasyConvertParam{FromCurrency: []string{"BTC"}}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceEasyConvert(contextGenerate(), PlaceEasyConvertParam{FromCurrency: []string{"BTC"}, ToCurrency: "USDT"}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetEasyConvertHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetEasyConvertHistory(contextGenerate(), time.Time{}, time.Time{}, 1); err != nil { - t.Errorf("%s GetEasyConvertHistory() error %v", ok.Name, err) - } + result, err := ok.GetEasyConvertHistory(contextGenerate(), time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOneClickRepayHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetOneClickRepayHistory(contextGenerate(), time.Time{}, time.Time{}, 1); err != nil && !strings.Contains(err.Error(), "Parameter acctLv error") { - t.Errorf("%s GetOneClickRepayHistory() error %v", ok.Name, err) - } + result, err := ok.GetOneClickRepayHistory(contextGenerate(), time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestTradeOneClickRepay(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.TradeOneClickRepay(contextGenerate(), TradeOneClickRepayParam{DebtCurrency: []string{}, RepayCurrency: "USDT"}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.TradeOneClickRepay(contextGenerate(), TradeOneClickRepayParam{DebtCurrency: []string{"BTC"}, RepayCurrency: ""}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) - if _, err := ok.TradeOneClickRepay(contextGenerate(), TradeOneClickRepayParam{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.TradeOneClickRepay(contextGenerate(), TradeOneClickRepayParam{ DebtCurrency: []string{"BTC"}, RepayCurrency: "USDT", - }); err != nil { - t.Errorf("%s TradeOneClickRepay() error %v", ok.Name, err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetCounterparties(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetCounterparties(contextGenerate()); err != nil && !strings.Contains(err.Error(), "code: 70006 message: Does not meet the minimum asset requirement.") { - t.Error("Okx GetCounterparties() error", err) - } + result, err := ok.GetCounterparties(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } -const createRfqInputJSON = `{"anonymous": true,"counterparties":["Trader1","Trader2"],"clRfqId":"rfq01","legs":[{"sz":"25","side":"buy","instId":"BTCUSD-221208-100000-C"},{"sz":"150","side":"buy","instId":"ETH-USDT","tgtCcy":"base_ccy"}]}` +const createRFQInputJSON = `{"anonymous": true,"counterparties":["Trader1","Trader2"],"clRfqId":"rfq01","legs":[{"sz":"25","side":"buy","instId":"BTCUSD-221208-100000-C"},{"sz":"150","side":"buy","instId":"ETH-USDT","tgtCcy":"base_ccy"}]}` -func TestCreateRfq(t *testing.T) { +func TestCreateRFQ(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + var input *CreateRFQInput + err := json.Unmarshal([]byte(createRFQInputJSON), &input) + require.NoError(t, err) - var input CreateRfqInput - if err := json.Unmarshal([]byte(createRfqInputJSON), &input); err != nil { - t.Error("Okx Decerializing to CreateRfqInput", err) - } - if _, err := ok.CreateRfq(contextGenerate(), input); err != nil { - t.Error("Okx CreateRfq() error", err) - } + _, err = ok.CreateRFQ(contextGenerate(), &CreateRFQInput{CounterParties: []string{}}) + require.ErrorIs(t, err, errInvalidCounterParties) + + _, err = ok.CreateRFQ(contextGenerate(), &CreateRFQInput{CounterParties: []string{"Trader1"}}) + require.ErrorIs(t, err, errMissingLegs) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CreateRFQ(contextGenerate(), input) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestCancelRfq(t *testing.T) { +func TestCancelRFQ(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelRFQ(contextGenerate(), "", "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - _, err := ok.CancelRfq(contextGenerate(), CancelRfqRequestParam{}) - if err != nil && !errors.Is(err, errMissingRfqIDAndClientRfqID) { - t.Errorf("Okx CancelRfq() expecting %v, but found %v", errMissingRfqIDAndClientRfqID, err) - } - _, err = ok.CancelRfq(context.Background(), CancelRfqRequestParam{ - ClientRfqID: "somersdjskfjsdkfjxvxv", - }) - if err != nil { - t.Error("Okx CancelRfq() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelRFQ(contextGenerate(), "", "somersdjskfjsdkfjxvxv") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestMultipleCancelRfq(t *testing.T) { +func TestMultipleCancelRFQ(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelMultipleRFQs(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) - _, err := ok.CancelMultipleRfqs(contextGenerate(), CancelRfqRequestsParam{}) - if err != nil && !errors.Is(err, errMissingRfqIDAndClientRfqID) { - t.Errorf("Okx CancelMultipleRfqs() expecting %v, but found %v", errMissingRfqIDAndClientRfqID, err) - } - _, err = ok.CancelMultipleRfqs(contextGenerate(), CancelRfqRequestsParam{ - ClientRfqIDs: []string{"somersdjskfjsdkfjxvxv"}, - }) - if err != nil { - t.Error("Okx CancelMultipleRfqs() error", err) - } + _, err = ok.CancelMultipleRFQs(contextGenerate(), &CancelRFQRequestsParam{}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + _, err = ok.CancelMultipleRFQs(contextGenerate(), &CancelRFQRequestsParam{RFQIDs: make([]string, 100), ClientRFQIDs: make([]string, 100)}) + require.ErrorIs(t, err, errMaxRFQOrdersToCancel) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelMultipleRFQs(contextGenerate(), &CancelRFQRequestsParam{ClientRFQIDs: []string{"somersdjskfjsdkfjxvxv"}}) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestCancelAllRfqs(t *testing.T) { +func TestCancelAllRFQs(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - - if _, err := ok.CancelAllRfqs(contextGenerate()); err != nil { - t.Errorf("%s CancelAllRfqs() error %v", ok.Name, err) - } + result, err := ok.CancelAllRFQs(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestExecuteQuote(t *testing.T) { t.Parallel() + _, err := ok.ExecuteQuote(contextGenerate(), "", "") + assert.ErrorIs(t, err, errMissingRFQIDOrQuoteID) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ExecuteQuote(contextGenerate(), "22540", "84073") + require.NoError(t, err) + assert.NotNil(t, result) +} - _, err := ok.ExecuteQuote(contextGenerate(), ExecuteQuoteParams{}) - if err != nil && !errors.Is(err, errMissingRfqIDOrQuoteID) { - t.Errorf("Okx ExecuteQuote() expected %v, but found %v", errMissingRfqIDOrQuoteID, err) - } - if _, err = ok.ExecuteQuote(contextGenerate(), ExecuteQuoteParams{ - RfqID: "22540", - QuoteID: "84073", - }); err != nil { - t.Error("Okx ExecuteQuote() error", err) - } +func TestGetQuoteProducts(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetQuoteProducts(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestSetQuoteProducts(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.SetQuoteProducts(contextGenerate(), []SetQuoteProductParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + _, err = ok.SetQuoteProducts(contextGenerate(), []SetQuoteProductParam{{InstrumentType: "ABC"}}) + require.ErrorIs(t, err, errInvalidInstrumentType) - if _, err := ok.SetQuoteProducts(contextGenerate(), []SetQuoteProductParam{ + arg := SetQuoteProductParam{InstrumentType: "SWAP"} + _, err = ok.SetQuoteProducts(contextGenerate(), []SetQuoteProductParam{arg}) + require.ErrorIs(t, err, errMissingMakerInstrumentSettings) + + data := MakerInstrumentSetting{MaxBlockSize: 10000, MakerPriceBand: 5} + arg.Data = []MakerInstrumentSetting{data} + _, err = ok.SetQuoteProducts(contextGenerate(), []SetQuoteProductParam{arg}) + require.ErrorIs(t, err, errInvalidUnderlying) + + arg.InstrumentType = "SPOT" + data = MakerInstrumentSetting{Underlying: "BTC-USD", MaxBlockSize: 10000, MakerPriceBand: 5} + arg.Data = []MakerInstrumentSetting{data} + _, err = ok.SetQuoteProducts(contextGenerate(), []SetQuoteProductParam{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetQuoteProducts(contextGenerate(), []SetQuoteProductParam{ { InstrumentType: "SWAP", Data: []MakerInstrumentSetting{ @@ -906,30 +1555,60 @@ func TestSetQuoteProducts(t *testing.T) { Underlying: "ETH-USDT", }, }, - }}); err != nil { - t.Errorf("%s SetQuoteProducts() error %v", ok.Name, err) - } + }}) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestResetMMPStatus(t *testing.T) { +func TestResetRFQMMPStatus(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.ResetMMPStatus(contextGenerate()); err != nil && !strings.Contains(err.Error(), "No permission to use this API") { - t.Errorf("%s ResetMMPStatus() error %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ResetRFQMMPStatus(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCreateQuote(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CreateQuote(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) - if _, err := ok.CreateQuote(contextGenerate(), CreateQuoteParams{}); err != nil && !errors.Is(err, errMissingRfqID) { - t.Errorf("Okx CreateQuote() expecting %v, but found %v", errMissingRfqID, err) - } - if _, err := ok.CreateQuote(contextGenerate(), CreateQuoteParams{ - RfqID: "12345", - QuoteSide: order.Buy, + arg := &CreateQuoteParams{} + _, err = ok.CreateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingRFQID) + + arg.RFQID = "123456789" + _, err = ok.CreateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.QuoteSide = "sell" + _, err = ok.CreateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingLegs) + + subArg := QuoteLeg{} + arg.Legs = []QuoteLeg{subArg} + _, err = ok.CreateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + subArg.InstrumentID = "SOL-USD-220909" + arg.Legs = []QuoteLeg{subArg} + _, err = ok.CreateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingSizeOfQuote) + + subArg.SizeOfQuoteLeg = 2 + arg.Legs = []QuoteLeg{subArg} + _, err = ok.CreateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingLegsQuotePrice) + + subArg.Price = 1234 + arg.Legs = []QuoteLeg{subArg} + _, err = ok.CreateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CreateQuote(contextGenerate(), &CreateQuoteParams{ + RFQID: "12345", + QuoteSide: order.Buy.Lower(), Legs: []QuoteLeg{ { Price: 1234, @@ -944,879 +1623,1519 @@ func TestCreateQuote(t *testing.T) { Side: order.Buy, }, }, - }); err != nil { - t.Errorf("%s CreateQuote() error %v", ok.Name, err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelQuote(t *testing.T) { t.Parallel() + _, err := ok.CancelQuote(contextGenerate(), "", "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelQuote(contextGenerate(), "1234", "") + require.NoError(t, err) + require.NotNil(t, result) - if _, err := ok.CancelQuote(contextGenerate(), CancelQuoteRequestParams{}); err != nil && !errors.Is(err, errMissingQuoteIDOrClientQuoteID) { - t.Error("Okx CancelQuote() error", err) - } - if _, err := ok.CancelQuote(contextGenerate(), CancelQuoteRequestParams{ - QuoteID: "1234", - }); err != nil { - t.Error("Okx CancelQuote() error", err) - } - if _, err := ok.CancelQuote(contextGenerate(), CancelQuoteRequestParams{ - ClientQuoteID: "1234", - }); err != nil { - t.Error("Okx CancelQuote() error", err) - } + result, err = ok.CancelQuote(contextGenerate(), "", "1234") + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelMultipleQuote(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelMultipleQuote(contextGenerate(), CancelQuotesRequestParams{}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.CancelMultipleQuote(contextGenerate(), CancelQuotesRequestParams{}); err != nil && !errors.Is(errMissingEitherQuoteIDAOrClientQuoteIDs, err) { - t.Error("Okx CancelQuote() error", err) - } - if _, err := ok.CancelMultipleQuote(contextGenerate(), CancelQuotesRequestParams{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelMultipleQuote(contextGenerate(), CancelQuotesRequestParams{ QuoteIDs: []string{"1150", "1151", "1152"}, // Block trades require a minimum of $100,000 in assets in your trading account - }); err != nil { - t.Error("Okx CancelQuote() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestCancelAllQuotes(t *testing.T) { +func TestCancelAllRFQQuotes(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - - time, err := ok.CancelAllQuotes(contextGenerate()) - switch { - case err != nil: - t.Error("Okx CancelAllQuotes() error", err) - case time.IsZero(): - t.Error("Okx CancelAllQuotes() zero timestamp message ") - } + tt, err := ok.CancelAllRFQQuotes(contextGenerate()) + require.NoError(t, err) + assert.NotEmpty(t, tt) } -func TestGetRfqs(t *testing.T) { +func TestGetRFQs(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetRFQs(contextGenerate(), &RFQRequestParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - if _, err := ok.GetRfqs(contextGenerate(), &RfqRequestParams{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetRFQs(contextGenerate(), &RFQRequestParams{ Limit: 1, - }); err != nil { - t.Error("Okx GetRfqs() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetQuotes(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetQuotes(contextGenerate(), &QuoteRequestParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - if _, err := ok.GetQuotes(contextGenerate(), &QuoteRequestParams{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetQuotes(contextGenerate(), &QuoteRequestParams{ Limit: 3, - }); err != nil { - t.Error("Okx GetQuotes() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetRfqTrades(t *testing.T) { +func TestGetRFQTrades(t *testing.T) { t.Parallel() + _, err := ok.GetRFQTrades(contextGenerate(), &RFQTradesRequestParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetRFQTrades(contextGenerate(), &RFQTradesRequestParams{Limit: 1}) + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.GetRfqTrades(contextGenerate(), &RfqTradesRequestParams{ - Limit: 1, - }); err != nil { - t.Error("Okx GetRfqTrades() error", err) - } -} - -func TestGetPublicBlockTrades(t *testing.T) { - t.Parallel() - trades, err := ok.GetPublicBlockTrades(contextGenerate(), "", "", 3) - assert.NoError(t, err, "GetPublicBlockTrades should not error") - assert.NotEmpty(t, trades, "Should get some block trades back") - for _, trade := range trades { - assert.NotEmpty(t, trade.CreationTime, "CreationTime shound not be empty") - assert.NotEmpty(t, trade.BlockTradeID, "BlockTradeID shound not be empty") - if assert.NotEmpty(t, trade.Legs, "Should get some trades") { - leg := trade.Legs[0] - assert.NotEmpty(t, leg.InstrumentID, "InstrumentID should have correct value") - assert.NotEmpty(t, leg.TradeID, "TradeID should not be empty") - assert.Positive(t, leg.Price, "Price should have a positive value") - assert.Positive(t, leg.Size, "Size should have a positive value") - assert.Contains(t, []order.Side{order.Buy, order.Sell}, leg.Side, "Side should be a side") - } - } +func TestGetPublicRFQTrades(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetPublicRFQTrades(contextGenerate(), "", "", 3) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetFundingCurrencies(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetFundingCurrencies(contextGenerate()); err != nil { - t.Error("Okx GetFundingCurrencies() error", err) - } + result, err := ok.GetFundingCurrencies(contextGenerate(), currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetBalance(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetBalance(contextGenerate(), currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.GetBalance(contextGenerate(), ""); err != nil { - t.Error("Okx GetBalance() error", err) - } +func TestGetNonTradableAssets(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetNonTradableAssets(contextGenerate(), currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetAccountAssetValuation(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetAccountAssetValuation(contextGenerate(), ""); err != nil { - t.Error("Okx GetAccountAssetValuation() error", err) - } + result, err := ok.GetAccountAssetValuation(contextGenerate(), currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) } func TestFundingTransfer(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{}) + assert.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ + BeneficiaryAccountType: "6", RemittingAccountType: "18", Currency: currency.BTC}) + assert.ErrorIs(t, err, order.ErrAmountBelowMin) + _, err = ok.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ + Amount: 12.000, BeneficiaryAccountType: "6", + RemittingAccountType: "18", Currency: currency.EMPTYCODE}) + assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ + Amount: 12.000, BeneficiaryAccountType: "2", + RemittingAccountType: "3", Currency: currency.BTC}) + assert.ErrorIs(t, err, errAddressRequired) + _, err = ok.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ + Amount: 12.000, BeneficiaryAccountType: "2", + RemittingAccountType: "18", Currency: currency.BTC}) + assert.ErrorIs(t, err, errAddressRequired) - if _, err := ok.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ - Amount: 12.000, - To: "6", - From: "18", - Currency: "BTC", - }); err != nil { - t.Error("Okx FundingTransfer() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ + Amount: 12.000, + BeneficiaryAccountType: "6", + RemittingAccountType: "18", + Currency: currency.BTC, + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetFundsTransferState(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetFundsTransferState(contextGenerate(), "", "", 1) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.GetFundsTransferState(contextGenerate(), "754147", "1232", 1); err != nil && !strings.Contains(err.Error(), "Parameter transId error") { - t.Error("Okx GetFundsTransferState() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFundsTransferState(contextGenerate(), "754147", "1232", 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetAssetBillsDetails(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - _, err := ok.GetAssetBillsDetails(contextGenerate(), "", "", time.Time{}, time.Time{}, 0, 1) - if err != nil { - t.Error("Okx GetAssetBillsDetail() error", err) - } + result, err := ok.GetAssetBillsDetails(contextGenerate(), currency.EMPTYCODE, "", time.Time{}, time.Time{}, 0, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetLightningDeposits(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetLightningDeposits(contextGenerate(), currency.EMPTYCODE, 1.00, 0) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.GetLightningDeposits(contextGenerate(), currency.BTC, 0, 0) + require.ErrorIs(t, err, order.ErrAmountBelowMin) - if _, err := ok.GetLightningDeposits(contextGenerate(), "BTC", 1.00, 0); err != nil && !strings.Contains(err.Error(), "58355") { - t.Error("Okx GetLightningDeposits() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLightningDeposits(contextGenerate(), currency.BTC, 1.00, 0) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetCurrencyDepositAddress(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetCurrencyDepositAddress(contextGenerate(), currency.EMPTYCODE) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) - if _, err := ok.GetCurrencyDepositAddress(contextGenerate(), "BTC"); err != nil { - t.Error("Okx GetCurrencyDepositAddress() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetCurrencyDepositAddress(contextGenerate(), currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetCurrencyDepositHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetCurrencyDepositHistory(contextGenerate(), "BTC", "", "", time.Time{}, time.Time{}, 0, 1); err != nil { - t.Error("Okx GetCurrencyDepositHistory() error", err) - } + result, err := ok.GetCurrencyDepositHistory(contextGenerate(), currency.BTC, "", "", "", "271", time.Time{}, time.Time{}, 0, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestWithdrawal(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.Withdrawal(contextGenerate(), &WithdrawalInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: currency.EMPTYCODE, WithdrawalDestination: "4", ToAddress: core.BitcoinDonationAddress}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.Withdrawal(contextGenerate(), &WithdrawalInput{TransactionFee: 0.00005, Currency: currency.BTC, WithdrawalDestination: "4", ToAddress: core.BitcoinDonationAddress}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + _, err = ok.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: currency.BTC, ToAddress: core.BitcoinDonationAddress}) + require.ErrorIs(t, err, errAddressRequired) + _, err = ok.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: currency.BTC, WithdrawalDestination: "4"}) + require.ErrorIs(t, err, errAddressRequired) - _, err := ok.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: "BTC", WithdrawalDestination: "4", ToAddress: core.BitcoinDonationAddress}) - if err != nil { - t.Error("Okx Withdrawal error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: currency.BTC, WithdrawalDestination: "4", ToAddress: core.BitcoinDonationAddress}) + require.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.Withdrawal(contextGenerate(), &WithdrawalInput{ + Amount: 0.1, + WithdrawalDestination: "4", + TransactionFee: 0.00005, + Currency: currency.BTC, + ChainName: "BTC-Bitcoin", + ToAddress: core.BitcoinDonationAddress, + RecipientInformation: &WithdrawalRecipientInformation{ + WalletType: "exchange", + ExchangeID: "did:ethr:0xfeb4f99829a9acdf52979abee87e83addf22a7e1", + ReceiverFirstName: "Bruce", + ReceiverLastName: "Wayne", + }, + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestLightningWithdrawal(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.LightningWithdrawal(contextGenerate(), &LightningWithdrawalRequestInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - if _, err := ok.LightningWithdrawal(contextGenerate(), LightningWithdrawalRequestInput{ - Currency: currency.BTC.String(), + _, err = ok.LightningWithdrawal(contextGenerate(), &LightningWithdrawalRequestInput{ + Invoice: "lnbc100u1psnnvhtpp5yq2x3q5hhrzsuxpwx7ptphwzc4k4wk0j3stp0099968m44cyjg9sdqqcqzpgxqzjcsp5hz", Currency: currency.EMPTYCODE}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + _, err = ok.LightningWithdrawal(contextGenerate(), &LightningWithdrawalRequestInput{Invoice: "", Currency: currency.BTC}) + require.ErrorIs(t, err, errInvoiceTextMissing) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.LightningWithdrawal(contextGenerate(), &LightningWithdrawalRequestInput{ + Currency: currency.BTC, Invoice: "lnbc100u1psnnvhtpp5yq2x3q5hhrzsuxpwx7ptphwzc4k4wk0j3stp0099968m44cyjg9sdqqcqzpgxqzjcsp5hz", - }); err != nil { - t.Error("Okx LightningWithdrawal() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelWithdrawal(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelWithdrawal(contextGenerate(), "") + require.ErrorIs(t, err, errMissingValidWithdrawalID) - if _, err := ok.CancelWithdrawal(contextGenerate(), "fjasdfkjasdk"); err != nil { - t.Error("Okx CancelWithdrawal() error", err.Error()) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelWithdrawal(contextGenerate(), "fjasdfkjasdk") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetWithdrawalHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetWithdrawalHistory(contextGenerate(), "BTC", "", "", "", "", time.Time{}, time.Time{}, 1); err != nil { - t.Error("Okx GetWithdrawalHistory() error", err) - } + result, err := ok.GetWithdrawalHistory(contextGenerate(), currency.BTC, "", "", "", "", time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestSmallAssetsConvert(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - - if _, err := ok.SmallAssetsConvert(contextGenerate(), []string{"BTC", "USDT"}); err != nil { - t.Error("Okx SmallAssetsConvert() error", err) - } + result, err := ok.SmallAssetsConvert(contextGenerate(), []string{"BTC", "USDT"}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetSavingBalance(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetSavingBalance(contextGenerate(), "BTC"); err != nil { - t.Error("Okx GetSavingBalance() error", err) - } + result, err := ok.GetSavingBalance(contextGenerate(), currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } func TestSavingsPurchase(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.SavingsPurchaseOrRedemption(contextGenerate(), &SavingsPurchaseRedemptionInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg := &SavingsPurchaseRedemptionInput{Rate: 1} + _, err = ok.SavingsPurchaseOrRedemption(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + arg.Currency = currency.BTC + _, err = ok.SavingsPurchaseOrRedemption(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.Amount = 123.4 + _, err = ok.SavingsPurchaseOrRedemption(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Rate = 0.001 + arg.ActionType = "purchase" + _, err = ok.SavingsPurchaseOrRedemption(contextGenerate(), arg) + require.ErrorIs(t, err, errRateRequired) - if _, err := ok.SavingsPurchaseOrRedemption(contextGenerate(), &SavingsPurchaseRedemptionInput{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SavingsPurchaseOrRedemption(contextGenerate(), &SavingsPurchaseRedemptionInput{ Amount: 123.4, - Currency: "BTC", + Currency: currency.BTC, Rate: 1, ActionType: "purchase", - }); err != nil { - t.Error("Okx SavingsPurchaseOrRedemption() error", err) - } - if _, err := ok.SavingsPurchaseOrRedemption(contextGenerate(), &SavingsPurchaseRedemptionInput{ + }) + require.NoError(t, err) + require.NotNil(t, result) + + result, err = ok.SavingsPurchaseOrRedemption(contextGenerate(), &SavingsPurchaseRedemptionInput{ Amount: 123.4, - Currency: "BTC", + Currency: currency.BTC, Rate: 1, ActionType: "redempt", - }); err != nil { - t.Error("Okx SavingsPurchaseOrRedemption() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestSetLendingRate(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.SetLendingRate(contextGenerate(), &LendingRate{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.SetLendingRate(contextGenerate(), &LendingRate{Currency: currency.EMPTYCODE, Rate: 2}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.SetLendingRate(contextGenerate(), &LendingRate{Currency: currency.BTC}) + require.ErrorIs(t, err, errRateRequired) - if _, err := ok.SetLendingRate(contextGenerate(), LendingRate{Currency: "BTC", Rate: 2}); err != nil { - t.Error("Okx SetLendingRate() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetLendingRate(contextGenerate(), &LendingRate{Currency: currency.BTC, Rate: 2}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetLendingHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetLendingHistory(contextGenerate(), "USDT", time.Time{}, time.Time{}, 1); err != nil { - t.Error("Okx GetLendingHostory() error", err) - } + result, err := ok.GetLendingHistory(contextGenerate(), currency.USDT, time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetPublicBorrowInfo(t *testing.T) { t.Parallel() - if _, err := ok.GetPublicBorrowInfo(contextGenerate(), ""); err != nil { - t.Error("Okx GetPublicBorrowInfo() error", err) - } - if _, err := ok.GetPublicBorrowInfo(context.Background(), "USDT"); err != nil { - t.Error("Okx GetPublicBorrowInfo() error", err) - } + result, err := ok.GetPublicBorrowInfo(contextGenerate(), currency.EMPTYCODE) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.GetPublicBorrowInfo(contextGenerate(), currency.USDT) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetPublicBorrowHistory(t *testing.T) { t.Parallel() - if _, err := ok.GetPublicBorrowHistory(context.Background(), "USDT", time.Time{}, time.Time{}, 1); err != nil { - t.Error("Okx GetPublicBorrowHistory() error", err) - } + result, err := ok.GetPublicBorrowHistory(contextGenerate(), currency.USDT, time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetConvertCurrencies(t *testing.T) { +func TestGetMonthlyStatement(t *testing.T) { t.Parallel() + _, err := ok.GetMonthlyStatement(contextGenerate(), "") + require.ErrorIs(t, err, errMonthNameRequired) + + _, err = ok.GetMonthlyStatement(contextGenerate(), "") + require.ErrorIs(t, err, errMonthNameRequired) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetMonthlyStatement(contextGenerate(), "Jan") + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.GetConvertCurrencies(contextGenerate()); err != nil { - t.Error("Okx GetConvertCurrencies() error", err) - } +func TestApplyForMonthlyStatement(t *testing.T) { + t.Parallel() + _, err := ok.ApplyForMonthlyStatement(contextGenerate(), "") + require.ErrorIs(t, err, errMonthNameRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ApplyForMonthlyStatement(contextGenerate(), "Jan") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetConvertCurrencyPair(t *testing.T) { +func TestGetConvertCurrencies(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetConvertCurrencies(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.GetConvertCurrencyPair(contextGenerate(), "USDT", "BTC"); err != nil { - t.Error("Okx GetConvertCurrencyPair() error", err) - } +func TestGetConvertCurrencyPair(t *testing.T) { + t.Parallel() + _, err := ok.GetConvertCurrencyPair(contextGenerate(), currency.EMPTYCODE, currency.BTC) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.GetConvertCurrencyPair(contextGenerate(), currency.USDT, currency.EMPTYCODE) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetConvertCurrencyPair(contextGenerate(), currency.USDT, currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } func TestEstimateQuote(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.EstimateQuote(contextGenerate(), &EstimateQuoteRequestInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg := &EstimateQuoteRequestInput{Tag: "abcd"} + _, err = ok.EstimateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + arg.BaseCurrency = currency.BTC + _, err = ok.EstimateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + arg.QuoteCurrency = currency.BTC + _, err = ok.EstimateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + arg.Side = order.Sell.Lower() + _, err = ok.EstimateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + arg.RFQAmount = 30 + _, err = ok.EstimateQuote(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) - if _, err := ok.EstimateQuote(contextGenerate(), &EstimateQuoteRequestInput{ - BaseCurrency: "BTC", - QuoteCurrency: "USDT", - Side: "sell", - RfqAmount: 30, - RfqSzCurrency: "USDT", - }); err != nil { - t.Error("Okx EstimateQuote() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.EstimateQuote(contextGenerate(), &EstimateQuoteRequestInput{ + BaseCurrency: currency.BTC, + QuoteCurrency: currency.USDT, + Side: order.Sell.Lower(), + RFQAmount: 30, + RFQSzCurrency: "USDT", + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestConvertTrade(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.ConvertTrade(contextGenerate(), &ConvertTradeInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + arg := &ConvertTradeInput{Tag: "123"} + _, err = ok.ConvertTrade(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + arg.BaseCurrency = "BTC" + _, err = ok.ConvertTrade(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) - if _, err := ok.ConvertTrade(contextGenerate(), &ConvertTradeInput{ + arg.QuoteCurrency = "USDT" + _, err = ok.ConvertTrade(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = order.Buy.Lower() + _, err = ok.ConvertTrade(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.Size = 2 + _, err = ok.ConvertTrade(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + arg.SizeCurrency = currency.USDT + _, err = ok.ConvertTrade(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ConvertTrade(contextGenerate(), &ConvertTradeInput{ BaseCurrency: "BTC", QuoteCurrency: "USDT", - Side: "Buy", + Side: order.Buy.Lower(), Size: 2, - SizeCurrency: "USDT", - QuoteID: "quoterETH-USDT16461885104612381", - }); err != nil { - t.Error("Okx ConvertTrade() error", err) - } + SizeCurrency: currency.USDT, + QuoteID: "16461885104612381", + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetConvertHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetConvertHistory(contextGenerate(), time.Time{}, time.Time{}, 1, ""); err != nil { - t.Error("Okx GetConvertHistory() error", err) - } + result, err := ok.GetConvertHistory(contextGenerate(), time.Time{}, time.Time{}, 1, "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetNonZeroAccountBalance(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.AccountBalance(contextGenerate(), ""); err != nil { - t.Error("Okx GetBalance() error", err) - } + result, err := ok.AccountBalance(contextGenerate(), currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetPositions(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetPositions(contextGenerate(), "", "", ""); err != nil { - t.Error("Okx GetPositions() error", err) - } + result, err := ok.GetPositions(contextGenerate(), "", "", "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetPositionsHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetPositionsHistory(contextGenerate(), "", "", "", 0, 1, time.Time{}, time.Time{}); err != nil { - t.Error("Okx GetPositionsHistory() error", err) - } + result, err := ok.GetPositionsHistory(contextGenerate(), "", "", "", "1234213123", 0, 1, time.Time{}, time.Time{}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetAccountAndPositionRisk(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetAccountAndPositionRisk(contextGenerate(), ""); err != nil { - t.Error("Okx GetAccountAndPositionRisk() error", err) - } + result, err := ok.GetAccountAndPositionRisk(contextGenerate(), "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetBillsDetail(t *testing.T) { t.Parallel() + _, err := ok.GetBillsDetailLast7Days(contextGenerate(), &BillsDetailQueryParameter{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetBillsDetailLast7Days(contextGenerate(), &BillsDetailQueryParameter{ + Limit: 3}) + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.GetBillsDetailLast7Days(contextGenerate(), &BillsDetailQueryParameter{ - Limit: 3, - }); err != nil { - t.Error("Okx GetBillsDetailLast7Days() error", err) - } +func TestGetBillsDetail3Months(t *testing.T) { + t.Parallel() + _, err := ok.GetBillsDetail3Months(contextGenerate(), &BillsDetailQueryParameter{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetBillsDetail3Months(contextGenerate(), &BillsDetailQueryParameter{Limit: 3}) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetAccountConfiguration(t *testing.T) { +func TestApplyBillDetails(t *testing.T) { t.Parallel() + _, err := ok.ApplyBillDetails(contextGenerate(), "", "Q2") + require.ErrorIs(t, err, errYearRequired) + _, err = ok.ApplyBillDetails(contextGenerate(), "2023", "") + require.ErrorIs(t, err, errQuarterValueRequired) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.ApplyBillDetails(contextGenerate(), "2023", "Q2") + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.GetAccountConfiguration(contextGenerate()); err != nil { - t.Error("Okx GetAccountConfiguration() error", err) - } +func TestGetBillsHistoryArchive(t *testing.T) { + t.Parallel() + _, err := ok.GetBillsHistoryArchive(contextGenerate(), "", "Q2") + require.ErrorIs(t, err, errYearRequired) + _, err = ok.GetBillsHistoryArchive(contextGenerate(), "2023", "") + require.ErrorIs(t, err, errQuarterValueRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetBillsHistoryArchive(contextGenerate(), "2023", "Q2") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestSetPositionMode(t *testing.T) { +func TestGetAccountConfiguration(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetAccountConfiguration(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.SetPositionMode(contextGenerate(), "net_mode"); err != nil { - t.Error("Okx SetPositionMode() error", err) - } +func TestSetPositionMode(t *testing.T) { + t.Parallel() + _, err := ok.SetPositionMode(contextGenerate(), "") + require.ErrorIs(t, err, errInvalidPositionMode) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.SetPositionMode(contextGenerate(), "net_mode") + require.NoError(t, err) + assert.NotNil(t, result) } func TestSetLeverageRate(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.SetLeverageRate(contextGenerate(), &SetLeverageInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.SetLeverageRate(contextGenerate(), &SetLeverageInput{Leverage: 5, MarginMode: "isolated", AssetType: asset.Futures}) + require.ErrorIs(t, err, errEitherInstIDOrCcyIsRequired) - if _, err := ok.SetLeverageRate(contextGenerate(), SetLeverageInput{ - Currency: "USDT", + _, err = ok.SetLeverageRate(contextGenerate(), &SetLeverageInput{ + Currency: currency.USDT, + Leverage: 5, + MarginMode: "isolated", + InstrumentID: "BTC-USDT", + AssetType: asset.Futures, + }) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err = ok.SetLeverageRate(contextGenerate(), &SetLeverageInput{ + Currency: currency.USDT, Leverage: 5, MarginMode: "cross", InstrumentID: "BTC-USDT", - }); err != nil && !errors.Is(err, errNoValidResponseFromServer) { - t.Error("Okx SetLeverageRate() error", err) - } + }) + assert.True(t, err == nil || errors.Is(err, common.ErrNoResponse)) } func TestGetMaximumBuySellAmountOROpenAmount(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, "", "cross", "", 5, true) + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, "BTC-USDT", "", "", 5, true) + require.ErrorIs(t, err, errInvalidTradeModeValue) - if _, err := ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), "BTC-USDT", "cross", "BTC", "", 5); err != nil { - t.Error("Okx GetMaximumBuySellAmountOROpenAmount() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetMaximumBuySellAmountOROpenAmount(contextGenerate(), currency.BTC, "BTC-USDT", "cross", "", 5, true) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetMaximumAvailableTradableAmount(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, "", "cross", "", true, false, 123) + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, "BTC-USDT", "", "", true, false, 123) + require.ErrorIs(t, err, errInvalidTradeModeValue) - if _, err := ok.GetMaximumAvailableTradableAmount(contextGenerate(), "BTC-USDT", "BTC", "cross", true, 123); err != nil && !strings.Contains(err.Error(), "51010") { - t.Error("Okx GetMaximumAvailableTradableAmount() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetMaximumAvailableTradableAmount(contextGenerate(), currency.BTC, "BTC-USDT", "cross", "", true, false, 123) + require.NoError(t, err) + assert.NotNil(t, result) } func TestIncreaseDecreaseMargin(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.IncreaseDecreaseMargin(contextGenerate(), &IncreaseDecreaseMarginInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - if _, err := ok.IncreaseDecreaseMargin(contextGenerate(), &IncreaseDecreaseMarginInput{ - InstrumentID: "BTC-USDT", - PositionSide: "long", - Type: "add", - Amount: 1000, - Currency: "USD", - }); err != nil { - t.Error("Okx IncreaseDecreaseMargin() error", err) - } + arg := &IncreaseDecreaseMarginInput{Currency: "USD"} + _, err = ok.IncreaseDecreaseMargin(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = "BTC-USDT" + _, err = ok.IncreaseDecreaseMargin(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.PositionSide = "long" + _, err = ok.IncreaseDecreaseMargin(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.MarginBalanceType = "reduce" + _, err = ok.IncreaseDecreaseMargin(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.IncreaseDecreaseMargin(contextGenerate(), &IncreaseDecreaseMarginInput{ + InstrumentID: "BTC-USDT", + PositionSide: "long", + MarginBalanceType: "add", + Amount: 1000, + Currency: "USD", + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetLeverageRate(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetLeverageRate(contextGenerate(), "", "cross", currency.EMPTYCODE) + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.GetLeverageRate(contextGenerate(), "BTC-USDT", "", currency.EMPTYCODE) + require.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) - if _, err := ok.GetLeverageRate(contextGenerate(), "BTC-USDT", "cross"); err != nil { - t.Error("Okx GetLeverageRate() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLeverageRate(contextGenerate(), "BTC-USDT", "cross", currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetMaximumLoanOfInstrument(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetMaximumLoanOfInstrument(contextGenerate(), "", "isolated", currency.ZRX) + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.GetMaximumLoanOfInstrument(contextGenerate(), "ZRX-BTC", "", currency.ZRX) + require.ErrorIs(t, err, margin.ErrInvalidMarginType) - if _, err := ok.GetMaximumLoanOfInstrument(contextGenerate(), "ZRX-BTC", "isolated", "ZRX"); err != nil && !strings.Contains(err.Error(), "51010") { - t.Error("Okx GetMaximumLoanOfInstrument() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetMaximumLoanOfInstrument(contextGenerate(), spotTP.String(), "isolated", currency.ZRX) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetTradeFee(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetTradeFee(contextGenerate(), "", "", "", "", "") + require.ErrorIs(t, err, errInvalidInstrumentType) - if _, err := ok.GetTradeFee(contextGenerate(), "SPOT", "", ""); err != nil { - t.Error("Okx GetTradeFeeRate() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetTradeFee(contextGenerate(), instTypeSpot, "", "", "", "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetInterestAccruedData(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetInterestAccruedData(contextGenerate(), 0, 1, "", "", "", time.Time{}, time.Time{}); err != nil { - t.Error("Okx GetInterestAccruedData() error", err) - } + result, err := ok.GetInterestAccruedData(contextGenerate(), 0, 1, currency.EMPTYCODE, "", "", time.Time{}, time.Time{}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetInterestRate(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetInterestRate(contextGenerate(), ""); err != nil { - t.Error("Okx GetInterestRate() error", err) - } + result, err := ok.GetInterestRate(contextGenerate(), currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) } func TestSetGreeks(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.SetGreeks(contextGenerate(), "") + require.ErrorIs(t, err, errMissingValidGreeksType) - if _, err := ok.SetGreeks(contextGenerate(), "PA"); err != nil { - t.Error("Okx SetGreeks() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetGreeks(contextGenerate(), "PA") + require.NoError(t, err) + assert.NotNil(t, result) } func TestIsolatedMarginTradingSettings(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.IsolatedMarginTradingSettings(contextGenerate(), &IsolatedMode{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.IsolatedMarginTradingSettings(contextGenerate(), &IsolatedMode{IsoMode: "", InstrumentType: "MARGIN"}) + require.ErrorIs(t, err, errMissingIsolatedMarginTradingSetting) + _, err = ok.IsolatedMarginTradingSettings(contextGenerate(), &IsolatedMode{IsoMode: "autonomy", InstrumentType: ""}) + require.ErrorIs(t, err, errInvalidInstrumentType) - if _, err := ok.IsolatedMarginTradingSettings(contextGenerate(), IsolatedMode{ - IsoMode: "autonomy", - InstrumentType: "MARGIN", - }); err != nil { - t.Error("Okx IsolatedMarginTradingSettings() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.IsolatedMarginTradingSettings(contextGenerate(), &IsolatedMode{IsoMode: "autonomy", InstrumentType: "MARGIN"}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetMaximumWithdrawals(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetMaximumWithdrawals(contextGenerate(), "BTC"); err != nil { - t.Error("Okx GetMaximumWithdrawals() error", err) - } + result, err := ok.GetMaximumWithdrawals(contextGenerate(), currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetAccountRiskState(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetAccountRiskState(contextGenerate()); err != nil && !strings.Contains(err.Error(), "51010") { - t.Error("Okx GetAccountRiskState() error", err) - } + result, err := ok.GetAccountRiskState(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestVIPLoansBorrowAndRepay(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{Currency: currency.EMPTYCODE, Side: "borrow", Amount: 12}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{Currency: currency.BTC, Side: "", Amount: 12}) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + _, err = ok.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{Currency: currency.BTC, Side: "borrow", Amount: 0}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) - if _, err := ok.VIPLoansBorrowAndRepay(contextGenerate(), LoanBorrowAndReplayInput{Currency: "BTC", Side: "borrow", Amount: 12}); err != nil && - !strings.Contains(err.Error(), "Your account does not support VIP loan") { - t.Error("Okx VIPLoansBorrowAndRepay() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{Currency: currency.BTC, Side: "borrow", Amount: 12}) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetBorrowAndRepayHistoryForVIPLoans(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetBorrowAndRepayHistoryForVIPLoans(contextGenerate(), "", time.Time{}, time.Time{}, 3); err != nil { - t.Error("Okx GetBorrowAndRepayHistoryForVIPLoans() error", err) - } + result, err := ok.GetBorrowAndRepayHistoryForVIPLoans(contextGenerate(), currency.EMPTYCODE, time.Time{}, time.Time{}, 3) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetBorrowInterestAndLimit(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetBorrowInterestAndLimit(contextGenerate(), 1, "BTC"); err != nil && !strings.Contains(err.Error(), "59307") { // You are not eligible for VIP loans - t.Error("Okx GetBorrowInterestAndLimit() error", err) - } + result, err := ok.GetBorrowInterestAndLimit(contextGenerate(), 1, currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestPositionBuilder(t *testing.T) { +func TestGetFixedLoanBorrowLimit(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFixedLoanBorrowLimit(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFixedLoanBorrowQuote(t *testing.T) { + t.Parallel() + _, err := ok.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "", "30D", "123423423", 1, .4) + require.ErrorIs(t, err, errBorrowTypeRequired) + _, err = ok.GetFixedLoanBorrowQuote(contextGenerate(), currency.EMPTYCODE, "normal", "30D", "123423423", 1, .4) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "normal", "30D", "", 0, .4) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + _, err = ok.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "normal", "30D", "123423423", 1, 0) + require.ErrorIs(t, err, errMaxRateRequired) + _, err = ok.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "normal", "", "123423423", 1, .4) + require.ErrorIs(t, err, errLendingTermIsRequired) + _, err = ok.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "reborrow", "30D", "", 1, .4) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.PositionBuilder(contextGenerate(), PositionBuilderInput{ - ImportExistingPosition: true, - }); err != nil { - t.Error("Okx PositionBuilder() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "normal", "30D", "123423423", 1, .4) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetGreeks(t *testing.T) { +func TestPlaceFixedLoanBorrowingOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.EMPTYCODE, 1, .3, .2, "30D", false) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.USDT, 0, .3, .2, "30D", false) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + _, err = ok.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.USDT, 1, 0, .2, "30D", false) + require.ErrorIs(t, err, errMaxRateRequired) + _, err = ok.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.USDT, 1, .3, .2, "", false) + require.ErrorIs(t, err, errLendingTermIsRequired) - if _, err := ok.GetGreeks(contextGenerate(), ""); err != nil && !strings.Contains(err.Error(), "Unsupported operation") { - t.Error("Okx GetGreeks() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.USDT, 1, .3, .2, "30D", false) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetPMLimitation(t *testing.T) { +func TestAmendFixedLoanBorrowingOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.AmendFixedLoanBorrowingOrder(contextGenerate(), "", false, .4) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.GetPMLimitation(contextGenerate(), "SWAP", "BTC-USDT"); err != nil { - t.Errorf("%s GetPMLimitation() error %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendFixedLoanBorrowingOrder(contextGenerate(), "12312312", false, .4) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestViewSubaccountList(t *testing.T) { +func TestManualRenewFixedLoanBorrowingOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.ManualRenewFixedLoanBorrowingOrder(contextGenerate(), "", .3) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = ok.ManualRenewFixedLoanBorrowingOrder(contextGenerate(), "12312312", 0) + require.ErrorIs(t, err, errMaxRateRequired) - if _, err := ok.ViewSubAccountList(contextGenerate(), false, "", time.Time{}, time.Time{}, 2); err != nil { - t.Error("Okx ViewSubaccountList() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ManualRenewFixedLoanBorrowingOrder(contextGenerate(), "12312312", .3) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestResetSubAccountAPIKey(t *testing.T) { +func TestRepayFixedLoanBorrowingOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.RepayFixedLoanBorrowingOrder(contextGenerate(), "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{ - SubAccountName: "sam", - APIKey: apiKey, - APIKeyPermission: "trade", - }); err != nil && !strings.Contains(err.Error(), "Parameter subAcct can not be empty.") { - t.Errorf("%s ResetSubAccountAPIKey() error %v", ok.Name, err) - } - if _, err := ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{ - SubAccountName: "sam", - APIKey: apiKey, - Permissions: []string{"trade", "read"}, - }); err != nil && !strings.Contains(err.Error(), "Parameter subAcct can not be empty.") { - t.Errorf("%s ResetSubAccountAPIKey() error %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.RepayFixedLoanBorrowingOrder(contextGenerate(), "12321") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetSubaccountTradingBalance(t *testing.T) { +func TestConvertFixedLoanToMarketLoan(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.ConvertFixedLoanToMarketLoan(contextGenerate(), "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.GetSubaccountTradingBalance(contextGenerate(), ""); err != nil && !errors.Is(err, errMissingRequiredParameterSubaccountName) { - t.Errorf("Okx GetSubaccountTradingBalance() expecting \"%v\", but found \"%v\"", errMissingRequiredParameterSubaccountName, err) - } - if _, err := ok.GetSubaccountTradingBalance(contextGenerate(), "test1"); err != nil && !strings.Contains(err.Error(), "sub-account does not exist") { - t.Error("Okx GetSubaccountTradingBalance() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ConvertFixedLoanToMarketLoan(contextGenerate(), "12321") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetSubaccountFundingBalance(t *testing.T) { +func TestReduceLiabilitiesForFixedLoan(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.ReduceLiabilitiesForFixedLoan(contextGenerate(), "", false) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.GetSubaccountFundingBalance(contextGenerate(), "test1", ""); err != nil && !strings.Contains(err.Error(), "Sub-account test1 does not exists") && !strings.Contains(err.Error(), "59510") { - t.Error("Okx GetSubaccountFundingBalance() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ReduceLiabilitiesForFixedLoan(contextGenerate(), "123123", false) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestHistoryOfSubaccountTransfer(t *testing.T) { +func TestGetFixedLoanBorrowOrderList(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.HistoryOfSubaccountTransfer(contextGenerate(), "", "0", "", time.Time{}, time.Time{}, 1); err != nil { - t.Error("Okx HistoryOfSubaccountTransfer() error", err) - } + result, err := ok.GetFixedLoanBorrowOrderList(contextGenerate(), currency.USDT, "1231231", "8", "30D", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestMasterAccountsManageTransfersBetweenSubaccounts(t *testing.T) { +func TestManualBorrowOrRepay(t *testing.T) { t.Parallel() + _, err := ok.ManualBorrowOrRepay(contextGenerate(), currency.EMPTYCODE, "borrow", 1) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.ManualBorrowOrRepay(contextGenerate(), currency.USDT, "", 1) + require.ErrorIs(t, err, errLendingSideRequired) + _, err = ok.ManualBorrowOrRepay(contextGenerate(), currency.USDT, "borrow", 0) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ManualBorrowOrRepay(contextGenerate(), currency.USDT, "borrow", 1) + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), &SubAccountAssetTransferParams{Currency: "BTC", Amount: 1200, From: 9, To: 9, FromSubAccount: "", ToSubAccount: "", LoanTransfer: true}); err != nil && !errors.Is(err, errInvalidSubaccount) { - t.Error("Okx MasterAccountsManageTransfersBetweenSubaccounts() error", err) - } - if _, err := ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), &SubAccountAssetTransferParams{Currency: "BTC", Amount: 1200, From: 8, To: 8, FromSubAccount: "", ToSubAccount: "", LoanTransfer: true}); err != nil && !errors.Is(err, errInvalidSubaccount) { - t.Error("Okx MasterAccountsManageTransfersBetweenSubaccounts() error", err) - } - if _, err := ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), &SubAccountAssetTransferParams{Currency: "BTC", Amount: 1200, From: 6, To: 6, FromSubAccount: "test1", ToSubAccount: "test2", LoanTransfer: true}); err != nil && !strings.Contains(err.Error(), "Sub-account test1 does not exists") { - t.Error("Okx MasterAccountsManageTransfersBetweenSubaccounts() error", err) - } +func TestSetAutoRepay(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetAutoRepay(contextGenerate(), true) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestSetPermissionOfTransferOut(t *testing.T) { +func TestGetBorrowRepayHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.SetPermissionOfTransferOut(contextGenerate(), PermissionOfTransfer{SubAcct: "Test1"}); err != nil && !strings.Contains(err.Error(), "Sub-account does not exist") { - t.Error("Okx SetPermissionOfTransferOut() error", err) - } + result, err := ok.GetBorrowRepayHistory(contextGenerate(), currency.ETH, "auto_borrow", time.Time{}, time.Time{}, 100) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetCustodyTradingSubaccountList(t *testing.T) { +func TestNewPositionBuilder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.NewPositionBuilder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) - if _, err := ok.GetCustodyTradingSubaccountList(contextGenerate(), ""); err != nil { - t.Error("Okx GetCustodyTradingSubaccountList() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.NewPositionBuilder(contextGenerate(), &PositionBuilderParam{ + InclRealPosAndEq: false, + SimPos: []SimulatedPosition{ + { + Position: "-10", + InstrumentID: "BTC-USDT-SWAP", + }, + { + Position: "10", + InstrumentID: "LTC-USDT-SWAP", + }, + }, + SimAsset: []SimulatedAsset{ + { + Currency: "USDT", + Amount: 100, + }, + }, + SpotOffsetType: "1", + GreeksType: "CASH", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -const gridTradingPlaceOrder = `{"instId": "BTC-USD-SWAP","algoOrdType": "contract_grid","maxPx": "5000","minPx": "400","gridNum": "10","runType": "1","sz": "200", "direction": "long","lever": "2"}` - -func TestPlaceGridAlgoOrder(t *testing.T) { +func TestSetRiskOffsetAmount(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.SetRiskOffsetAmount(contextGenerate(), currency.EMPTYCODE, 123) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.SetRiskOffsetAmount(contextGenerate(), currency.USDT, 0) + require.ErrorIs(t, err, order.ErrAmountBelowMin) - var input GridAlgoOrder - if err := json.Unmarshal([]byte(gridTradingPlaceOrder), &input); err != nil { - t.Error("Okx Decerializing to GridALgoOrder error", err) - } - if _, err := ok.PlaceGridAlgoOrder(contextGenerate(), &input); err != nil { - t.Error("Okx PlaceGridAlgoOrder() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.SetRiskOffsetAmount(contextGenerate(), currency.USDT, 123) + require.NoError(t, err) + assert.NotNil(t, result) } -const gridOrderAmendAlgo = `{ - "algoId":"448965992920907776", - "instId":"BTC-USDT", - "slTriggerPx":"1200", - "tpTriggerPx":"" -}` +func TestGetGreeks(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetGreeks(contextGenerate(), currency.EMPTYCODE) + assert.NoError(t, err) +} -func TestAmendGridAlgoOrder(t *testing.T) { +func TestGetPMLimitation(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.GetPMPositionLimitation(contextGenerate(), "", "BTC-USDT", "") + require.ErrorIs(t, err, errInvalidInstrumentType) + _, err = ok.GetPMPositionLimitation(contextGenerate(), "SWAP", "", "") + require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) - var input GridAlgoOrderAmend - if err := json.Unmarshal([]byte(gridOrderAmendAlgo), &input); err != nil { - t.Error("Okx Decerializing to GridAlgoOrderAmend error", err) - } - if _, err := ok.AmendGridAlgoOrder(contextGenerate(), input); err != nil { - t.Error("Okx AmendGridAlgoOrder() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetPMPositionLimitation(contextGenerate(), "SWAP", "BTC-USDT", "") + require.NoError(t, err) + assert.NotNil(t, result) } -const stopGridAlgoOrderJSON = `{"algoId":"198273485", "instId":"BTC-USDT", "stopType":"1", "algoOrdType":"grid"}` +func TestViewSubaccountList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.ViewSubAccountList(contextGenerate(), false, "", time.Time{}, time.Time{}, 2) + require.NoError(t, err) + assert.NotNil(t, result) +} -func TestStopGridAlgoOrder(t *testing.T) { +func TestResetSubAccountAPIKey(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.ResetSubAccountAPIKey(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) + _, err = ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{APIKey: apiKey, APIKeyPermission: "trade"}) + require.ErrorIs(t, err, errInvalidSubAccountName) + _, err = ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{SubAccountName: "sam", APIKey: "", APIKeyPermission: "trade"}) + require.ErrorIs(t, err, errInvalidAPIKey) + _, err = ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{IP: "1.2.3.", SubAccountName: "sam", APIKeyPermission: "trade", APIKey: "sample-api-key"}) + require.ErrorIs(t, err, errInvalidIPAddress) + _, err = ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{APIKeyPermission: "abc", APIKey: "sample-api-key", SubAccountName: "sam"}) + require.ErrorIs(t, err, errInvalidAPIKeyPermission) + _, err = ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{ + Permissions: []string{"abc"}, SubAccountName: "sam", + APIKey: "sample-api-key"}) + require.ErrorIs(t, err, errInvalidAPIKeyPermission) - var resp StopGridAlgoOrderRequest - if err := json.Unmarshal([]byte(stopGridAlgoOrderJSON), &resp); err != nil { - t.Error("error deserializing to StopGridAlgoOrder error", err) - } - if _, err := ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{ - resp, - }); err != nil && !strings.Contains(err.Error(), "The strategy does not exist or has stopped") { - t.Error("Okx StopGridAlgoOrder() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{ + SubAccountName: "sam", + APIKey: apiKey, + APIKeyPermission: "trade", + }) + assert.NoError(t, err) + assert.NotNil(t, result) + result, err = ok.ResetSubAccountAPIKey(contextGenerate(), &SubAccountAPIKeyParam{ + SubAccountName: "sam", + APIKey: apiKey, + Permissions: []string{"trade", "read"}, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetGridAlgoOrdersList(t *testing.T) { +func TestGetSubaccountTradingBalance(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetSubaccountTradingBalance(contextGenerate(), "") + assert.ErrorIs(t, err, errInvalidSubAccountName) - if _, err := ok.GetGridAlgoOrdersList(contextGenerate(), "grid", "", "", "", "", "", 1); err != nil { - t.Error("Okx GetGridAlgoOrdersList() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSubaccountTradingBalance(contextGenerate(), "test1") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetGridAlgoOrderHistory(t *testing.T) { +func TestGetSubaccountFundingBalance(t *testing.T) { t.Parallel() + _, err := ok.GetSubaccountFundingBalance(contextGenerate(), "", currency.EMPTYCODE) + require.ErrorIs(t, err, errInvalidSubAccountName) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSubaccountFundingBalance(contextGenerate(), "test1", currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) +} +func TestGetSubAccountMaximumWithdrawal(t *testing.T) { + t.Parallel() + _, err := ok.GetSubAccountMaximumWithdrawal(contextGenerate(), "", currency.BTC) + require.ErrorIs(t, err, errInvalidSubAccountName) - if _, err := ok.GetGridAlgoOrderHistory(contextGenerate(), "contract_grid", "", "", "", "", "", 1); err != nil { - t.Error("Okx GetGridAlgoOrderHistory() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSubAccountMaximumWithdrawal(contextGenerate(), "test1", currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetGridAlgoOrderDetails(t *testing.T) { +func TestHistoryOfSubaccountTransfer(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetGridAlgoOrderDetails(contextGenerate(), "grid", ""); err != nil && !errors.Is(err, errMissingAlgoOrderID) { - t.Errorf("Okx GetGridAlgoOrderDetails() expecting %v, but found %v error", errMissingAlgoOrderID, err) - } - if _, err := ok.GetGridAlgoOrderDetails(contextGenerate(), "grid", "7878"); err != nil && !strings.Contains(err.Error(), "Order does not exist") { - t.Error("Okx GetGridAlgoOrderDetails() error", err) - } + result, err := ok.HistoryOfSubaccountTransfer(contextGenerate(), currency.EMPTYCODE, "0", "", time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetGridAlgoSubOrders(t *testing.T) { +func TestGetHistoryOfManagedSubAccountTransfer(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetGridAlgoSubOrders(contextGenerate(), "", "", "", "", "", "", 2); err != nil && !errors.Is(err, errMissingAlgoOrderType) { - t.Errorf("Okx GetGridAlgoSubOrders() expecting %v, but found %v", err, errMissingAlgoOrderType) - } - if _, err := ok.GetGridAlgoSubOrders(contextGenerate(), "grid", "", "", "", "", "", 2); err != nil && !errors.Is(err, errMissingAlgoOrderID) { - t.Errorf("Okx GetGridAlgoSubOrders() expecting %v, but found %v", err, errMissingAlgoOrderID) - } - if _, err := ok.GetGridAlgoSubOrders(contextGenerate(), "grid", "1234", "", "", "", "", 2); err != nil && !errors.Is(err, errMissingSubOrderType) { - t.Errorf("Okx GetGridAlgoSubOrders() expecting %v, but found %v", err, errMissingSubOrderType) - } - if _, err := ok.GetGridAlgoSubOrders(contextGenerate(), "grid", "1234", "live", "", "", "", 2); err != nil && !errors.Is(err, errMissingSubOrderType) { - t.Errorf("Okx GetGridAlgoSubOrders() expecting %v, but found %v", err, errMissingSubOrderType) - } + result, err := ok.GetHistoryOfManagedSubAccountTransfer(contextGenerate(), currency.BTC, "", "", "", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) } -const spotGridAlgoOrderPosition = `{"adl": "1","algoId": "449327675342323712","avgPx": "29215.0142857142857149","cTime": "1653400065917","ccy": "USDT","imr": "2045.386","instId": "BTC-USDT-SWAP","instType": "SWAP","last": "29206.7","lever": "5","liqPx": "661.1684795867162","markPx": "29213.9","mgnMode": "cross","mgnRatio": "217.19370606167573","mmr": "40.907720000000005","notionalUsd": "10216.70307","pos": "35","posSide": "net","uTime": "1653400066938","upl": "1.674999999999818","uplRatio": "0.0008190504784478"}` - -func TestGetGridAlgoOrderPositions(t *testing.T) { +func TestMasterAccountsManageTransfersBetweenSubaccounts(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), &SubAccountAssetTransferParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - var resp AlgoOrderPosition - if err := json.Unmarshal([]byte(spotGridAlgoOrderPosition), &resp); err != nil { - t.Error("Okx Decerializing to AlgoOrderPosition error", err) - } - if _, err := ok.GetGridAlgoOrderPositions(contextGenerate(), "", ""); err != nil && !errors.Is(err, errInvalidAlgoOrderType) { - t.Errorf("Okx GetGridAlgoOrderPositions() expecting %v, but found %v", errInvalidAlgoOrderType, err) - } - if _, err := ok.GetGridAlgoOrderPositions(contextGenerate(), "contract_grid", ""); err != nil && !errors.Is(err, errMissingAlgoOrderID) { - t.Errorf("Okx GetGridAlgoOrderPositions() expecting %v, but found %v", errMissingAlgoOrderID, err) - } - if _, err := ok.GetGridAlgoOrderPositions(contextGenerate(), "contract_grid", ""); err != nil && !errors.Is(err, errMissingAlgoOrderID) { - t.Errorf("Okx GetGridAlgoOrderPositions() expecting %v, but found %v", errMissingAlgoOrderID, err) - } - if _, err := ok.GetGridAlgoOrderPositions(contextGenerate(), "contract_grid", "448965992920907776"); err != nil && !strings.Contains(err.Error(), "The strategy does not exist or has stopped") { - t.Errorf("Okx GetGridAlgoOrderPositions() expecting %v, but found %v", errMissingAlgoOrderID, err) - } + arg := &SubAccountAssetTransferParams{LoanTransfer: true} + _, err = ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + arg.Currency = currency.BTC + _, err = ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.Amount = 1234 + _, err = ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidSubaccount) + + arg.From = 1 + _, err = ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidSubaccount) + + arg.To = 7 + _, err = ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidSubaccount) + + arg.To = 6 + _, err = ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidSubAccountName) + + arg.FromSubAccount = "sami" + _, err = ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidSubAccountName) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), &SubAccountAssetTransferParams{Currency: currency.BTC, Amount: 1200, From: 6, To: 6, FromSubAccount: "test1", ToSubAccount: "test2", LoanTransfer: true}) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestSpotGridWithdrawProfit(t *testing.T) { +func TestSetPermissionOfTransferOut(t *testing.T) { + t.Parallel() + _, err := ok.SetPermissionOfTransferOut(contextGenerate(), &PermissionOfTransfer{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.SetPermissionOfTransferOut(contextGenerate(), &PermissionOfTransfer{CanTransOut: true}) + require.ErrorIs(t, err, errInvalidSubAccountName) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.SetPermissionOfTransferOut(contextGenerate(), &PermissionOfTransfer{SubAcct: "Test1"}) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetCustodyTradingSubaccountList(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetCustodyTradingSubaccountList(contextGenerate(), "") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestSetSubAccountVIPLoanAllocation(t *testing.T) { t.Parallel() + _, err := ok.SetSubAccountVIPLoanAllocation(contextGenerate(), &SubAccountLoanAllocationParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg := subAccountVIPLoanAllocationInfo{} + _, err = ok.SetSubAccountVIPLoanAllocation(contextGenerate(), &SubAccountLoanAllocationParam{Alloc: []subAccountVIPLoanAllocationInfo{arg}}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg.LoanAlloc = 123 + _, err = ok.SetSubAccountVIPLoanAllocation(contextGenerate(), &SubAccountLoanAllocationParam{Alloc: []subAccountVIPLoanAllocationInfo{arg}}) + require.ErrorIs(t, err, errInvalidSubAccountName) + + arg.LoanAlloc = -1 + arg.SubAcct = "sams" + _, err = ok.SetSubAccountVIPLoanAllocation(contextGenerate(), &SubAccountLoanAllocationParam{Alloc: []subAccountVIPLoanAllocationInfo{arg}}) + require.ErrorIs(t, err, errInvalidLoanAllocationValue) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetSubAccountVIPLoanAllocation(contextGenerate(), &SubAccountLoanAllocationParam{ + Enable: true, + Alloc: []subAccountVIPLoanAllocationInfo{ + { + SubAcct: "subAcct1", + LoanAlloc: 20.01, + }, + }, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.SpotGridWithdrawProfit(contextGenerate(), ""); err != nil && !errors.Is(err, errMissingAlgoOrderID) { - t.Errorf("Okx SpotGridWithdrawProfit() expecting %v, but found %v", errMissingAlgoOrderID, err) - } - if _, err := ok.SpotGridWithdrawProfit(contextGenerate(), "1234"); err != nil { - t.Error("Okx SpotGridWithdrawProfit() error", err) - } +func TestGetSubAccountBorrowInterestAndLimit(t *testing.T) { + t.Parallel() + _, err := ok.GetSubAccountBorrowInterestAndLimit(contextGenerate(), "", currency.ETH) + require.ErrorIs(t, err, errInvalidSubAccountName) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSubAccountBorrowInterestAndLimit(contextGenerate(), "123456", currency.ETH) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestComputeMarginBalance(t *testing.T) { +// ETH Staking + +func TestGetProductInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetProductInfo(contextGenerate()) + require.NoError(t, err) + assert.NotEmpty(t, result) +} - if _, err := ok.ComputeMarginBalance(contextGenerate(), MarginBalanceParam{ - AlgoID: "123456", - Type: "other", - }); err != nil && !errors.Is(err, errInvalidMarginTypeAdjust) { - t.Errorf("%s ComputeMarginBalance() expected %v, but found %v", ok.Name, errInvalidMarginTypeAdjust, err) - } - if _, err := ok.ComputeMarginBalance(contextGenerate(), MarginBalanceParam{ - AlgoID: "123456", - Type: "add", - }); err != nil && !strings.Contains(err.Error(), "The strategy does not exist or has stopped") { - t.Errorf("%s ComputeMarginBalance() error %v", ok.Name, err) - } +func TestPurcahseETHStaking(t *testing.T) { + t.Parallel() + err := ok.PurchaseETHStaking(contextGenerate(), 0) + assert.ErrorIs(t, err, order.ErrAmountBelowMin) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + err = ok.PurchaseETHStaking(contextGenerate(), 100) + assert.NoError(t, err) } -func TestAdjustMarginBalance(t *testing.T) { +// RedeemETHStaking +func TestRedeemETHStaking(t *testing.T) { t.Parallel() + err := ok.RedeemETHStaking(contextGenerate(), 0) + assert.ErrorIs(t, err, order.ErrAmountBelowMin) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + err = ok.RedeemETHStaking(contextGenerate(), 100) + assert.NoError(t, err) +} - if _, err := ok.AdjustMarginBalance(contextGenerate(), MarginBalanceParam{ - AlgoID: "1234", - Type: "add", - Amount: 12345, - }); err != nil { - t.Errorf("%s AdjustMarginBalance() error %v", ok.Name, err) - } +func TestGetBETHAssetsBalance(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetBETHAssetsBalance(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetPurchaseAndRedeemHistory(t *testing.T) { + t.Parallel() + _, err := ok.GetPurchaseAndRedeemHistory(contextGenerate(), "", "pending", time.Time{}, time.Now(), 10) + require.ErrorIs(t, err, errLendingTermIsRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetPurchaseAndRedeemHistory(contextGenerate(), "purchase", "pending", time.Time{}, time.Now(), 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetAPYHistory(t *testing.T) { + t.Parallel() + result, err := ok.GetAPYHistory(contextGenerate(), 34) + require.NoError(t, err) + assert.NotNil(t, result) +} + +const gridTradingPlaceOrder = `{"instId": "BTC-USD-SWAP","algoOrdType": "contract_grid","maxPx": "5000","minPx": "400","gridNum": "10","runType": "1","sz": "200", "direction": "long","lever": "2"}` + +func TestPlaceGridAlgoOrder(t *testing.T) { + t.Parallel() + var input GridAlgoOrder + err := json.Unmarshal([]byte(gridTradingPlaceOrder), &input) + require.NoError(t, err) + + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), &GridAlgoOrder{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg := &GridAlgoOrder{BasePosition: true} + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingAlgoOrderType) + + arg.AlgoOrdType = "contract_grid" + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.MaxPrice = 1000 + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.MinPrice = 1200 + arg.GridQuantity = -1 + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidGridQuantity) + + arg.GridQuantity = 123 + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountMustBeSet) + + arg.Size = 123 + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingRequiredArgumentDirection) + + arg.Direction = positionSideLong + _, err = ok.PlaceGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidLeverage) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceGridAlgoOrder(contextGenerate(), &input) + require.NoError(t, err) + assert.NotNil(t, result) +} + +const gridOrderAmendAlgo = `{ + "algoId":"448965992920907776", + "instId":"BTC-USDT", + "slTriggerPx":"1200", + "tpTriggerPx":"" +}` + +func TestAmendGridAlgoOrder(t *testing.T) { + t.Parallel() + var input *GridAlgoOrderAmend + err := json.Unmarshal([]byte(gridOrderAmendAlgo), &input) + require.NoError(t, err) + + arg := &GridAlgoOrderAmend{} + _, err = ok.AmendGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg.TakeProfitTriggerPrice = 1234.5 + _, err = ok.AmendGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errAlgoIDRequired) + + arg.AlgoID = "560472804207104000" + _, err = ok.AmendGridAlgoOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendGridAlgoOrder(contextGenerate(), input) + require.NoError(t, err) + assert.NotNil(t, result) +} + +const stopGridAlgoOrderJSON = `{"algoId":"198273485", "instId":"BTC-USDT", "stopType":"1", "algoOrdType":"grid"}` + +func TestStopGridAlgoOrder(t *testing.T) { + t.Parallel() + var resp StopGridAlgoOrderRequest + err := json.Unmarshal([]byte(stopGridAlgoOrderJSON), &resp) + require.NoError(t, err) + + _, err = ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg := StopGridAlgoOrderRequest{} + _, err = ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{arg}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg.StopType = 20 + _, err = ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{arg}) + require.ErrorIs(t, err, errAlgoIDRequired) + + arg.AlgoID = "algo_id" + _, err = ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{arg}) + require.ErrorIs(t, err, errMissingAlgoOrderType) + + arg.AlgoOrderType = AlgoOrdTypeGrid + _, err = ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{arg}) + require.ErrorIs(t, err, errMissingValidStopType) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.StopGridAlgoOrder(contextGenerate(), []StopGridAlgoOrderRequest{resp}) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetGridAlgoOrdersList(t *testing.T) { + t.Parallel() + _, err := ok.GetGridAlgoOrdersList(contextGenerate(), "abc", "", "", "", "", "", 1) + require.ErrorIs(t, err, errMissingAlgoOrderType) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetGridAlgoOrdersList(contextGenerate(), "grid", "", "", "", "", "", 1) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetGridAlgoOrderHistory(t *testing.T) { + t.Parallel() + _, err := ok.GetGridAlgoOrderHistory(contextGenerate(), "abc", "", "", "", "", "", 1) + require.ErrorIs(t, err, errMissingAlgoOrderType) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetGridAlgoOrderHistory(contextGenerate(), "contract_grid", "", "", "", "", "", 1) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetGridAlgoOrderDetails(t *testing.T) { + t.Parallel() + _, err := ok.GetGridAlgoOrderDetails(contextGenerate(), "grid", "") + require.ErrorIs(t, err, errAlgoIDRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetGridAlgoOrderDetails(contextGenerate(), "grid", "7878") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetGridAlgoSubOrders(t *testing.T) { + t.Parallel() + _, err := ok.GetGridAlgoSubOrders(contextGenerate(), "", "", "", "", "", "", 2) + require.ErrorIs(t, err, errMissingAlgoOrderType) + _, err = ok.GetGridAlgoSubOrders(contextGenerate(), "grid", "", "", "", "", "", 2) + require.ErrorIs(t, err, errAlgoIDRequired) + _, err = ok.GetGridAlgoSubOrders(contextGenerate(), "grid", "1234", "", "", "", "", 2) + require.ErrorIs(t, err, errMissingSubOrderType) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetGridAlgoSubOrders(contextGenerate(), "grid", "1234", "live", "", "", "", 2) + require.NoError(t, err) + assert.NotNil(t, result) +} + +const spotGridAlgoOrderPosition = `{"adl": "1","algoId": "449327675342323712","avgPx": "29215.0142857142857149","cTime": "1653400065917","ccy": "USDT","imr": "2045.386","instId": "BTC-USDT-SWAP","instType": "SWAP","last": "29206.7","lever": "5","liqPx": "661.1684795867162","markPx": "29213.9","mgnMode": "cross","mgnRatio": "217.19370606167573","mmr": "40.907720000000005","notionalUsd": "10216.70307","pos": "35","posSide": "net","uTime": "1653400066938","upl": "1.674999999999818","uplRatio": "0.0008190504784478"}` + +func TestGetGridAlgoOrderPositions(t *testing.T) { + t.Parallel() + var resp AlgoOrderPosition + err := json.Unmarshal([]byte(spotGridAlgoOrderPosition), &resp) + require.NoError(t, err) + _, err = ok.GetGridAlgoOrderPositions(contextGenerate(), "", "") + require.ErrorIs(t, err, errInvalidAlgoOrderType) + _, err = ok.GetGridAlgoOrderPositions(contextGenerate(), "contract_grid", "") + require.ErrorIs(t, err, errAlgoIDRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetGridAlgoOrderPositions(contextGenerate(), "contract_grid", "448965992920907776") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestSpotGridWithdrawProfit(t *testing.T) { + t.Parallel() + _, err := ok.SpotGridWithdrawProfit(contextGenerate(), "") + require.ErrorIs(t, err, errAlgoIDRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SpotGridWithdrawProfit(contextGenerate(), "1234") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestComputeMarginBalance(t *testing.T) { + t.Parallel() + _, err := ok.ComputeMarginBalance(contextGenerate(), MarginBalanceParam{AlgoID: "123456", AdjustMarginBalanceType: "other"}) + require.ErrorIs(t, err, errInvalidMarginTypeAdjust) + + _, err = ok.ComputeMarginBalance(contextGenerate(), MarginBalanceParam{AdjustMarginBalanceType: "other"}) + require.ErrorIs(t, err, errAlgoIDRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.ComputeMarginBalance(contextGenerate(), MarginBalanceParam{ + AlgoID: "123456", + AdjustMarginBalanceType: "reduce", + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestAdjustMarginBalance(t *testing.T) { + t.Parallel() + arg := &MarginBalanceParam{} + _, err := ok.AdjustMarginBalance(contextGenerate(), arg) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg.Amount = 12345 + _, err = ok.AdjustMarginBalance(contextGenerate(), arg) + require.ErrorIs(t, err, errAlgoIDRequired) + + arg.AlgoID = "1234" + _, err = ok.AdjustMarginBalance(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidMarginTypeAdjust) + + arg.AdjustMarginBalanceType = "reduce" + arg.Amount = 0 + _, err = ok.AdjustMarginBalance(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountIsInvalid) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AdjustMarginBalance(contextGenerate(), &MarginBalanceParam{ + AlgoID: "1234", + AdjustMarginBalanceType: "reduce", + Amount: 12345, + }) + require.NoError(t, err) + assert.NotNil(t, result) } const gridAIParamJSON = `{"algoOrdType": "grid","annualizedRate": "1.5849","ccy": "USDT","direction": "", "duration": "7D","gridNum": "5","instId": "BTC-USDT","lever": "0","maxPx": "21373.3","minInvestment": "0.89557758", "minPx": "15544.2", "perMaxProfitRate": "0.0733865364573281","perMinProfitRate": "0.0561101403446263","runType": "1"}` @@ -1824,91 +3143,141 @@ const gridAIParamJSON = `{"algoOrdType": "grid","annualizedRate": "1.5849","ccy" func TestGetGridAIParameter(t *testing.T) { t.Parallel() var response GridAIParameterResponse - if err := json.Unmarshal([]byte(gridAIParamJSON), &response); err != nil { - t.Errorf("%s error while deserializing to GridAIParameterResponse error %v", ok.Name, err) - } - if _, err := ok.GetGridAIParameter(contextGenerate(), "grid", "BTC-USDT", "", ""); err != nil { - t.Errorf("%s GetGridAIParameter() error %v", ok.Name, err) - } + err := json.Unmarshal([]byte(gridAIParamJSON), &response) + require.NoError(t, err) + + _, err = ok.GetGridAIParameter(contextGenerate(), "", "BTC-USDT", "", "") + require.ErrorIs(t, err, errInvalidAlgoOrderType) + _, err = ok.GetGridAIParameter(contextGenerate(), "grid", "", "", "") + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.GetGridAIParameter(contextGenerate(), "contract_grid", "BTC-USDT", "", "") + require.ErrorIs(t, err, errMissingRequiredArgumentDirection) + _, err = ok.GetGridAIParameter(contextGenerate(), "grid", "BTC-USDT", "", "12M") + require.ErrorIs(t, err, errInvalidDuration) + + result, err := ok.GetGridAIParameter(contextGenerate(), "grid", "BTC-USDT", "", "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOffers(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetOffers(contextGenerate(), "", "", ""); err != nil { - t.Errorf("%s GetOffers() error %v", ok.Name, err) - } + result, err := ok.GetOffers(contextGenerate(), "", "", currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) } func TestPurchase(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.Purchase(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) - if _, err := ok.Purchase(contextGenerate(), PurchaseRequestParam{ + _, err = ok.Purchase(contextGenerate(), &PurchaseRequestParam{Term: 2}) + require.ErrorIs(t, err, errMissingRequiredParameter) + _, err = ok.Purchase(contextGenerate(), &PurchaseRequestParam{ProductID: "1234", Term: 2, InvestData: []PurchaseInvestDataItem{{Amount: 1}}}) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.Purchase(contextGenerate(), &PurchaseRequestParam{ProductID: "1234", Term: 2, InvestData: []PurchaseInvestDataItem{{Currency: currency.USDT}}}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.Purchase(contextGenerate(), &PurchaseRequestParam{ ProductID: "1234", InvestData: []PurchaseInvestDataItem{ { - Currency: "BTC", + Currency: currency.BTC, Amount: 100, }, { - Currency: "ETH", + Currency: currency.ETH, Amount: 100, }, }, Term: 30, - }); err != nil { - t.Errorf("%s Purchase() %v", ok.Name, err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestRedeem(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.Redeem(contextGenerate(), &RedeemRequestParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.Redeem(contextGenerate(), &RedeemRequestParam{AllowEarlyRedeem: true}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = ok.Redeem(contextGenerate(), &RedeemRequestParam{OrderID: "754147"}) + require.ErrorIs(t, err, errInvalidProtocolType) - if _, err := ok.Redeem(contextGenerate(), RedeemRequestParam{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.Redeem(contextGenerate(), &RedeemRequestParam{ OrderID: "754147", ProtocolType: "defi", AllowEarlyRedeem: true, - }); err != nil && !strings.Contains(err.Error(), "Order not found") { - t.Errorf("%s Redeem() error %v", ok.Name, err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelPurchaseOrRedemption(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelPurchaseOrRedemption(contextGenerate(), &CancelFundingParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.CancelPurchaseOrRedemption(contextGenerate(), &CancelFundingParam{ProtocolType: "defi"}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = ok.CancelPurchaseOrRedemption(contextGenerate(), &CancelFundingParam{OrderID: "754147"}) + require.ErrorIs(t, err, errInvalidProtocolType) - if _, err := ok.CancelPurchaseOrRedemption(contextGenerate(), CancelFundingParam{ + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelPurchaseOrRedemption(contextGenerate(), &CancelFundingParam{ OrderID: "754147", ProtocolType: "defi", - }); err != nil && !strings.Contains(err.Error(), "Order not found") { - t.Errorf("%s CancelPurchaseOrRedemption() error %v", ok.Name, err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetEarnActiveOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetEarnActiveOrders(contextGenerate(), "", "", "", ""); err != nil { - t.Errorf("%s GetEarnActiveOrders() error %v", ok.Name, err) - } + result, err := ok.GetEarnActiveOrders(contextGenerate(), "", "", "", currency.EMPTYCODE) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetFundingOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetFundingOrderHistory(contextGenerate(), "", "", "", time.Time{}, time.Time{}, 1); err != nil { - t.Errorf("%s GetFundingOrderHistory() error %v", ok.Name, err) - } + result, err := ok.GetFundingOrderHistory(contextGenerate(), "", "", currency.EMPTYCODE, time.Time{}, time.Time{}, 1) + require.NoError(t, err) + assert.NotNil(t, result) } func TestSystemStatusResponse(t *testing.T) { t.Parallel() - if _, err := ok.SystemStatusResponse(contextGenerate(), "completed"); err != nil { - t.Error("Okx SystemStatusResponse() error", err) + result, err := ok.SystemStatusResponse(contextGenerate(), "completed") + require.NoError(t, err) + assert.NotNil(t, result) +} + +var instrumentTypeToAssetTypeMap = map[string]struct { + AssetType asset.Item + Error error +}{ + instTypeSwap: {AssetType: asset.PerpetualSwap}, + instTypeContract: {AssetType: asset.PerpetualSwap}, + instTypeSpot: {AssetType: asset.Spot}, + instTypeMargin: {AssetType: asset.Margin}, + instTypeFutures: {AssetType: asset.Futures}, + instTypeOption: {AssetType: asset.Options}, + "": {AssetType: asset.Empty}, + "lol": {AssetType: asset.Empty, Error: asset.ErrNotSupported}, +} + +func TestAssetTypeFromInstrumentType(t *testing.T) { + t.Parallel() + for k, v := range instrumentTypeToAssetTypeMap { + assetItem, err := assetTypeFromInstrumentType(k) + require.ErrorIs(t, err, v.Error) + assert.Equal(t, v.AssetType, assetItem) } } @@ -1916,8 +3285,10 @@ func TestSystemStatusResponse(t *testing.T) { func TestFetchTradablePairs(t *testing.T) { t.Parallel() - if _, err := ok.FetchTradablePairs(contextGenerate(), asset.Options); err != nil { - t.Error("Okx FetchTradablePairs() error", err) + for _, a := range []asset.Item{asset.Options, asset.PerpetualSwap, asset.Futures, asset.Spot, asset.Spread} { + result, err := ok.FetchTradablePairs(contextGenerate(), a) + assert.NoError(t, err) + assert.NotNil(t, result) } } @@ -1928,7 +3299,6 @@ func TestUpdateTradablePairs(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - tests := map[asset.Item][]currency.Pair{ asset.Spot: { currency.NewPair(currency.ETH, currency.USDT), @@ -1941,23 +3311,24 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { } for _, a := range []asset.Item{asset.PerpetualSwap, asset.Futures, asset.Options} { - pairs, err := ok.FetchTradablePairs(context.Background(), a) + pairs, err := ok.FetchTradablePairs(contextGenerate(), a) if assert.NoErrorf(t, err, "FetchTradablePairs should not error for %s", a) { tests[a] = []currency.Pair{pairs[0]} } } + var err error for _, a := range ok.GetAssetTypes(false) { - if err := ok.UpdateOrderExecutionLimits(context.Background(), a); err != nil { - t.Error("Okx UpdateOrderExecutionLimits() error", err) + err = ok.UpdateOrderExecutionLimits(contextGenerate(), a) + if !assert.NoError(t, err) { continue } for _, p := range tests[a] { limits, err := ok.GetOrderExecutionLimits(a, p) if assert.NoError(t, err, "GetOrderExecutionLimits should not error") { - assert.Positivef(t, limits.PriceStepIncrementSize, "PriceStepIncrementSize should be positive for %s", p) - assert.Positivef(t, limits.MinimumBaseAmount, "PriceStepIncrementSize should be positive for %s", p) + require.Positivef(t, limits.PriceStepIncrementSize, "PriceStepIncrementSize should be positive for %s", p) + require.Positivef(t, limits.MinimumBaseAmount, "PriceStepIncrementSize should be positive for %s", p) } } } @@ -1965,236 +3336,461 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { func TestUpdateTicker(t *testing.T) { t.Parallel() - if _, err := ok.UpdateTicker(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot); err != nil { - t.Error("Okx UpdateTicker() error", err) - } + result, err := ok.UpdateTicker(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot) + require.NoError(t, err) + assert.NotNil(t, result) } func TestUpdateTickers(t *testing.T) { t.Parallel() - if err := ok.UpdateTickers(contextGenerate(), asset.Spot); err != nil { - t.Error("Okx UpdateTicker() error", err) - } + err := ok.UpdateTickers(contextGenerate(), asset.Spot) + require.NoError(t, err) + err = ok.UpdateTickers(contextGenerate(), asset.Spread) + assert.NoError(t, err) } func TestFetchTicker(t *testing.T) { t.Parallel() - _, err := ok.FetchTicker(contextGenerate(), currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP")), asset.PerpetualSwap) - if err != nil { - t.Error("Okx FetchTicker() error", err) - } - if _, err = ok.FetchTicker(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot); err != nil { - t.Error("Okx FetchTicker() error", err) - } + result, err := ok.FetchTicker(contextGenerate(), currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP")), asset.PerpetualSwap) + require.NoError(t, err) + require.NotNil(t, result) + result, err = ok.FetchTicker(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot) + require.NoError(t, err) + assert.NotNil(t, result) } func TestFetchOrderbook(t *testing.T) { t.Parallel() - if _, err := ok.FetchOrderbook(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot); err != nil { - t.Error("Okx FetchOrderbook() error", err) - } + result, err := ok.FetchOrderbook(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot) + require.NoError(t, err) + assert.NotNil(t, result) } func TestUpdateOrderbook(t *testing.T) { t.Parallel() - if _, err := ok.UpdateOrderbook(contextGenerate(), currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP")), asset.Spot); err != nil { - t.Error("Okx UpdateOrderbook() error", err) - } + result, err := ok.UpdateOrderbook(contextGenerate(), currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP")), asset.Spot) + require.NoError(t, err) + require.NotNil(t, result) + + result, err = ok.UpdateOrderbook(contextGenerate(), spreadTP, asset.Spread) + require.NoError(t, err) + assert.NotNil(t, result) } func TestUpdateAccountInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.UpdateAccountInfo(contextGenerate(), asset.Spot); err != nil { - t.Error("Okx UpdateAccountInfo() error", err) - } + result, err := ok.UpdateAccountInfo(contextGenerate(), asset.Spot) + require.NoError(t, err) + assert.NotNil(t, result) } func TestFetchAccountInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.FetchAccountInfo(contextGenerate(), asset.Spot); err != nil { - t.Error("Okx FetchAccountInfo() error", err) - } + result, err := ok.FetchAccountInfo(contextGenerate(), asset.Spot) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetAccountFundingHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetAccountFundingHistory(contextGenerate()); err != nil { - t.Error("Okx GetFundingHistory() error", err) - } + result, err := ok.GetAccountFundingHistory(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetWithdrawalsHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetWithdrawalsHistory(contextGenerate(), currency.BTC, asset.Spot); err != nil { - t.Error("Okx GetWithdrawalsHistory() error", err) - } + result, err := ok.GetWithdrawalsHistory(contextGenerate(), currency.BTC, asset.Spot) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetRecentTrades(t *testing.T) { t.Parallel() - if _, err := ok.GetRecentTrades(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.PerpetualSwap); err != nil { - t.Error("Okx GetRecentTrades() error", err) - } + result, err := ok.GetRecentTrades(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.PerpetualSwap) + require.NoError(t, err) + require.NotNil(t, result) + result, err = ok.GetRecentTrades(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spread) + require.NoError(t, err) + assert.NotNil(t, result) } func TestSubmitOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - - var resp WsPlaceOrderInput + var resp []PlaceOrderRequestParam err := json.Unmarshal([]byte(placeOrderArgs), &resp) - if err != nil { - t.Fatal(err) - } - if len(resp.Arguments) == 0 { - t.Error("order not found") + require.NoError(t, err) + + arg := &order.Submit{ + Exchange: ok.Name, + Side: order.Buy, + Type: order.Limit, + Price: 1, + ClientID: "yeneOrder", + AssetType: asset.Binary, } - var orderSubmission = &order.Submit{ + _, err = ok.SubmitOrder(contextGenerate(), arg) + require.ErrorIs(t, err, asset.ErrNotSupported) + + arg.AssetType = asset.Spot + _, err = ok.SubmitOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.Amount = 1000000000 + _, err = ok.SubmitOrder(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + arg.Pair = spotTP + arg.AssetType = asset.Futures + arg.Leverage = -1 + _, err = ok.SubmitOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSubmitLeverageNotSupported) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + arg = &order.Submit{ Pair: currency.Pair{ Base: currency.LTC, Quote: currency.BTC, }, Exchange: ok.Name, - Side: order.Buy, + Side: order.Sell, Type: order.Limit, - Price: 1, + Price: 120000, Amount: 1000000000, ClientID: "yeneOrder", AssetType: asset.Spot, } - _, err = ok.SubmitOrder(contextGenerate(), orderSubmission) - if err != nil { - t.Error("Okx SubmitOrder() error", err) + result, err := ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.Trigger + arg.TriggerPrice = 11999 + arg.TriggerPriceType = order.LastPrice + result, err = ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.ConditionalStop + arg.TriggerPrice = 11999 + arg.TriggerPriceType = order.IndexPrice + result, err = ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.Chase + _, err = ok.SubmitOrder(contextGenerate(), arg) + assert.ErrorIs(t, err, order.ErrUnknownTrackingMode) + + arg.TrackingMode = order.Percentage + _, err = ok.SubmitOrder(contextGenerate(), arg) + assert.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.TrackingValue = .5 + result, err = ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.TWAP + result, err = ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.TrailingStop + result, err = ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.OCO + _, err = ok.SubmitOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.RiskManagementModes = order.RiskManagementModes{ + TakeProfit: order.RiskManagement{ + Price: 11999, + LimitPrice: 12000, + }, + StopLoss: order.RiskManagement{ + Price: 10999, + LimitPrice: 11000, + }, } + result, err = ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) cp, err := currency.NewPairFromString("BTC-USDT-230630") - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - orderSubmission = &order.Submit{ + arg = &order.Submit{ Pair: cp, Exchange: ok.Name, - Side: order.Buy, + Side: order.Long, Type: order.Market, Amount: 1, ClientID: "hellomoto", AssetType: asset.Futures, MarginType: margin.Multi, } - _, err = ok.SubmitOrder(contextGenerate(), orderSubmission) - if err != nil { - t.Error("Okx SubmitOrder() error", err) - } -} + result, err = ok.SubmitOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) -func TestCancelOrder(t *testing.T) { - t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + pair, err := currency.NewPairFromString("BTC-USDT-SWAP_BTC-USDT-250328") + require.NoError(t, err) + + result, err = ok.SubmitOrder(contextGenerate(), &order.Submit{ + Pair: pair, + Exchange: ok.Name, + Side: order.Sell, + Type: order.Limit, + Price: 120000, + Amount: 1, + ClientID: "hellomoto", + AssetType: asset.Spread, + MarginType: margin.Multi, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} - var orderCancellation = &order.Cancel{ - OrderID: "1", +func TestCancelOrder(t *testing.T) { + t.Parallel() + var arg = &order.Cancel{ WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - Pair: currency.NewPair(currency.LTC, currency.BTC), - AssetType: asset.Spot, - } - if err := ok.CancelOrder(contextGenerate(), orderCancellation); err != nil { - t.Error(err) + AssetType: asset.Binary, } + err := ok.CancelOrder(contextGenerate(), arg) + require.ErrorIs(t, err, asset.ErrNotSupported) + + arg.AssetType = asset.Spot + err = ok.CancelOrder(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + arg.Pair = spotTP + err = ok.CancelOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + err = ok.CancelOrder(contextGenerate(), &order.Cancel{ + OrderID: "1", WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", Pair: spotTP, AssetType: asset.Spot}) + assert.NoError(t, err) + + err = ok.CancelOrder(contextGenerate(), &order.Cancel{ + Type: order.OCO, + OrderID: "1", WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", Pair: spotTP, AssetType: asset.Spot}) + assert.NoError(t, err) + + err = ok.CancelOrder(contextGenerate(), &order.Cancel{OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, AccountID: "1", + Pair: spreadTP, AssetType: asset.Spread}) + assert.NoError(t, err) } func TestCancelBatchOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.CancelBatchOrders(contextGenerate(), make([]order.Cancel, 21)) + require.ErrorIs(t, err, errExceedLimit) + _, err = ok.CancelBatchOrders(contextGenerate(), nil) + require.ErrorIs(t, err, order.ErrCancelOrderIsNil) + + arg := order.Cancel{ + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + AssetType: asset.Binary, + } + _, err = ok.CancelBatchOrders(contextGenerate(), []order.Cancel{arg}) + require.ErrorIs(t, err, asset.ErrNotSupported) + + arg.AssetType = asset.Spot + _, err = ok.CancelBatchOrders(contextGenerate(), []order.Cancel{arg}) + require.ErrorIs(t, err, currency.ErrCurrencyPairsEmpty) + + arg.Pair = spotTP + arg.Type = order.Liquidation + _, err = ok.CancelBatchOrders(contextGenerate(), []order.Cancel{arg}) + require.ErrorIs(t, err, order.ErrUnsupportedOrderType) + + arg.Type = order.Trigger + _, err = ok.CancelBatchOrders(contextGenerate(), []order.Cancel{arg}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + arg.Type = order.Limit + _, err = ok.CancelBatchOrders(contextGenerate(), []order.Cancel{arg}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) var orderCancellationParams = []order.Cancel{ { OrderID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - Pair: currency.NewPair(currency.LTC, currency.BTC), + Pair: spotTP, AssetType: asset.Spot, }, { OrderID: "1", WalletAddress: core.BitcoinDonationAddress, AccountID: "1", - Pair: currency.NewPair(currency.LTC, currency.BTC), + Pair: perpetualSwapTP, AssetType: asset.PerpetualSwap, }, + { + OrderID: "1", + WalletAddress: core.BitcoinDonationAddress, + AccountID: "1", + Type: order.Trigger, + Pair: spotTP, + AssetType: asset.Spot, + }, } - _, err := ok.CancelBatchOrders(contextGenerate(), orderCancellationParams) - if err != nil && !strings.Contains(err.Error(), "order does not exist.") { - t.Error("Okx CancelBatchOrders() error", err) - } + result, err := ok.CancelBatchOrders(contextGenerate(), orderCancellationParams) + require.NoError(t, err) + assert.NotNil(t, result) } func TestCancelAllOrders(t *testing.T) { t.Parallel() + _, err := ok.CancelAllOrders(contextGenerate(), &order.Cancel{AssetType: asset.Binary}) + require.ErrorIs(t, err, asset.ErrNotSupported) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelAllOrders(contextGenerate(), &order.Cancel{AssetType: asset.Spread}) + assert.NoError(t, err) + assert.NotNil(t, result) - if _, err := ok.CancelAllOrders(contextGenerate(), &order.Cancel{}); err != nil { - t.Errorf("%s CancelAllOrders() error: %v", ok.Name, err) - } + result, err = ok.CancelAllOrders(contextGenerate(), &order.Cancel{AssetType: asset.Futures}) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.CancelAllOrders(contextGenerate(), &order.Cancel{AssetType: asset.Spot}) + assert.NoError(t, err) + assert.NotNil(t, result) } func TestModifyOrder(t *testing.T) { t.Parallel() + _, err := ok.ModifyOrder(contextGenerate(), nil) + require.ErrorIs(t, err, order.ErrModifyOrderIsNil) + + arg := &order.Modify{} + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPairIsEmpty) + + arg.Pair = spotTP + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAssetNotSet) + + arg.AssetType = asset.Spot + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + arg.OrderID = "1234" + arg.Type = order.Liquidation + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrUnsupportedOrderType) + + arg.Type = order.Limit + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation) + + arg.Type = order.Trigger + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.Type = order.OCO + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + arg = &order.Modify{ + AssetType: asset.Spot, + Pair: spotTP, + OrderID: "1234", + Price: 123456.44, + Amount: 123, + } + result, err := ok.ModifyOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.Limit + result, err = ok.ModifyOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) - _, err := ok.ModifyOrder(contextGenerate(), + arg.Type = order.Trigger + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.TriggerPrice = 12345678 + _, err = ok.ModifyOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + arg.Type = order.OCO + _, err = ok.ModifyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.RiskManagementModes = order.RiskManagementModes{ + TakeProfit: order.RiskManagement{Price: 12345677}, + StopLoss: order.RiskManagement{Price: 12345667}, + } + result, err = ok.ModifyOrder(contextGenerate(), arg) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.ModifyOrder(contextGenerate(), &order.Modify{ - AssetType: asset.Spot, - Pair: currency.NewPair(currency.LTC, currency.BTC), + AssetType: asset.Spread, + Pair: spotTP, OrderID: "1234", Price: 123456.44, Amount: 123, }) - if err != nil { - t.Errorf("Okx ModifyOrder() error %v", err) - } + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOrderInfo(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - enabled, err := ok.GetEnabledPairs(asset.Spot) - if err != nil { - t.Error("couldn't find enabled tradable pairs") - } + require.NoError(t, err) if len(enabled) == 0 { t.SkipNow() } - _, err = ok.GetOrderInfo(contextGenerate(), - "123", enabled[0], asset.Futures) - if err != nil && !strings.Contains(err.Error(), "Order does not exist") { - t.Errorf("Okx GetOrderInfo() expecting %s, but found %v", "Order does not exist", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetOrderInfo(contextGenerate(), "123", enabled[0], asset.Futures) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.GetOrderInfo(contextGenerate(), "123", enabled[0], asset.Spread) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetDepositAddress(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetDepositAddress(contextGenerate(), currency.EMPTYCODE, "", "") + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) - if _, err := ok.GetDepositAddress(contextGenerate(), currency.BTC, "", ""); err != nil && !errors.Is(err, errDepositAddressNotFound) { - t.Error("Okx GetDepositAddress() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetDepositAddress(contextGenerate(), currency.BTC, core.BitcoinDonationAddress, "") + require.NoError(t, err) + assert.NotNil(t, result) } func TestWithdraw(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) withdrawCryptoRequest := withdraw.Request{ Exchange: ok.Name, Amount: 0.00000000001, @@ -2203,57 +3799,80 @@ func TestWithdraw(t *testing.T) { Address: core.BitcoinDonationAddress, }, } - if _, err := ok.WithdrawCryptocurrencyFunds(contextGenerate(), &withdrawCryptoRequest); err != nil { - t.Error("Okx WithdrawCryptoCurrencyFunds() error", err) + result, err := ok.WithdrawCryptocurrencyFunds(contextGenerate(), &withdrawCryptoRequest) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetPairFromInstrumentID(t *testing.T) { + t.Parallel() + instruments := []string{ + "BTC-USDT", + "BTC-USDT-SWAP", + "BTC-USDT-ER33234", } + dPair, err := ok.GetPairFromInstrumentID(instruments[0]) + require.NoError(t, err) + require.NotNil(t, dPair) + dPair, err = ok.GetPairFromInstrumentID(instruments[1]) + require.NoError(t, err) + require.NotNil(t, dPair) + dPair, err = ok.GetPairFromInstrumentID(instruments[2]) + require.NoError(t, err) + assert.NotNil(t, dPair) } func TestGetActiveOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - pair, err := currency.NewPairFromString("BTC-USD") - if err != nil { - t.Error(err) - } - var getOrdersRequest = order.MultiOrderRequest{ + require.NoError(t, err) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetActiveOrders(contextGenerate(), &order.MultiOrderRequest{ Type: order.Limit, - Pairs: currency.Pairs{pair, currency.NewPair(currency.USDT, currency.USD), currency.NewPair(currency.USD, currency.LTC)}, + Pairs: currency.Pairs{pair, spotTP, currency.NewPair(currency.USD, currency.LTC)}, AssetType: asset.Spot, Side: order.Buy, - } - if _, err := ok.GetActiveOrders(contextGenerate(), &getOrdersRequest); err != nil { - t.Error("Okx GetActiveOrders() error", err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.GetActiveOrders(contextGenerate(), &order.MultiOrderRequest{ + Type: order.Limit, + Pairs: currency.Pairs{pair, spotTP, currency.NewPair(currency.USD, currency.LTC)}, + AssetType: asset.Spread, + Side: order.Buy, + }) + assert.NoError(t, err) + assert.NotNil(t, result) } func TestGetOrderHistory(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - var getOrdersRequest = order.MultiOrderRequest{ Type: order.AnyType, AssetType: asset.Spot, Side: order.Buy, } _, err := ok.GetOrderHistory(contextGenerate(), &getOrdersRequest) - if err == nil { - t.Errorf("Okx GetOrderHistory() Expected: %v. received nil", err) - } else if !errors.Is(err, errMissingAtLeast1CurrencyPair) { - t.Errorf("Okx GetOrderHistory() Expected: %v, but found %v", errMissingAtLeast1CurrencyPair, err) - } - getOrdersRequest.Pairs = []currency.Pair{ - currency.NewPair(currency.LTC, - currency.BTC)} - if _, err := ok.GetOrderHistory(contextGenerate(), &getOrdersRequest); err != nil { - t.Error("Okx GetOrderHistory() error", err) - } + require.ErrorIs(t, err, currency.ErrCurrencyPairsEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + getOrdersRequest.Pairs = []currency.Pair{currency.NewPair(currency.LTC, currency.BTC)} + result, err := ok.GetOrderHistory(contextGenerate(), &getOrdersRequest) + assert.NoError(t, err) + assert.NotNil(t, result) + + getOrdersRequest.AssetType = asset.Spread + getOrdersRequest.Type = order.Market + result, err = ok.GetOrderHistory(contextGenerate(), &getOrdersRequest) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetFeeByType(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if _, err := ok.GetFeeByType(contextGenerate(), &exchange.FeeBuilder{ + result, err := ok.GetFeeByType(contextGenerate(), &exchange.FeeBuilder{ Amount: 1, FeeType: exchange.CryptocurrencyTradeFee, Pair: currency.NewPairWithDelimiter(currency.BTC.String(), @@ -2262,18 +3881,16 @@ func TestGetFeeByType(t *testing.T) { PurchasePrice: 1, FiatCurrency: currency.USD, BankTransactionType: exchange.WireTransfer, - }); err != nil { - t.Errorf("%s GetFeeByType() error %v", ok.Name, err) - } + }) + require.NoError(t, err) + assert.NotNil(t, result) } func TestValidateAPICredentials(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if err := ok.ValidateAPICredentials(contextGenerate(), asset.Spot); err != nil { - t.Errorf("%s ValidateAPICredentials() error %v", ok.Name, err) - } + err := ok.ValidateAPICredentials(contextGenerate(), asset.Spot) + assert.NoError(t, err) } func TestGetHistoricCandles(t *testing.T) { @@ -2281,1397 +3898,2229 @@ func TestGetHistoricCandles(t *testing.T) { pair := currency.NewPair(currency.BTC, currency.USDT) startTime := time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC) endTime := startTime.AddDate(0, 0, 100) - _, err := ok.GetHistoricCandles(contextGenerate(), pair, asset.Spot, kline.OneDay, startTime, endTime) - if err != nil { - t.Fatal(err) - } + _, err := ok.GetHistoricCandles(contextGenerate(), pair, asset.Spot, kline.Interval(time.Hour*4), startTime, endTime) + require.ErrorIs(t, err, kline.ErrRequestExceedsExchangeLimits) - _, err = ok.GetHistoricCandles(contextGenerate(), pair, asset.Spot, kline.Interval(time.Hour*4), startTime, endTime) - if !errors.Is(err, kline.ErrRequestExceedsExchangeLimits) { - t.Errorf("received: '%v' but expected: '%v'", err, kline.ErrRequestExceedsExchangeLimits) - } + result, err := ok.GetHistoricCandles(contextGenerate(), pair, asset.Spot, kline.OneDay, startTime, endTime) + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetHistoricCandlesExtended(t *testing.T) { t.Parallel() currencyPair := currency.NewPair(currency.BTC, currency.USDT) - _, err := ok.GetHistoricCandlesExtended(contextGenerate(), currencyPair, asset.Spot, kline.OneMin, time.Now().Add(-time.Hour), time.Now()) - if err != nil { - t.Errorf("%s GetHistoricCandlesExtended() error: %v", ok.Name, err) - } + result, err := ok.GetHistoricCandlesExtended(contextGenerate(), currencyPair, asset.Spot, kline.OneMin, time.Now().Add(-time.Hour), time.Now()) + require.NoError(t, err) + assert.NotNil(t, result) } -const wsInstrumentPushData = `{"arg": {"channel": "instruments","instType": "FUTURES"},"data": [{"instType": "FUTURES","instId": "BTC-USD-191115","uly": "BTC-USD","category": "1","baseCcy": "","quoteCcy": "","settleCcy": "BTC","ctVal": "10","ctMult": "1","ctValCcy": "USD","optType": "","stk": "","listTime": "","expTime": "","tickSz": "0.01","lotSz": "1","minSz": "1","ctType": "linear","alias": "this_week","state": "live","maxLmtSz":"10000","maxMktSz":"99999","maxTwapSz":"99999","maxIcebergSz":"99999","maxTriggerSz":"9999","maxStopSz":"9999"}]}` - -func TestWSInstruments(t *testing.T) { +func TestCalculateUpdateOrderbookChecksum(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(wsInstrumentPushData)); err != nil { - t.Errorf("%s Websocket Instruments Push Data error %v", ok.Name, err) - } -} + var orderbookBase orderbook.Base + err := json.Unmarshal([]byte(calculateOrderbookChecksumUpdateOrderbookJSON), &orderbookBase) + require.NoError(t, err) -var tickerChannelPushData = `{"arg": {"channel": "tickers","instId": "%v"},"data": [{"instType": "SWAP","instId": "%v","last": "9999.99","lastSz": "0.1","askPx": "9999.99","askSz": "11","bidPx": "8888.88","bidSz": "5","open24h": "9000","high24h": "10000","low24h": "8888.88","volCcy24h": "2222","vol24h": "2222","sodUtc0": "2222","sodUtc8": "2222","ts": "1597026383085"}]}` + err = ok.CalculateUpdateOrderbookChecksum(&orderbookBase, 2832680552) + assert.NoError(t, err) +} -func TestTickerChannel(t *testing.T) { +func TestOrderPushData(t *testing.T) { t.Parallel() - curr := ok.CurrencyPairs.Pairs[asset.PerpetualSwap].Enabled[0] - if err := ok.WsHandleData([]byte(fmt.Sprintf(tickerChannelPushData, curr, curr))); err != nil { - t.Error("Okx TickerChannel push data error", err) + ok := new(Okx) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + require.NoError(t, testexch.Setup(ok), "Test instance Setup must not error") + testexch.FixtureToDataHandler(t, "testdata/wsOrders.json", ok.WsHandleData) + close(ok.Websocket.DataHandler) + require.Len(t, ok.Websocket.DataHandler, 4, "Should see 4 orders") + for resp := range ok.Websocket.DataHandler { + switch v := resp.(type) { + case *order.Detail: + switch len(ok.Websocket.DataHandler) { + case 3: + require.Equal(t, "452197707845865472", v.OrderID, "OrderID") + require.Equal(t, "HamsterParty14", v.ClientOrderID, "ClientOrderID") + require.Equal(t, asset.Spot, v.AssetType, "AssetType") + require.Equal(t, order.Sell, v.Side, "Side") + require.Equal(t, order.Filled, v.Status, "Status") + require.Equal(t, order.Limit, v.Type, "Type") + require.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "Pair") + require.Equal(t, 31527.1, v.AverageExecutedPrice, "AverageExecutedPrice") + require.Equal(t, time.UnixMilli(1654084334977), v.Date, "Date") + require.Equal(t, time.UnixMilli(1654084353263), v.CloseTime, "CloseTime") + require.Equal(t, 0.001, v.Amount, "Amount") + require.Equal(t, 0.001, v.ExecutedAmount, "ExecutedAmount") + require.Equal(t, 0.000, v.RemainingAmount, "RemainingAmount") + require.Equal(t, 31527.1, v.Price, "Price") + require.Equal(t, 0.02522168, v.Fee, "Fee") + require.Equal(t, currency.USDT, v.FeeAsset, "FeeAsset") + case 2: + require.Equal(t, "620258920632008725", v.OrderID, "OrderID") + require.Equal(t, asset.Spot, v.AssetType, "AssetType") + require.Equal(t, order.Market, v.Type, "Type") + require.Equal(t, order.Sell, v.Side, "Side") + require.Equal(t, order.Active, v.Status, "Status") + require.Equal(t, 0.0, v.Amount, "Amount should be 0 for a market sell") + require.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + case 1: + require.Equal(t, "620258920632008725", v.OrderID, "OrderID") + require.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + require.Equal(t, 0.00038127046945832905, v.Amount, "Amount") + require.Equal(t, 0.010000249968, v.Fee, "Fee") + require.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + require.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + require.Equal(t, order.PartiallyFilled, v.Status, "Status") + case 0: + require.Equal(t, "620258920632008725", v.OrderID, "OrderID") + require.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") + require.Equal(t, 0.010000249968, v.Fee, "Fee") + require.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") + require.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") + require.Equal(t, 0.00038128, v.Amount, "Amount should be derived because order filled") + require.Equal(t, order.Filled, v.Status, "Status") + } + case error: + t.Error(v) + default: + t.Errorf("Got unexpected data: %T %v", v, v) + } } } -const openInterestChannelPushData = `{"arg": {"channel": "open-interest","instId": "LTC-USD-SWAP"},"data": [{"instType": "SWAP","instId": "LTC-USD-SWAP","oi": "5000","oiCcy": "555.55","ts": "1597026383085"}]}` - -func TestOpenInterestPushData(t *testing.T) { - t.Parallel() - if err := ok.WsHandleData([]byte(openInterestChannelPushData)); err != nil { - t.Error("Okx Open Interest Push Data error", err) +var pushDataMap = map[string]string{ + "Algo Orders": `{"arg": {"channel": "orders-algo","uid": "77982378738415879","instType": "FUTURES","instId": "BTC-USD-200329"},"data": [{"instType": "FUTURES","instId": "BTC-USD-200329","ordId": "312269865356374016","ccy": "BTC","algoId": "1234","px": "999","sz": "3","tdMode": "cross","tgtCcy": "","notionalUsd": "","ordType": "trigger","side": "buy","posSide": "long","state": "live","lever": "20","tpTriggerPx": "","tpTriggerPxType": "","tpOrdPx": "","slTriggerPx": "","slTriggerPxType": "","triggerPx": "99","triggerPxType": "last","ordPx": "12","actualSz": "","actualPx": "","tag": "adadadadad","actualSide": "","triggerTime": "1597026383085","cTime": "1597026383000"}]}`, + "Advanced Algo Order": `{"arg": {"channel":"algo-advance","uid": "77982378738415879","instType":"SPOT","instId":"BTC-USDT"},"data":[{"actualPx":"","actualSide":"","actualSz":"0","algoId":"355056228680335360","cTime":"1630924001545","ccy":"","count":"1","instId":"BTC-USDT","instType":"SPOT","lever":"0","notionalUsd":"","ordPx":"","ordType":"iceberg","pTime":"1630924295204","posSide":"net","pxLimit":"10","pxSpread":"1","pxVar":"","side":"buy","slOrdPx":"","slTriggerPx":"","state":"pause","sz":"0.1","szLimit":"0.1","tdMode":"cash","timeInterval":"","tpOrdPx":"","tpTriggerPx":"","tag": "adadadadad","triggerPx":"","triggerTime":"","callbackRatio":"","callbackSpread":"","activePx":"","moveTriggerPx":""}]}`, + "Position Risk": `{"arg": {"channel": "liquidation-warning","uid": "77982378738415879","instType": "FUTURES"},"data": [{"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-210430","instType":"FUTURES","interest":"0","last":"2566.22","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}, {"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-SWAP","instType":"SWAP","interest":"0","last":"2566.22","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}]}`, + "Account Greeks": `{"arg": {"channel": "account-greeks","ccy": "BTC"},"data": [{"thetaBS": "","thetaPA":"","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","vegaBS":"","vegaPA":"","ccy":"BTC","ts":"1620282889345"}]}`, + "RFQs": `{"arg": {"channel": "account-greeks","ccy": "BTC"},"data": [{"thetaBS": "","thetaPA":"","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","vegaBS":"","vegaPA":"","ccy":"BTC","ts":"1620282889345"}]}`, + "Accounts": `{"arg": {"channel": "account","ccy": "BTC","uid": "77982378738415879"}, "data": [{"uTime": "1597026383085","totalEq": "41624.32","isoEq": "3624.32","adjEq": "41624.32","ordFroz": "0","imr": "4162.33","mmr": "4","notionalUsd": "","mgnRatio": "41624.32","details": [{"availBal": "","availEq": "1","ccy": "BTC","cashBal": "1","uTime": "1617279471503","disEq": "50559.01","eq": "1","eqUsd": "45078.3790756226851775","frozenBal": "0","interest": "0","isoEq": "0","liab": "0","maxLoan": "","mgnRatio": "","notionalLever": "0.0022195262185864","ordFrozen": "0","upl": "0","uplLiab": "0","crossLiab": "0","isoLiab": "0","coinUsdPrice": "60000","stgyEq":"0","spotInUseAmt":"","isoUpl":""}]}]}`, + "Quotes": `{"arg": {"channel":"quotes"},"data":[{"validUntil":"1608997227854","uTime":"1608267227834","cTime":"1608267227834","legs":[{"px":"0.0023","sz":"25.0","instId":"BTC-USD-220114-25000-C","side":"sell","tgtCcy":""},{"px":"0.0045","sz":"25","instId":"BTC-USD-220114-35000-C","side":"buy","tgtCcy":""}],"quoteId":"25092","rfqId":"18753","traderCode":"SATS","quoteSide":"sell","state":"canceled","clQuoteId":""}]}`, + "Structure Block Trades": `{"arg": {"channel":"struc-block-trades"},"data":[{"cTime":"1608267227834","rfqId":"18753","clRfqId":"","quoteId":"25092","clQuoteId":"","blockTdId":"180184","tTraderCode":"ANAND","mTraderCode":"WAGMI","legs":[{"px":"0.0023","sz":"25.0","instId":"BTC-USD-20220630-60000-C","side":"sell","fee":"0.1001","feeCcy":"BTC","tradeId":"10211","tgtCcy":""},{"px":"0.0033","sz":"25","instId":"BTC-USD-20220630-50000-C","side":"buy","fee":"0.1001","feeCcy":"BTC","tradeId":"10212","tgtCcy":""}]}]}`, + "Spot Grid Algo Orders": `{"arg": {"channel": "grid-orders-spot","instType": "ANY"},"data": [{"algoId": "448965992920907776","algoOrdType": "grid","annualizedRate": "0","arbitrageNum": "0","baseSz": "0","cTime": "1653313834104","cancelType": "0","curBaseSz": "0.001776289214","curQuoteSz": "46.801755866","floatProfit": "-0.4953878967772","gridNum": "6","gridProfit": "0","instId": "BTC-USDC","instType": "SPOT","investment": "100","maxPx": "33444.8","minPx": "24323.5","pTime": "1653476023742","perMaxProfitRate": "0.060375293181491054543","perMinProfitRate": "0.0455275366818586","pnlRatio": "0","quoteSz": "100","runPx": "30478.1","runType": "1","singleAmt": "0.00059261","slTriggerPx": "","state": "running","stopResult": "0","stopType": "0","totalAnnualizedRate": "-0.9643551057262827","totalPnl": "-0.4953878967772","tpTriggerPx": "","tradeNum": "3","triggerTime": "1653378736894","uTime": "1653378736894"}]}`, + "Contract Grid Algo Orders": `{"arg": {"channel": "grid-orders-contract","instType": "ANY"},"data": [{"actualLever": "1.02","algoId": "449327675342323712","algoOrdType": "contract_grid","annualizedRate": "0.7572437878956523","arbitrageNum": "1","basePos": true,"cTime": "1653400065912","cancelType": "0","direction": "long","eq": "10129.419829834853","floatProfit": "109.537858234853","gridNum": "50","gridProfit": "19.8819716","instId": "BTC-USDT-SWAP","instType": "SWAP","investment": "10000","lever": "5","liqPx": "603.2149534767834","maxPx": "100000","minPx": "10","pTime": "1653484573918","perMaxProfitRate": "995.7080916791230692","perMinProfitRate": "0.0946277854875634","pnlRatio": "0.0129419829834853","runPx": "29216.3","runType": "1","singleAmt": "1","slTriggerPx": "","state": "running","stopType": "0","sz": "10000","tag": "","totalAnnualizedRate": "4.929207431970923","totalPnl": "129.419829834853","tpTriggerPx": "","tradeNum": "37","triggerTime": "1653400066940","uTime": "1653484573589","uly": "BTC-USDT"}]}`, + "Grid Positions": `{"arg": {"channel": "grid-positions","uid": "44705892343619584","algoId": "449327675342323712"},"data": [{"adl": "1","algoId": "449327675342323712","avgPx": "29181.4638888888888895","cTime": "1653400065917","ccy": "USDT","imr": "2089.2690000000002","instId": "BTC-USDT-SWAP","instType": "SWAP","last": "29852.7","lever": "5","liqPx": "604.7617536513744","markPx": "29849.7","mgnMode": "cross","mgnRatio": "217.71740878394456","mmr": "41.78538","notionalUsd": "10435.794191550001","pTime": "1653536068723","pos": "35","posSide": "net","uTime": "1653445498682","upl": "232.83263888888962","uplRatio": "0.1139826489932205"}]}`, + "Grid Sub Orders": `{"arg": {"channel": "grid-sub-orders","uid": "44705892343619584","algoId": "449327675342323712"},"data": [{"accFillSz": "0","algoId": "449327675342323712","algoOrdType": "contract_grid","avgPx": "0","cTime": "1653445498664","ctVal": "0.01","fee": "0","feeCcy": "USDT","groupId": "-1","instId": "BTC-USDT-SWAP","instType": "SWAP","lever": "5","ordId": "449518234142904321","ordType": "limit","pTime": "1653486524502","pnl": "","posSide": "net","px": "28007.2","side": "buy","state": "live","sz": "1","tag":"","tdMode": "cross","uTime": "1653445498674"}]}`, + "Instrument": `{"arg": {"channel": "instruments","instType": "FUTURES"},"data": [{"instType": "FUTURES","instId": "BTC-USD-191115","uly": "BTC-USD","category": "1","baseCcy": "","quoteCcy": "","settleCcy": "BTC","ctVal": "10","ctMult": "1","ctValCcy": "USD","optType": "","stk": "","listTime": "","expTime": "","tickSz": "0.01","lotSz": "1","minSz": "1","ctType": "linear","alias": "this_week","state": "live","maxLmtSz":"10000","maxMktSz":"99999","maxTwapSz":"99999","maxIcebergSz":"99999","maxTriggerSz":"9999","maxStopSz":"9999"}]}`, + "Open Interest": `{"arg": {"channel": "open-interest","instId": "LTC-USD-SWAP"},"data": [{"instType": "SWAP","instId": "LTC-USD-SWAP","oi": "5000","oiCcy": "555.55","ts": "1597026383085"}]}`, + "Trade": `{"arg": {"channel": "trades","instId": "BTC-USDT"},"data": [{"instId": "BTC-USDT","tradeId": "130639474","px": "42219.9","sz": "0.12060306","side": "buy","ts": "1630048897897"}]}`, + "Estimated Delivery And Exercise Price": `{"arg": {"args": "estimated-price","instType": "FUTURES","uly": "BTC-USD"},"data": [{"instType": "FUTURES","instId": "BTC-USD-170310","settlePx": "200","ts": "1597026383085"}]}`, + "Mark Price": `{"arg": {"channel": "mark-price","instId": "LTC-USD-190628"},"data": [{"instType": "FUTURES","instId": "LTC-USD-190628","markPx": "0.1","ts": "1597026383085"}]}`, + "Mark Price Candlestick": `{"arg": {"channel": "mark-price-candle1D","instId": "BTC-USD-190628"},"data": [["1597026383085", "3.721", "3.743", "3.677", "3.708"],["1597026383085", "3.731", "3.799", "3.494", "3.72"]]}`, + "Price Limit": `{"arg": {"channel": "price-limit","instId": "LTC-USD-190628"},"data": [{"instId": "LTC-USD-190628","buyLmt": "200","sellLmt": "300","ts": "1597026383085"}]}`, + "Test Snapshot Orderbook": `{"arg": {"channel":"books","instId":"BTC-USDT"},"action":"snapshot","data":[{"asks":[["0.07026","5","0","1"],["0.07027","765","0","3"],["0.07028","110","0","1"],["0.0703","1264","0","1"],["0.07034","280","0","1"],["0.07035","2255","0","1"],["0.07036","28","0","1"],["0.07037","63","0","1"],["0.07039","137","0","2"],["0.0704","48","0","1"],["0.07041","32","0","1"],["0.07043","3985","0","1"],["0.07057","257","0","1"],["0.07058","7870","0","1"],["0.07059","161","0","1"],["0.07061","4539","0","1"],["0.07068","1438","0","3"],["0.07088","3162","0","1"],["0.07104","99","0","1"],["0.07108","5018","0","1"],["0.07115","1540","0","1"],["0.07129","5080","0","1"],["0.07145","1512","0","1"],["0.0715","5016","0","1"],["0.07171","5026","0","1"],["0.07192","5062","0","1"],["0.07197","1517","0","1"],["0.0726","1511","0","1"],["0.07314","10376","0","1"],["0.07354","1","0","1"],["0.07466","10277","0","1"],["0.07626","269","0","1"],["0.07636","269","0","1"],["0.0809","1","0","1"],["0.08899","1","0","1"],["0.09789","1","0","1"],["0.10768","1","0","1"]],"bids":[["0.07014","56","0","2"],["0.07011","608","0","1"],["0.07009","110","0","1"],["0.07006","1264","0","1"],["0.07004","2347","0","3"],["0.07003","279","0","1"],["0.07001","52","0","1"],["0.06997","91","0","1"],["0.06996","4242","0","2"],["0.06995","486","0","1"],["0.06992","161","0","1"],["0.06991","63","0","1"],["0.06988","7518","0","1"],["0.06976","186","0","1"],["0.06975","71","0","1"],["0.06973","1086","0","1"],["0.06961","513","0","2"],["0.06959","4603","0","1"],["0.0695","186","0","1"],["0.06946","3043","0","1"],["0.06939","103","0","1"],["0.0693","5053","0","1"],["0.06909","5039","0","1"],["0.06888","5037","0","1"],["0.06886","1526","0","1"],["0.06867","5008","0","1"],["0.06846","5065","0","1"],["0.06826","1572","0","1"],["0.06801","1565","0","1"],["0.06748","67","0","1"],["0.0674","111","0","1"],["0.0672","10038","0","1"],["0.06652","1","0","1"],["0.06625","1526","0","1"],["0.06619","10924","0","1"],["0.05986","1","0","1"],["0.05387","1","0","1"],["0.04848","1","0","1"],["0.04363","1","0","1"]],"ts":"1659792392540","checksum":-1462286744}]}`, + "Options Trades": `{"arg": {"channel": "option-trades", "instType": "OPTION", "instFamily": "BTC-USD" }, "data": [ { "fillVol": "0.5066007836914062", "fwdPx": "16469.69928595038", "idxPx": "16537.2", "instFamily": "BTC-USD", "instId": "BTC-USD-230224-18000-C", "markPx": "0.04690107010619562", "optType": "C", "px": "0.045", "side": "sell", "sz": "2", "tradeId": "38", "ts": "1672286551080" } ] }`, + "Public Block Trades": `{"arg": {"channel":"public-block-trades", "instId":"BTC-USD-231020-5000-P" }, "data":[ { "fillVol":"5", "fwdPx":"26808.16", "idxPx":"27222.5", "instId":"BTC-USD-231020-5000-P", "markPx":"0.0022406326071111", "px":"0.0048", "side":"buy", "sz":"1", "tradeId":"633971452580106242", "ts":"1697422572972"}]}`, + "Option Summary": `{"arg": {"channel": "opt-summary","uly": "BTC-USD"},"data": [{"instType": "OPTION","instId": "BTC-USD-200103-5500-C","uly": "BTC-USD","delta": "0.7494223636","gamma": "-0.6765419039","theta": "-0.0000809873","vega": "0.0000077307","deltaBS": "0.7494223636","gammaBS": "-0.6765419039","thetaBS": "-0.0000809873","vegaBS": "0.0000077307","realVol": "0","bidVol": "","askVol": "1.5625","markVol": "0.9987","lever": "4.0342","fwdPx": "39016.8143629068452065","ts": "1597026383085"}]}`, + "Funding Rate": `{"arg": {"channel": "funding-rate","instId": "BTC-USD-SWAP"},"data": [{"instType": "SWAP","instId": "BTC-USD-SWAP","fundingRate": "0.018","nextFundingRate": "","fundingTime": "1597026383085"}]}`, + "Index Candlestick": `{"arg": {"channel": "index-candle30m","instId": "BTC-USDT"},"data": [["1597026383085", "3811.31", "3811.31", "3811.31", "3811.31"]]}`, + "Index Ticker": `{"arg": {"channel": "index-tickers","instId": "BTC-USDT"},"data": [{"instId": "BTC-USDT","idxPx": "0.1","high24h": "0.5","low24h": "0.1","open24h": "0.1","sodUtc0": "0.1","sodUtc8": "0.1","ts": "1597026383085"}]}`, + "Status": `{"arg": {"channel": "status"},"data": [{"title": "Spot System Upgrade","state": "scheduled","begin": "1610019546","href": "","end": "1610019546","serviceType": "1","system": "classic","scheDesc": "","ts": "1597026383085"}]}`, + "Public Struct Block Trades": `{"arg": {"channel":"public-struc-block-trades"},"data":[{"cTime":"1608267227834","blockTdId":"1802896","legs":[{"px":"0.323","sz":"25.0","instId":"BTC-USD-20220114-13250-C","side":"sell","tradeId":"15102"},{"px":"0.666","sz":"25","instId":"BTC-USD-20220114-21125-C","side":"buy","tradeId":"15103"}]}]}`, + "Block Ticker": `{"arg": {"channel": "block-tickers"},"data": [{"instType": "SWAP","instId": "LTC-USD-SWAP","volCcy24h": "0","vol24h": "0","ts": "1597026383085"}]}`, + "Account": `{"arg": {"channel": "block-tickers"},"data": [{"instType": "SWAP","instId": "LTC-USD-SWAP","volCcy24h": "0","vol24h": "0","ts": "1597026383085"}]}`, + "Position": `{"arg": {"channel":"positions","instType":"FUTURES"},"data":[{"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-210430","instType":"FUTURES","interest":"0","last":"2566.22","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}]}`, + "Position Data With Underlying": `{"arg": {"channel": "positions","uid": "77982378738415879","instType": "FUTURES"},"data": [{"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-210430","instType":"FUTURES","interest":"0","last":"2566.22","usdPx":"","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}, {"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-SWAP","instType":"SWAP","interest":"0","last":"2566.22","usdPx":"","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}]}`, + "Balance And Position": `{"arg": {"channel": "balance_and_position","uid": "77982378738415879"},"data": [{"pTime": "1597026383085","eventType": "snapshot","balData": [{"ccy": "BTC","cashBal": "1","uTime": "1597026383085"}],"posData": [{"posId": "1111111111","tradeId": "2","instId": "BTC-USD-191018","instType": "FUTURES","mgnMode": "cross","posSide": "long","pos": "10","ccy": "BTC","posCcy": "","avgPx": "3320","uTIme": "1597026383085"}]}]}`, + "Deposit Info Details": `{"arg": {"channel": "deposit-info", "uid": "289320****60975104" }, "data": [{ "actualDepBlkConfirm": "0", "amt": "1", "areaCodeFrom": "", "ccy": "USDT", "chain": "USDT-TRC20", "depId": "88165462", "from": "", "fromWdId": "", "pTime": "1674103661147", "state": "0", "subAcct": "test", "to": "TEhFAqpuHa3LY*****8ByNoGnrmexeGMw", "ts": "1674103661123", "txId": "bc5376817*****************dbb0d729f6b", "uid": "289320****60975104" }] }`, + "Withdrawal Info Details": `{"arg": {"channel": "deposit-info", "uid": "289320****60975104" }, "data": [{ "actualDepBlkConfirm": "0", "amt": "1", "areaCodeFrom": "", "ccy": "USDT", "chain": "USDT-TRC20", "depId": "88165462", "from": "", "fromWdId": "", "pTime": "1674103661147", "state": "0", "subAcct": "test", "to": "TEhFAqpuHa3LY*****8ByNoGnrmexeGMw", "ts": "1674103661123", "txId": "bc5376817*****************dbb0d729f6b", "uid": "289320****60975104" }] }`, + "Recurring Buy Order": `{"arg": {"channel": "algo-recurring-buy", "instType": "SPOT", "uid": "447*******584" }, "data": [{ "algoClOrdId": "", "algoId": "644497312047435776", "algoOrdType": "recurring", "amt": "100", "cTime": "1699932133373", "cycles": "0", "instType": "SPOT", "investmentAmt": "0", "investmentCcy": "USDC", "mktCap": "0", "nextInvestTime": "1699934415300", "pTime": "1699933314691", "period": "hourly", "pnlRatio": "0", "recurringDay": "", "recurringHour": "1", "recurringList": [{ "avgPx": "0", "ccy": "BTC", "profit": "0", "px": "36482", "ratio": "0.2", "totalAmt": "0" }, { "avgPx": "0", "ccy": "ETH", "profit": "0", "px": "2057.54", "ratio": "0.8", "totalAmt": "0" }], "recurringTime": "12", "state": "running", "stgyName": "stg1", "tag": "", "timeZone": "8", "totalAnnRate": "0", "totalPnl": "0", "uTime": "1699932136249" }] }`, + "Liquidation Orders": `{"arg": {"channel": "liquidation-orders", "instType": "SWAP" }, "data": [ { "details": [ { "bkLoss": "0", "bkPx": "0.007831", "ccy": "", "posSide": "short", "side": "buy", "sz": "13", "ts": "1692266434010" } ], "instFamily": "IOST-USDT", "instId": "IOST-USDT-SWAP", "instType": "SWAP", "uly": "IOST-USDT"}]}`, + "Economic Calendar": `{"arg": {"channel": "economic-calendar" }, "data": [ { "calendarId": "319275", "date": "1597026383085", "region": "United States", "category": "Manufacturing PMI", "event": "S&P Global Manufacturing PMI Final", "refDate": "1597026383085", "actual": "49.2", "previous": "47.3", "forecast": "49.3", "importance": "2", "prevInitial": "", "ccy": "", "unit": "", "ts": "1698648096590" } ] }`, + "Failure": `{ "event": "error", "code": "60012", "msg": "Invalid request: {\"op\": \"subscribe\", \"args\":[{ \"channel\" : \"block-tickers\", \"instId\" : \"LTC-USD-200327\"}]}", "connId": "a4d3ae55" }`, +} + +func TestPushData(t *testing.T) { + t.Parallel() + var err error + for x := range pushDataMap { + err = ok.WsHandleData([]byte(pushDataMap[x])) + require.NoErrorf(t, err, "Okx %s error %v", x, err) + } +} + +func TestPushDataDynamic(t *testing.T) { + t.Parallel() + dataMap := map[string]string{ + "Ticker": `{"arg": {"channel": "tickers","instId": "BTC-USD-SWAP"},"data": [{"instType": "SWAP","instId": "BTC-USD-SWAP","last": "9999.99","lastSz": "0.1","askPx": "9999.99","askSz": "11","bidPx": "8888.88","bidSz": "5","open24h": "9000","high24h": "10000","low24h": "8888.88","volCcy24h": "2222","vol24h": "2222","sodUtc0": "2222","sodUtc8": "2222","ts": "1597026383085"}]}`, + "Candlesticks": `{"arg": {"channel": "candle1D","instId": "BTC-USD-SWAP"},"data": [["1597026383085","8533.02","8553.74","8527.17","8548.26","45247","529.5858061"]]}`, + "Snapshot OrderBook": `{"arg":{"channel":"books","instId":"BTC-USD-SWAP"},"action":"snapshot","data":[{"asks":[["0.07026","5","0","1"],["0.07027","765","0","3"],["0.07028","110","0","1"],["0.0703","1264","0","1"],["0.07034","280","0","1"],["0.07035","2255","0","1"],["0.07036","28","0","1"],["0.07037","63","0","1"],["0.07039","137","0","2"],["0.0704","48","0","1"],["0.07041","32","0","1"],["0.07043","3985","0","1"],["0.07057","257","0","1"],["0.07058","7870","0","1"],["0.07059","161","0","1"],["0.07061","4539","0","1"],["0.07068","1438","0","3"],["0.07088","3162","0","1"],["0.07104","99","0","1"],["0.07108","5018","0","1"],["0.07115","1540","0","1"],["0.07129","5080","0","1"],["0.07145","1512","0","1"],["0.0715","5016","0","1"],["0.07171","5026","0","1"],["0.07192","5062","0","1"],["0.07197","1517","0","1"],["0.0726","1511","0","1"],["0.07314","10376","0","1"],["0.07354","1","0","1"],["0.07466","10277","0","1"],["0.07626","269","0","1"],["0.07636","269","0","1"],["0.0809","1","0","1"],["0.08899","1","0","1"],["0.09789","1","0","1"],["0.10768","1","0","1"]],"bids":[["0.07014","56","0","2"],["0.07011","608","0","1"],["0.07009","110","0","1"],["0.07006","1264","0","1"],["0.07004","2347","0","3"],["0.07003","279","0","1"],["0.07001","52","0","1"],["0.06997","91","0","1"],["0.06996","4242","0","2"],["0.06995","486","0","1"],["0.06992","161","0","1"],["0.06991","63","0","1"],["0.06988","7518","0","1"],["0.06976","186","0","1"],["0.06975","71","0","1"],["0.06973","1086","0","1"],["0.06961","513","0","2"],["0.06959","4603","0","1"],["0.0695","186","0","1"],["0.06946","3043","0","1"],["0.06939","103","0","1"],["0.0693","5053","0","1"],["0.06909","5039","0","1"],["0.06888","5037","0","1"],["0.06886","1526","0","1"],["0.06867","5008","0","1"],["0.06846","5065","0","1"],["0.06826","1572","0","1"],["0.06801","1565","0","1"],["0.06748","67","0","1"],["0.0674","111","0","1"],["0.0672","10038","0","1"],["0.06652","1","0","1"],["0.06625","1526","0","1"],["0.06619","10924","0","1"],["0.05986","1","0","1"],["0.05387","1","0","1"],["0.04848","1","0","1"],["0.04363","1","0","1"]],"ts":"1659792392540","checksum":-1462286744}]}`, + } + var err error + for x := range dataMap { + err = ok.WsHandleData([]byte(dataMap[x])) + require.NoError(t, err) } } -var candlesticksPushData = `{"arg": {"channel": "candle1D","instId": "%v"},"data": [["1597026383085","8533.02","8553.74","8527.17","8548.26","45247","529.5858061"]]}` - -func TestCandlestickPushData(t *testing.T) { +func TestGetHistoricTrades(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(fmt.Sprintf(candlesticksPushData, ok.CurrencyPairs.Pairs[asset.Futures].Enabled[0]))); err != nil { - t.Error("Okx Candlestick Push Data error", err) - } + result, err := ok.GetHistoricTrades(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot, time.Now().Add(-time.Minute*4), time.Now().Add(-time.Minute*2)) + require.NoError(t, err) + assert.NotNil(t, result) } -const tradePushDataJSON = `{"arg": {"channel": "trades","instId": "BTC-USDT"},"data": [{"instId": "BTC-USDT","tradeId": "130639474","px": "42219.9","sz": "0.12060306","side": "buy","ts": "1630048897897"}]}` - -func TestTradePushData(t *testing.T) { - t.Parallel() - if err := ok.WsHandleData([]byte(tradePushDataJSON)); err != nil { - t.Error("Okx Trade Push Data error", err) +func setupWS() { + if !ok.Websocket.IsEnabled() { + return + } + if !sharedtestvalues.AreAPICredentialsSet(ok) { + ok.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + err := ok.WsConnect() + if err != nil { + log.Fatal(err) } } -const estimatedDeliveryAndExercisePricePushDataJSON = `{"arg": {"args": "estimated-price","instType": "FUTURES","uly": "BTC-USD"},"data": [{"instType": "FUTURES","instId": "BTC-USD-170310","settlePx": "200","ts": "1597026383085"}]}` +// ************************** Public Channel Subscriptions ***************************** -func TestEstimatedDeliveryAndExercisePricePushData(t *testing.T) { +func TestInstrumentsSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(estimatedDeliveryAndExercisePricePushDataJSON)); err != nil { - t.Error("Okx Estimated Delivery and Exercise Price Push Data error", err) - } + err := ok.InstrumentsSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) + assert.NoError(t, err) } -const markPricePushData = `{"arg": {"channel": "mark-price","instId": "LTC-USD-190628"},"data": [{"instType": "FUTURES","instId": "LTC-USD-190628","markPx": "0.1","ts": "1597026383085"}]}` - -func TestMarkPricePushData(t *testing.T) { +func TestTickersSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(markPricePushData)); err != nil { - t.Error("Okx Mark Price Push Data error", err) - } + err := ok.TickersSubscription(contextGenerate(), "subscribe", asset.Margin, currency.NewPair(currency.BTC, currency.USDT)) + require.NoError(t, err) + err = ok.TickersSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) + assert.NoError(t, err) } - -const markPriceCandlestickPushData = `{"arg": {"channel": "mark-price-candle1D","instId": "BTC-USD-190628"},"data": [["1597026383085", "3.721", "3.743", "3.677", "3.708"],["1597026383085", "3.731", "3.799", "3.494", "3.72"]]}` - -func TestMarkPriceCandlestickPushData(t *testing.T) { +func TestOpenInterestSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(markPriceCandlestickPushData)); err != nil { - t.Error("Okx Mark Price Candlestick Push Data error", err) - } + err := ok.OpenInterestSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.BTC, currency.NewCode("USD-SWAP"))) + assert.NoError(t, err) } - -const priceLimitPushDataJSON = `{ "arg": { "channel": "price-limit", "instId": "LTC-USD-190628" }, "data": [{ "instId": "LTC-USD-190628", "buyLmt": "200", "sellLmt": "300", "ts": "1597026383085" }]}` - -func TestPriceLimitPushData(t *testing.T) { +func TestCandlesticksSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(priceLimitPushDataJSON)); err != nil { - t.Error("Okx Price Limit Push Data error", err) + enabled, err := ok.GetEnabledPairs(asset.PerpetualSwap) + require.NoError(t, err) + if len(enabled) == 0 { + t.SkipNow() } + err = ok.CandlesticksSubscription(contextGenerate(), "subscribe", channelCandle1m, asset.Futures, enabled[0]) + assert.NoError(t, err) } -const testSnapshotOrderbookPushData = `{"arg":{"channel":"books","instId":"BTC-USDT"},"action":"snapshot","data":[{"asks":[["0.07026","5","0","1"],["0.07027","765","0","3"],["0.07028","110","0","1"],["0.0703","1264","0","1"],["0.07034","280","0","1"],["0.07035","2255","0","1"],["0.07036","28","0","1"],["0.07037","63","0","1"],["0.07039","137","0","2"],["0.0704","48","0","1"],["0.07041","32","0","1"],["0.07043","3985","0","1"],["0.07057","257","0","1"],["0.07058","7870","0","1"],["0.07059","161","0","1"],["0.07061","4539","0","1"],["0.07068","1438","0","3"],["0.07088","3162","0","1"],["0.07104","99","0","1"],["0.07108","5018","0","1"],["0.07115","1540","0","1"],["0.07129","5080","0","1"],["0.07145","1512","0","1"],["0.0715","5016","0","1"],["0.07171","5026","0","1"],["0.07192","5062","0","1"],["0.07197","1517","0","1"],["0.0726","1511","0","1"],["0.07314","10376","0","1"],["0.07354","1","0","1"],["0.07466","10277","0","1"],["0.07626","269","0","1"],["0.07636","269","0","1"],["0.0809","1","0","1"],["0.08899","1","0","1"],["0.09789","1","0","1"],["0.10768","1","0","1"]],"bids":[["0.07014","56","0","2"],["0.07011","608","0","1"],["0.07009","110","0","1"],["0.07006","1264","0","1"],["0.07004","2347","0","3"],["0.07003","279","0","1"],["0.07001","52","0","1"],["0.06997","91","0","1"],["0.06996","4242","0","2"],["0.06995","486","0","1"],["0.06992","161","0","1"],["0.06991","63","0","1"],["0.06988","7518","0","1"],["0.06976","186","0","1"],["0.06975","71","0","1"],["0.06973","1086","0","1"],["0.06961","513","0","2"],["0.06959","4603","0","1"],["0.0695","186","0","1"],["0.06946","3043","0","1"],["0.06939","103","0","1"],["0.0693","5053","0","1"],["0.06909","5039","0","1"],["0.06888","5037","0","1"],["0.06886","1526","0","1"],["0.06867","5008","0","1"],["0.06846","5065","0","1"],["0.06826","1572","0","1"],["0.06801","1565","0","1"],["0.06748","67","0","1"],["0.0674","111","0","1"],["0.0672","10038","0","1"],["0.06652","1","0","1"],["0.06625","1526","0","1"],["0.06619","10924","0","1"],["0.05986","1","0","1"],["0.05387","1","0","1"],["0.04848","1","0","1"],["0.04363","1","0","1"]],"ts":"1659792392540","checksum":-1462286744}]}` -const updateOrderBookPushDataJSON = `{"arg":{"channel":"books","instId":"BTC-USDT"},"action":"update","data":[{"asks":[["0.07026","5","0","1"],["0.07027","765","0","3"],["0.07028","110","0","1"],["0.0703","1264","0","1"],["0.07034","280","0","1"],["0.07035","2255","0","1"],["0.07036","28","0","1"],["0.07037","63","0","1"],["0.07039","137","0","2"],["0.0704","48","0","1"],["0.07041","32","0","1"],["0.07043","3985","0","1"],["0.07057","257","0","1"],["0.07058","7870","0","1"],["0.07059","161","0","1"],["0.07061","4539","0","1"],["0.07068","1438","0","3"],["0.07088","3162","0","1"],["0.07104","99","0","1"],["0.07108","5018","0","1"],["0.07115","1540","0","1"],["0.07129","5080","0","1"],["0.07145","1512","0","1"],["0.0715","5016","0","1"],["0.07171","5026","0","1"],["0.07192","5062","0","1"],["0.07197","1517","0","1"],["0.0726","1511","0","1"],["0.07314","10376","0","1"],["0.07354","1","0","1"],["0.07466","10277","0","1"],["0.07626","269","0","1"],["0.07636","269","0","1"],["0.0809","1","0","1"],["0.08899","1","0","1"],["0.09789","1","0","1"],["0.10768","1","0","1"]],"bids":[["0.07014","56","0","2"],["0.07011","608","0","1"],["0.07009","110","0","1"],["0.07006","1264","0","1"],["0.07004","2347","0","3"],["0.07003","279","0","1"],["0.07001","52","0","1"],["0.06997","91","0","1"],["0.06996","4242","0","2"],["0.06995","486","0","1"],["0.06992","161","0","1"],["0.06991","63","0","1"],["0.06988","7518","0","1"],["0.06976","186","0","1"],["0.06975","71","0","1"],["0.06973","1086","0","1"],["0.06961","513","0","2"],["0.06959","4603","0","1"],["0.0695","186","0","1"],["0.06946","3043","0","1"],["0.06939","103","0","1"],["0.0693","5053","0","1"],["0.06909","5039","0","1"],["0.06888","5037","0","1"],["0.06886","1526","0","1"],["0.06867","5008","0","1"],["0.06846","5065","0","1"],["0.06826","1572","0","1"],["0.06801","1565","0","1"],["0.06748","67","0","1"],["0.0674","111","0","1"],["0.0672","10038","0","1"],["0.06652","1","0","1"],["0.06625","1526","0","1"],["0.06619","10924","0","1"],["0.05986","1","0","1"],["0.05387","1","0","1"],["0.04848","1","0","1"],["0.04363","1","0","1"]],"ts":"1659792392540","checksum":-1462286744}]}` - -func TestSnapshotAndUpdateOrderBookPushData(t *testing.T) { +func TestTradesSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(testSnapshotOrderbookPushData)); err != nil { - t.Error("Okx Snapshot order book push data error", err) - } - if err := ok.WsHandleData([]byte(updateOrderBookPushDataJSON)); err != nil { - t.Error("Okx Update Order Book Push Data error", err) - } + err := ok.TradesSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) + assert.NoError(t, err) } -var snapshotOrderBookPushData = `{"arg":{"channel":"books","instId":"%v"},"action":"snapshot","data":[{"asks":[["0.07026","5","0","1"],["0.07027","765","0","3"],["0.07028","110","0","1"],["0.0703","1264","0","1"],["0.07034","280","0","1"],["0.07035","2255","0","1"],["0.07036","28","0","1"],["0.07037","63","0","1"],["0.07039","137","0","2"],["0.0704","48","0","1"],["0.07041","32","0","1"],["0.07043","3985","0","1"],["0.07057","257","0","1"],["0.07058","7870","0","1"],["0.07059","161","0","1"],["0.07061","4539","0","1"],["0.07068","1438","0","3"],["0.07088","3162","0","1"],["0.07104","99","0","1"],["0.07108","5018","0","1"],["0.07115","1540","0","1"],["0.07129","5080","0","1"],["0.07145","1512","0","1"],["0.0715","5016","0","1"],["0.07171","5026","0","1"],["0.07192","5062","0","1"],["0.07197","1517","0","1"],["0.0726","1511","0","1"],["0.07314","10376","0","1"],["0.07354","1","0","1"],["0.07466","10277","0","1"],["0.07626","269","0","1"],["0.07636","269","0","1"],["0.0809","1","0","1"],["0.08899","1","0","1"],["0.09789","1","0","1"],["0.10768","1","0","1"]],"bids":[["0.07014","56","0","2"],["0.07011","608","0","1"],["0.07009","110","0","1"],["0.07006","1264","0","1"],["0.07004","2347","0","3"],["0.07003","279","0","1"],["0.07001","52","0","1"],["0.06997","91","0","1"],["0.06996","4242","0","2"],["0.06995","486","0","1"],["0.06992","161","0","1"],["0.06991","63","0","1"],["0.06988","7518","0","1"],["0.06976","186","0","1"],["0.06975","71","0","1"],["0.06973","1086","0","1"],["0.06961","513","0","2"],["0.06959","4603","0","1"],["0.0695","186","0","1"],["0.06946","3043","0","1"],["0.06939","103","0","1"],["0.0693","5053","0","1"],["0.06909","5039","0","1"],["0.06888","5037","0","1"],["0.06886","1526","0","1"],["0.06867","5008","0","1"],["0.06846","5065","0","1"],["0.06826","1572","0","1"],["0.06801","1565","0","1"],["0.06748","67","0","1"],["0.0674","111","0","1"],["0.0672","10038","0","1"],["0.06652","1","0","1"],["0.06625","1526","0","1"],["0.06619","10924","0","1"],["0.05986","1","0","1"],["0.05387","1","0","1"],["0.04848","1","0","1"],["0.04363","1","0","1"]],"ts":"1659792392540","checksum":-1462286744}]}` - -func TestSnapshotPushData(t *testing.T) { +func TestEstimatedDeliveryExercisePriceSubscription(t *testing.T) { t.Parallel() - err := ok.WsHandleData([]byte(fmt.Sprintf(snapshotOrderBookPushData, ok.CurrencyPairs.Pairs[asset.Futures].Enabled[0]))) - if err != nil { - t.Error("Okx Snapshot order book push data error", err) + futuresPairs, err := ok.FetchTradablePairs(contextGenerate(), asset.Futures) + require.NoErrorf(t, err, "%s error while fetching tradable pairs for instrument type %v: %v", ok.Name, asset.Futures, err) + if len(futuresPairs) == 0 { + t.SkipNow() } + err = ok.EstimatedDeliveryExercisePriceSubscription(contextGenerate(), "subscribe", asset.Futures, futuresPairs[0]) + assert.NoError(t, err) } -const calculateOrderbookChecksumUpdateorderbookJSON = `{"Bids":[{"Amount":56,"Price":0.07014,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":608,"Price":0.07011,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":110,"Price":0.07009,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1264,"Price":0.07006,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":2347,"Price":0.07004,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":279,"Price":0.07003,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":52,"Price":0.07001,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":91,"Price":0.06997,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":4242,"Price":0.06996,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":486,"Price":0.06995,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":161,"Price":0.06992,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":63,"Price":0.06991,"ID":0,"Period":0,"LiquidationOrders":0, -"OrderCount":0},{"Amount":7518,"Price":0.06988,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":186,"Price":0.06976,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":71,"Price":0.06975,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1086,"Price":0.06973,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":513,"Price":0.06961,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":4603,"Price":0.06959,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":186,"Price":0.0695,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":3043,"Price":0.06946,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":103,"Price":0.06939,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5053,"Price":0.0693,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5039,"Price":0.06909,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5037,"Price":0.06888,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1526,"Price":0.06886,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5008,"Price":0.06867,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5065,"Price":0.06846,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1572,"Price":0.06826,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1565,"Price":0.06801,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":67,"Price":0.06748,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":111,"Price":0.0674,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10038,"Price":0.0672,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.06652,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1526,"Price":0.06625,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10924,"Price":0.06619,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.05986,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.05387,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.04848,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.04363,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0}],"Asks":[{"Amount":5,"Price":0.07026,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":765,"Price":0.07027,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":110,"Price":0.07028,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1264,"Price":0.0703,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":280,"Price":0.07034,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":2255,"Price":0.07035,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":28,"Price":0.07036,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":63,"Price":0.07037,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":137,"Price":0.07039,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":48,"Price":0.0704,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":32,"Price":0.07041,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":3985,"Price":0.07043,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":257,"Price":0.07057,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":7870,"Price":0.07058,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":161,"Price":0.07059,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":4539,"Price":0.07061,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1438,"Price":0.07068,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":3162,"Price":0.07088,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":99,"Price":0.07104,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5018,"Price":0.07108,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1540,"Price":0.07115,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5080,"Price":0.07129,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1512,"Price":0.07145,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5016,"Price":0.0715,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5026,"Price":0.07171,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":5062,"Price":0.07192,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1517,"Price":0.07197,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1511,"Price":0.0726,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10376,"Price":0.07314,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.07354,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":10277,"Price":0.07466,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":269,"Price":0.07626,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":269,"Price":0.07636,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.0809,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.08899,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.09789,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0},{"Amount":1,"Price":0.10768,"ID":0,"Period":0,"LiquidationOrders":0,"OrderCount":0}],"Exchange":"Okx","Pair":"BTC-USDT","Asset":"spot","LastUpdated":"0001-01-01T00:00:00Z","LastUpdateID":0,"PriceDuplication":false,"IsFundingRate":false,"RestSnapshot":false,"IDAlignment":false}` - -func TestCalculateUpdateOrderbookChecksum(t *testing.T) { +func TestMarkPriceSubscription(t *testing.T) { t.Parallel() - - var orderbookBase orderbook.Base - err := json.Unmarshal([]byte(calculateOrderbookChecksumUpdateorderbookJSON), &orderbookBase) - if err != nil { - t.Errorf("%s error while deserializing to orderbook.Base %v", ok.Name, err) - } - if err := ok.CalculateUpdateOrderbookChecksum(&orderbookBase, 2832680552); err != nil { - t.Errorf("%s CalculateUpdateOrderbookChecksum() error: %v", ok.Name, err) + futuresPairs, err := ok.FetchTradablePairs(contextGenerate(), asset.Futures) + require.NoErrorf(t, err, "%s error while fetching tradable pairs for instrument type %v: %v", ok.Name, asset.Futures, err) + if len(futuresPairs) == 0 { + t.SkipNow() } + err = ok.MarkPriceSubscription(contextGenerate(), "subscribe", asset.Futures, futuresPairs[0]) + assert.NoError(t, err) } -const optionSummaryPushDataJSON = `{"arg": {"channel": "opt-summary","uly": "BTC-USD"},"data": [{"instType": "OPTION","instId": "BTC-USD-200103-5500-C","uly": "BTC-USD","delta": "0.7494223636","gamma": "-0.6765419039","theta": "-0.0000809873","vega": "0.0000077307","deltaBS": "0.7494223636","gammaBS": "-0.6765419039","thetaBS": "-0.0000809873","vegaBS": "0.0000077307","realVol": "0","bidVol": "","askVol": "1.5625","markVol": "0.9987","lever": "4.0342","fwdPx": "39016.8143629068452065","ts": "1597026383085"}]}` - -func TestOptionSummaryPushData(t *testing.T) { +func TestMarkPriceCandlesticksSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(optionSummaryPushDataJSON)); err != nil { - t.Error("Okx Option Summary Push Data error", err) + enabled, err := ok.GetEnabledPairs(asset.Spot) + require.NoError(t, err) + if len(enabled) == 0 { + t.SkipNow() } + err = ok.MarkPriceCandlesticksSubscription(contextGenerate(), "subscribe", channelMarkPriceCandle1Y, asset.Futures, enabled[0]) + assert.NoError(t, err) } -const fundingRatePushDataJSON = `{"arg": {"channel": "funding-rate","instId": "BTC-USD-SWAP"},"data": [{"instType": "SWAP","instId": "BTC-USD-SWAP","fundingRate": "0.018","nextFundingRate": "","fundingTime": "1597026383085"}]}` - -func TestFundingRatePushData(t *testing.T) { +func TestPriceLimitSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(fundingRatePushDataJSON)); err != nil { - t.Error("Okx Funding Rate Push Data error", err) - } + err := ok.PriceLimitSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPairWithDelimiter("BTC", "USD-SWAP", currency.DashDelimiter)) + assert.NoError(t, err) } -var indexCandlestickPushDataJSON = `{"arg": {"channel": "index-candle30m","instId": "BTC-USDT"},"data": [["1597026383085", "3811.31", "3811.31", "3811.31", "3811.31"]]}` - -func TestIndexCandlestickPushData(t *testing.T) { +func TestOrderBooksSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(indexCandlestickPushDataJSON)); err != nil { - t.Error("Okx Index Candlestick Push Data error", err) + enabled, err := ok.GetEnabledPairs(asset.Spot) + require.NoError(t, err) + if len(enabled) == 0 { + t.SkipNow() } + err = ok.OrderBooksSubscription(contextGenerate(), "subscribe", channelOrderBooks, asset.Futures, enabled[0]) + require.NoError(t, err) + err = ok.OrderBooksSubscription(contextGenerate(), "unsubscribe", channelOrderBooks, asset.Futures, enabled[0]) + assert.NoError(t, err) } -const indexTickerPushDataJSON = `{"arg": {"channel": "index-tickers","instId": "BTC-USDT"},"data": [{"instId": "BTC-USDT","idxPx": "0.1","high24h": "0.5","low24h": "0.1","open24h": "0.1","sodUtc0": "0.1","sodUtc8": "0.1","ts": "1597026383085"}]}` - -func TestIndexTickersPushData(t *testing.T) { +func TestOptionSummarySubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(indexTickerPushDataJSON)); err != nil { - t.Error("Okx Index Ticker Push Data error", err) - } + err := ok.OptionSummarySubscription(contextGenerate(), "subscribe", currency.NewPair(currency.SOL, currency.USD)) + require.NoError(t, err) + err = ok.OptionSummarySubscription(contextGenerate(), "unsubscribe", currency.NewPair(currency.SOL, currency.USD)) + assert.NoError(t, err) } -const statusPushDataJSON = `{"arg": {"channel": "status"},"data": [{"title": "Spot System Upgrade","state": "scheduled","begin": "1610019546","href": "","end": "1610019546","serviceType": "1","system": "classic","scheDesc": "","ts": "1597026383085"}]}` - -func TestStatusPushData(t *testing.T) { +func TestFundingRateSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(statusPushDataJSON)); err != nil { - t.Error("Okx Status Push Data error", err) - } + err := ok.FundingRateSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP"))) + require.NoError(t, err) + err = ok.FundingRateSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP"))) + assert.NoError(t, err) } -const publicStructBlockTradesPushDataJSON = `{"arg":{"channel":"public-struc-block-trades"},"data":[{"cTime":"1608267227834","blockTdId":"1802896","legs":[{"px":"0.323","sz":"25.0","instId":"BTC-USD-20220114-13250-C","side":"sell","tradeId":"15102"},{"px":"0.666","sz":"25","instId":"BTC-USD-20220114-21125-C","side":"buy","tradeId":"15103"}]}]}` - -func TestPublicStructBlockTrades(t *testing.T) { +func TestIndexCandlesticksSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(publicStructBlockTradesPushDataJSON)); err != nil { - t.Error("Okx Public Struct Block Trades error", err) - } + err := ok.IndexCandlesticksSubscription(contextGenerate(), "subscribe", channelIndexCandle6M, asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + require.NoError(t, err) + err = ok.IndexCandlesticksSubscription(contextGenerate(), "unsubscribe", channelIndexCandle6M, asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + assert.NoError(t, err) } - -const blockTickerPushDataJSON = `{"arg": {"channel": "block-tickers"},"data": [{"instType": "SWAP","instId": "LTC-USD-SWAP","volCcy24h": "0","vol24h": "0","ts": "1597026383085"}]}` - -func TestBlockTickerPushData(t *testing.T) { +func TestIndexTickerChannelIndexTickerChannel(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(blockTickerPushDataJSON)); err != nil { - t.Error("Okx Block Tickers push data error", err) - } + err := ok.IndexTickerChannel(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + require.NoError(t, err) + err = ok.IndexTickerChannel(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + assert.NoError(t, err) } -const accountPushDataJSON = `{"arg": {"channel": "block-tickers"},"data": [{"instType": "SWAP","instId": "LTC-USD-SWAP","volCcy24h": "0","vol24h": "0","ts": "1597026383085"}]}` - -func TestAccountPushData(t *testing.T) { +func TestStatusSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(accountPushDataJSON)); err != nil { - t.Error("Okx Account Push Data error", err) - } + err := ok.StatusSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + require.NoError(t, err) + err = ok.StatusSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + assert.NoError(t, err) } -const positionPushDataJSON = `{"arg":{"channel":"positions","instType":"FUTURES"},"data":[{"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-210430","instType":"FUTURES","interest":"0","last":"2566.22","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}]}` -const positionPushDataWithUnderlyingJSON = `{"arg": {"channel": "positions","uid": "77982378738415879","instType": "FUTURES"},"data": [{"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-210430","instType":"FUTURES","interest":"0","last":"2566.22","usdPx":"","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}, {"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-SWAP","instType":"SWAP","interest":"0","last":"2566.22","usdPx":"","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}]}` +func TestPublicStructureBlockTradesSubscription(t *testing.T) { + t.Parallel() + err := ok.PublicStructureBlockTradesSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + require.NoError(t, err) + err = ok.PublicStructureBlockTradesSubscription(contextGenerate(), "unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)) + assert.NoError(t, err) +} +func TestBlockTickerSubscription(t *testing.T) { + t.Parallel() + err := ok.BlockTickerSubscription(contextGenerate(), "subscribe", asset.Options, currency.NewPair(currency.BTC, currency.USDT)) + require.NoError(t, err) + err = ok.BlockTickerSubscription(contextGenerate(), "unsubscribe", asset.Options, currency.NewPair(currency.BTC, currency.USDT)) + assert.NoError(t, err) +} -func TestPositionPushData(t *testing.T) { +func TestPublicBlockTradesSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(positionPushDataJSON)); err != nil { - t.Error("Okx Account Push Data error", err) - } - if err := ok.WsHandleData([]byte(positionPushDataWithUnderlyingJSON)); err != nil { - t.Error("Okx Account Push Data error", err) - } + err := ok.PublicBlockTradesSubscription(contextGenerate(), "subscribe", asset.Options, currency.NewPairWithDelimiter("BTC", "USDT-SWAP", "-")) + require.NoError(t, err) + err = ok.PublicBlockTradesSubscription(contextGenerate(), "unsubscribe", asset.Options, currency.NewPairWithDelimiter("BTC", "USDT-SWAP", "-")) + assert.NoError(t, err) } -const balanceAndPositionJSON = `{"arg": {"channel": "balance_and_position","uid": "77982378738415879"},"data": [{"pTime": "1597026383085","eventType": "snapshot","balData": [{"ccy": "BTC","cashBal": "1","uTime": "1597026383085"}],"posData": [{"posId": "1111111111","tradeId": "2","instId": "BTC-USD-191018","instType": "FUTURES","mgnMode": "cross","posSide": "long","pos": "10","ccy": "BTC","posCcy": "","avgPx": "3320","uTIme": "1597026383085"}]}]}` +// ************ Authenticated Websocket endpoints Test ********************************************** -func TestBalanceAndPosition(t *testing.T) { +func TestWsAccountSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(balanceAndPositionJSON)); err != nil { - t.Error("Okx Balance And Position error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.WsAccountSubscription(contextGenerate(), "subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) + assert.NoError(t, err) } -func TestOrderPushData(t *testing.T) { +func TestWsPlaceOrder(t *testing.T) { t.Parallel() - ok := new(Okx) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes - require.NoError(t, testexch.Setup(ok), "Test instance Setup must not error") - testexch.FixtureToDataHandler(t, "testdata/wsOrders.json", ok.WsHandleData) - close(ok.Websocket.DataHandler) - assert.Len(t, ok.Websocket.DataHandler, 4, "Should see 4 orders") - for resp := range ok.Websocket.DataHandler { - switch v := resp.(type) { - case *order.Detail: - switch len(ok.Websocket.DataHandler) { - case 3: - assert.Equal(t, "452197707845865472", v.OrderID, "OrderID") - assert.Equal(t, "HamsterParty14", v.ClientOrderID, "ClientOrderID") - assert.Equal(t, asset.Spot, v.AssetType, "AssetType") - assert.Equal(t, order.Sell, v.Side, "Side") - assert.Equal(t, order.Filled, v.Status, "Status") - assert.Equal(t, order.Limit, v.Type, "Type") - assert.Equal(t, currency.NewPairWithDelimiter("BTC", "USDT", "-"), v.Pair, "Pair") - assert.Equal(t, 31527.1, v.AverageExecutedPrice, "AverageExecutedPrice") - assert.Equal(t, time.UnixMilli(1654084334977), v.Date, "Date") - assert.Equal(t, time.UnixMilli(1654084353263), v.CloseTime, "CloseTime") - assert.Equal(t, 0.001, v.Amount, "Amount") - assert.Equal(t, 0.001, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 0.000, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 31527.1, v.Price, "Price") - assert.Equal(t, 0.02522168, v.Fee, "Fee") - assert.Equal(t, currency.USDT, v.FeeAsset, "FeeAsset") - case 2: - assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") - assert.Equal(t, asset.Spot, v.AssetType, "AssetType") - assert.Equal(t, order.Market, v.Type, "Type") - assert.Equal(t, order.Sell, v.Side, "Side") - assert.Equal(t, order.Active, v.Status, "Status") - assert.Equal(t, 0.0, v.Amount, "Amount should be 0 for a market sell") - assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") - case 1: - assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") - assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") - assert.Equal(t, 0.00038127046945832905, v.Amount, "Amount") - assert.Equal(t, 0.010000249968, v.Fee, "Fee") - assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, order.PartiallyFilled, v.Status, "Status") - case 0: - assert.Equal(t, "620258920632008725", v.OrderID, "OrderID") - assert.Equal(t, 10.0, v.QuoteAmount, "QuoteAmount") - assert.Equal(t, 0.010000249968, v.Fee, "Fee") - assert.Equal(t, 0.0, v.RemainingAmount, "RemainingAmount") - assert.Equal(t, 0.00038128, v.ExecutedAmount, "ExecutedAmount") - assert.Equal(t, 0.00038128, v.Amount, "Amount should be derived because order filled") - assert.Equal(t, order.Filled, v.Status, "Status") - } - case error: - t.Error(v) - default: - t.Errorf("Got unexpected data: %T %v", v, v) - } + _, err := ok.WsPlaceOrder(contextGenerate(), &PlaceOrderRequestParam{}) + require.ErrorIs(t, err, common.ErrNilPointer) + + arg := &PlaceOrderRequestParam{ + ReduceOnly: true, + AssetType: asset.Margin, } -} + _, err = ok.WsPlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.WsPlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = "Buy" + arg.TradeMode = "abc" + _, err = ok.WsPlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = "cross" + _, err = ok.WsPlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) -const algoOrdersPushDataJSON = `{"arg": {"channel": "orders-algo","uid": "77982378738415879","instType": "FUTURES","instId": "BTC-USD-200329"},"data": [{"instType": "FUTURES","instId": "BTC-USD-200329","ordId": "312269865356374016","ccy": "BTC","algoId": "1234","px": "999","sz": "3","tdMode": "cross","tgtCcy": "","notionalUsd": "","ordType": "trigger","side": "buy","posSide": "long","state": "live","lever": "20","tpTriggerPx": "","tpTriggerPxType": "","tpOrdPx": "","slTriggerPx": "","slTriggerPxType": "","triggerPx": "99","triggerPxType": "last","ordPx": "12","actualSz": "","actualPx": "","tag": "adadadadad","actualSide": "","triggerTime": "1597026383085","cTime": "1597026383000"}]}` + arg.OrderType = order.Limit.String() + _, err = ok.WsPlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) -func TestAlgoOrderPushData(t *testing.T) { + arg.AssetType = asset.Futures + _, err = ok.WsPlaceOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsPlaceOrder(contextGenerate(), &PlaceOrderRequestParam{ + InstrumentID: "BTC-USDC", + TradeMode: "cross", + Side: order.Buy.Lower(), + OrderType: "limit", + Amount: 2.6, + Price: 2.1, + Currency: "BTC", + }) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.WsPlaceOrder(contextGenerate(), &PlaceOrderRequestParam{ + InstrumentID: "BTC-USDC", + TradeMode: "cross", + Side: order.Buy.Lower(), + PositionSide: "long", + OrderType: "limit", + Amount: 2.6, + Price: 2.1, + Currency: "BTC", + AssetType: asset.Futures, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestWsPlaceMultipleOrder(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(algoOrdersPushDataJSON)); err != nil { - t.Error("Okx Algo Order Push Data error", err) + var resp []PlaceOrderRequestParam + err := json.Unmarshal([]byte(placeOrderArgs), &resp) + require.NoError(t, err) + + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{}) + require.ErrorIs(t, err, order.ErrSubmissionIsNil) + + arg := PlaceOrderRequestParam{ + ReduceOnly: true, } + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = "buy" + arg.TradeMode = "abc" + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = "cross" + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.OrderType = "limit" + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.AssetType = asset.Futures + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.PositionSide = "long" + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err = ok.WsPlaceMultipleOrders(contextGenerate(), resp) + assert.False(t, (err != nil && !errors.Is(err, errWebsocketStreamNotAuthenticated)), err) } -const advancedAlgoOrderPushDataJSON = `{"arg":{"channel":"algo-advance","uid": "77982378738415879","instType":"SPOT","instId":"BTC-USDT"},"data":[{"actualPx":"","actualSide":"","actualSz":"0","algoId":"355056228680335360","cTime":"1630924001545","ccy":"","count":"1","instId":"BTC-USDT","instType":"SPOT","lever":"0","notionalUsd":"","ordPx":"","ordType":"iceberg","pTime":"1630924295204","posSide":"net","pxLimit":"10","pxSpread":"1","pxVar":"","side":"buy","slOrdPx":"","slTriggerPx":"","state":"pause","sz":"0.1","szLimit":"0.1","tdMode":"cash","timeInterval":"","tpOrdPx":"","tpTriggerPx":"","tag": "adadadadad","triggerPx":"","triggerTime":"","callbackRatio":"","callbackSpread":"","activePx":"","moveTriggerPx":""}]}` +func TestWsCancelOrder(t *testing.T) { + t.Parallel() + _, err := ok.WsCancelOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) + + _, err = ok.WsCancelOrder(contextGenerate(), &CancelOrderRequestParam{OrderID: "1234"}) + require.ErrorIs(t, err, errMissingInstrumentID) -func TestAdvancedAlgoOrderPushData(t *testing.T) { + _, err = ok.WsCancelOrder(contextGenerate(), &CancelOrderRequestParam{InstrumentID: "BTC-USD-190927"}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsCancelOrder(contextGenerate(), &CancelOrderRequestParam{ + InstrumentID: "BTC-USD-190927", + OrderID: "2510789768709120", + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestWsCancleMultipleOrder(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(advancedAlgoOrderPushDataJSON)); err != nil { - t.Error("Okx Advanced Algo Orders Push Data error", err) + arg := CancelOrderRequestParam{ + OrderID: "2510789768709120", } + _, err := ok.WsCancelMultipleOrder(contextGenerate(), []CancelOrderRequestParam{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = "DCR-BTC" + arg.OrderID = "" + _, err = ok.WsCancelMultipleOrder(contextGenerate(), []CancelOrderRequestParam{arg}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsCancelMultipleOrder(contextGenerate(), []CancelOrderRequestParam{{ + InstrumentID: "DCR-BTC", + OrderID: "2510789768709120", + }}) + require.NoError(t, err) + assert.NotNil(t, result) } -const positionRiskPushDataJSON = `{"arg": {"channel": "liquidation-warning","uid": "77982378738415879","instType": "FUTURES"},"data": [{"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-210430","instType":"FUTURES","interest":"0","last":"2566.22","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}, {"adl":"1","availPos":"1","avgPx":"2566.31","cTime":"1619507758793","ccy":"ETH","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","imr":"","instId":"ETH-USD-SWAP","instType":"SWAP","interest":"0","last":"2566.22","lever":"10","liab":"","liabCcy":"","liqPx":"2352.8496681818233","markPx":"2353.849","margin":"0.0003896645377994","mgnMode":"isolated","mgnRatio":"11.731726509588816","mmr":"0.0000311811092368","notionalUsd":"2276.2546609009605","optVal":"","pTime":"1619507761462","pos":"1","posCcy":"","posId":"307173036051017730","posSide":"long","thetaBS":"","thetaPA":"","tradeId":"109844","uTime":"1619507761462","upl":"-0.0000009932766034","uplRatio":"-0.0025490556801078","vegaBS":"","vegaPA":""}]}` +func TestWsAmendOrder(t *testing.T) { + t.Parallel() + _, err := ok.WsAmendOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) + + arg := &AmendOrderRequestParams{} + _, err = ok.WsAmendOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = spotTP.String() + _, err = ok.WsAmendOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + arg.OrderID = "1234" + _, err = ok.WsAmendOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsAmendOrder(contextGenerate(), &AmendOrderRequestParams{ + InstrumentID: spotTP.String(), + OrderID: "2510789768709120", + NewQuantity: 1234, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} -func TestPositionRiskPushDataJSON(t *testing.T) { +func TestWsAmendMultipleOrders(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(positionRiskPushDataJSON)); err != nil { - t.Error("Okx Position Risk Push Data error", err) + arg := AmendOrderRequestParams{ + CancelOnFail: true, } + _, err := ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = "DCR-BTC" + _, err = ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + arg.OrderID = "2510789768709120" + _, err = ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{arg}) + require.ErrorIs(t, err, errInvalidNewSizeOrPriceInformation) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsAmendMultipleOrders(contextGenerate(), []AmendOrderRequestParams{ + { + InstrumentID: "DCR-BTC", + OrderID: "2510789768709120", + NewPrice: 1233324.332, + NewQuantity: 1234, + }, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -const accountGreeksPushData = `{"arg": {"channel": "account-greeks","ccy": "BTC"},"data": [{"thetaBS": "","thetaPA":"","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","vegaBS":"", "vegaPA":"","ccy":"BTC","ts":"1620282889345"}]}` +func TestWsMassCancelOrders(t *testing.T) { + t.Parallel() + _, err := ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{{}}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + _, err = ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{{InstrumentFamily: "BTC-USD"}}) + require.ErrorIs(t, err, errInvalidInstrumentType) + + _, err = ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{{InstrumentType: "OPTION"}}) + require.ErrorIs(t, err, errInstrumentFamilyRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsMassCancelOrders(contextGenerate(), []CancelMassReqParam{ + { + InstrumentType: "OPTION", + InstrumentFamily: "BTC-USD", + }, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} -func TestAccountGreeksPushData(t *testing.T) { +func TestWsPositionChannel(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(accountGreeksPushData)); err != nil { - t.Error("Okx Account Greeks Push Data error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.WsPositionChannel(contextGenerate(), "subscribe", asset.Options, currency.NewPair(currency.USD, currency.BTC)) + assert.NoError(t, err) } -const rfqsPushDataJSON = `{"arg": {"channel": "account-greeks","ccy": "BTC"},"data": [{"thetaBS": "","thetaPA":"","deltaBS":"","deltaPA":"","gammaBS":"","gammaPA":"","vegaBS":"", "vegaPA":"","ccy":"BTC","ts":"1620282889345"}]}` +func TestBalanceAndPositionSubscription(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.BalanceAndPositionSubscription(contextGenerate(), "subscribe", "1234") + require.NoError(t, err) + err = ok.BalanceAndPositionSubscription(contextGenerate(), "unsubscribe", "1234") + assert.NoError(t, err) +} -func TestRfqs(t *testing.T) { +func TestWsOrderChannel(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(rfqsPushDataJSON)); err != nil { - t.Error("Okx Rfqs Push Data error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.WsOrderChannel(contextGenerate(), "subscribe", asset.Margin, currency.NewPair(currency.SOL, currency.USDT), "") + require.NoError(t, err) + err = ok.WsOrderChannel(contextGenerate(), "unsubscribe", asset.Margin, currency.NewPair(currency.SOL, currency.USDT), "") + assert.NoError(t, err) } -const accountsPushDataJSON = `{ "arg": { "channel": "account", "ccy": "BTC", "uid": "77982378738415879" }, "data": [ { "uTime": "1597026383085", "totalEq": "41624.32", "isoEq": "3624.32", "adjEq": "41624.32", "ordFroz": "0", "imr": "4162.33", "mmr": "4", "notionalUsd": "", "mgnRatio": "41624.32", "details": [ { "availBal": "", "availEq": "1", "ccy": "BTC", "cashBal": "1", "uTime": "1617279471503", "disEq": "50559.01", "eq": "1", "eqUsd": "45078.3790756226851775", "frozenBal": "0", "interest": "0", "isoEq": "0", "liab": "0", "maxLoan": "", "mgnRatio": "", "notionalLever": "0.0022195262185864", "ordFrozen": "0", "upl": "0", "uplLiab": "0", "crossLiab": "0", "isoLiab": "0", "coinUsdPrice": "60000", "stgyEq":"0", "spotInUseAmt":"", "isoUpl":"" } ] } ]}` +func TestAlgoOrdersSubscription(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.AlgoOrdersSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) + require.NoError(t, err) + err = ok.AlgoOrdersSubscription(contextGenerate(), "unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) + assert.NoError(t, err) +} -func TestAccounts(t *testing.T) { +func TestAdvanceAlgoOrdersSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(accountsPushDataJSON)); err != nil { - t.Errorf("%s Accounts push data error %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.AdvanceAlgoOrdersSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP")), "") + require.NoError(t, err) + err = ok.AdvanceAlgoOrdersSubscription(contextGenerate(), "unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP")), "") + assert.NoError(t, err) } -const quotesPushDataJSON = `{"arg":{"channel":"quotes"},"data":[{"validUntil":"1608997227854","uTime":"1608267227834","cTime":"1608267227834","legs":[{"px":"0.0023","sz":"25.0","instId":"BTC-USD-220114-25000-C","side":"sell","tgtCcy":""},{"px":"0.0045","sz":"25","instId":"BTC-USD-220114-35000-C","side":"buy","tgtCcy":""}],"quoteId":"25092","rfqId":"18753","traderCode":"SATS","quoteSide":"sell","state":"canceled","clQuoteId":""}]}` +func TestPositionRiskWarningSubscription(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.PositionRiskWarningSubscription(contextGenerate(), "subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) + require.NoError(t, err) + err = ok.PositionRiskWarningSubscription(contextGenerate(), "unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))) + assert.NoError(t, err) +} -func TestQuotesPushData(t *testing.T) { +func TestAccountGreeksSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(quotesPushDataJSON)); err != nil { - t.Error("Okx Quotes Push Data error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.AccountGreeksSubscription(contextGenerate(), "subscribe", currency.NewPair(currency.SOL, currency.USD)) + require.NoError(t, err) + err = ok.AccountGreeksSubscription(contextGenerate(), "unsubscribe", currency.NewPair(currency.SOL, currency.USD)) + assert.NoError(t, err) } -const structureBlockTradesPushDataJSON = `{"arg":{"channel":"struc-block-trades"},"data":[{"cTime":"1608267227834","rfqId":"18753","clRfqId":"","quoteId":"25092","clQuoteId":"","blockTdId":"180184","tTraderCode":"ANAND","mTraderCode":"WAGMI","legs":[{"px":"0.0023","sz":"25.0","instId":"BTC-USD-20220630-60000-C","side":"sell","fee":"0.1001","feeCcy":"BTC","tradeId":"10211","tgtCcy":""},{"px":"0.0033","sz":"25","instId":"BTC-USD-20220630-50000-C","side":"buy","fee":"0.1001","feeCcy":"BTC","tradeId":"10212","tgtCcy":""}]}]}` +func TestRFQSubscription(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.RFQSubscription(contextGenerate(), "subscribe", "") + require.NoError(t, err) + err = ok.RFQSubscription(contextGenerate(), "unsubscribe", "") + assert.NoError(t, err) +} -func TestStructureBlockTradesPushData(t *testing.T) { +func TestQuotesSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(structureBlockTradesPushDataJSON)); err != nil { - t.Error("Okx Structure Block Trades error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.QuotesSubscription(contextGenerate(), "subscribe") + require.NoError(t, err) + err = ok.QuotesSubscription(contextGenerate(), "unsubscribe") + assert.NoError(t, err) } -const spotGridAlgoOrdersPushDataJSON = `{"arg": {"channel": "grid-orders-spot","instType": "ANY"},"data": [{"algoId": "448965992920907776","algoOrdType": "grid","annualizedRate": "0","arbitrageNum": "0","baseSz": "0","cTime": "1653313834104","cancelType": "0","curBaseSz": "0.001776289214","curQuoteSz": "46.801755866","floatProfit": "-0.4953878967772","gridNum": "6","gridProfit": "0","instId": "BTC-USDC","instType": "SPOT","investment": "100","maxPx": "33444.8","minPx": "24323.5","pTime": "1653476023742","perMaxProfitRate": "0.060375293181491054543","perMinProfitRate": "0.0455275366818586","pnlRatio": "0","quoteSz": "100","runPx": "30478.1","runType": "1","singleAmt": "0.00059261","slTriggerPx": "","state": "running","stopResult": "0","stopType": "0","totalAnnualizedRate": "-0.9643551057262827","totalPnl": "-0.4953878967772","tpTriggerPx": "","tradeNum": "3","triggerTime": "1653378736894","uTime": "1653378736894"}]}` +func TestStructureBlockTradesSubscription(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.StructureBlockTradesSubscription(contextGenerate(), "subscribe") + require.NoError(t, err) + err = ok.StructureBlockTradesSubscription(contextGenerate(), "unsubscribe") + assert.NoError(t, err) +} -func TestSpotGridAlgoOrdersPushData(t *testing.T) { +func TestSpotGridAlgoOrdersSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(spotGridAlgoOrdersPushDataJSON)); err != nil { - t.Error("Okx Spot Grid Algo Orders Push Data error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.SpotGridAlgoOrdersSubscription(contextGenerate(), "subscribe", asset.Empty, currency.EMPTYPAIR, "") + require.NoError(t, err) + err = ok.SpotGridAlgoOrdersSubscription(contextGenerate(), "unsubscribe", asset.Empty, currency.EMPTYPAIR, "") + assert.NoError(t, err) } -const contractGridAlgoOrdersPushDataJSON = `{"arg": {"channel": "grid-orders-contract","instType": "ANY"},"data": [{"actualLever": "1.02","algoId": "449327675342323712","algoOrdType": "contract_grid","annualizedRate": "0.7572437878956523","arbitrageNum": "1","basePos": true,"cTime": "1653400065912","cancelType": "0","direction": "long","eq": "10129.419829834853","floatProfit": "109.537858234853","gridNum": "50","gridProfit": "19.8819716","instId": "BTC-USDT-SWAP","instType": "SWAP","investment": "10000","lever": "5","liqPx": "603.2149534767834","maxPx": "100000","minPx": "10","pTime": "1653484573918","perMaxProfitRate": "995.7080916791230692","perMinProfitRate": "0.0946277854875634","pnlRatio": "0.0129419829834853","runPx": "29216.3","runType": "1","singleAmt": "1","slTriggerPx": "","state": "running","stopType": "0","sz": "10000","tag": "","totalAnnualizedRate": "4.929207431970923","totalPnl": "129.419829834853","tpTriggerPx": "","tradeNum": "37","triggerTime": "1653400066940","uTime": "1653484573589","uly": "BTC-USDT"}]}` +func TestContractGridAlgoOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.ContractGridAlgoOrders(contextGenerate(), "subscribe", asset.Empty, currency.EMPTYPAIR, "") + require.NoError(t, err) + err = ok.ContractGridAlgoOrders(contextGenerate(), "unsubscribe", asset.Empty, currency.EMPTYPAIR, "") + assert.NoError(t, err) +} -func TestContractGridAlgoOrdersPushData(t *testing.T) { +func TestGridPositionsSubscription(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(contractGridAlgoOrdersPushDataJSON)); err != nil { - t.Error("Okx Contract Grid Algo Order Push Data error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.GridPositionsSubscription(contextGenerate(), "subscribe", "1234") + require.NoError(t, err) + err = ok.GridPositionsSubscription(contextGenerate(), "unsubscribe", "1234") + assert.NoError(t, err) } -const gridPositionsPushDataJSON = `{"arg": {"channel": "grid-positions","uid": "44705892343619584","algoId": "449327675342323712"},"data": [{"adl": "1","algoId": "449327675342323712","avgPx": "29181.4638888888888895","cTime": "1653400065917","ccy": "USDT","imr": "2089.2690000000002","instId": "BTC-USDT-SWAP","instType": "SWAP","last": "29852.7","lever": "5","liqPx": "604.7617536513744","markPx": "29849.7","mgnMode": "cross","mgnRatio": "217.71740878394456","mmr": "41.78538","notionalUsd": "10435.794191550001","pTime": "1653536068723","pos": "35","posSide": "net","uTime": "1653445498682","upl": "232.83263888888962","uplRatio": "0.1139826489932205"}]}` +func TestGridSubOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + err := ok.GridSubOrders(contextGenerate(), "subscribe", "") + require.NoError(t, err) + err = ok.GridSubOrders(contextGenerate(), "unsubscribe", "") + assert.NoError(t, err) +} -func TestGridPositionsPushData(t *testing.T) { +func TestGetServerTime(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(gridPositionsPushDataJSON)); err != nil { - t.Error("Okx Grid Positions Push Data error", err) - } + result, err := ok.GetServerTime(contextGenerate(), asset.Empty) + require.NoError(t, err) + assert.NotNil(t, result) } -const gridSubOrdersPushDataJSON = `{"arg": {"channel": "grid-sub-orders","uid": "44705892343619584","algoId": "449327675342323712"},"data": [{"accFillSz": "0","algoId": "449327675342323712","algoOrdType": "contract_grid","avgPx": "0","cTime": "1653445498664","ctVal": "0.01","fee": "0","feeCcy": "USDT","groupId": "-1","instId": "BTC-USDT-SWAP","instType": "SWAP","lever": "5","ordId": "449518234142904321","ordType": "limit","pTime": "1653486524502","pnl": "","posSide": "net","px": "28007.2","side": "buy","state": "live","sz": "1","tag":"","tdMode": "cross","uTime": "1653445498674"}]}` +func TestGetAvailableTransferChains(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetAvailableTransferChains(contextGenerate(), currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) +} -func TestGridSubOrdersPushData(t *testing.T) { +func TestGetIntervalEnum(t *testing.T) { t.Parallel() - if err := ok.WsHandleData([]byte(gridSubOrdersPushDataJSON)); err != nil { - t.Error("Okx Grid Sub orders Push Data error", err) + tests := []struct { + Description string + Interval kline.Interval + Expected string + AppendUTC bool + }{ + {Expected: "", AppendUTC: true}, + {Description: "kline.OneMin ", Interval: kline.OneMin, Expected: "1m"}, + {Description: "kline.ThreeMin ", Interval: kline.ThreeMin, Expected: "3m"}, + {Description: "kline.FiveMin ", Interval: kline.FiveMin, Expected: "5m"}, + {Description: "kline.FifteenMin ", Interval: kline.FifteenMin, Expected: "15m"}, + {Description: "kline.ThirtyMin ", Interval: kline.ThirtyMin, Expected: "30m"}, + {Description: "kline.OneHour ", Interval: kline.OneHour, Expected: "1H"}, + {Description: "kline.TwoHour ", Interval: kline.TwoHour, Expected: "2H"}, + {Description: "kline.FourHour ", Interval: kline.FourHour, Expected: "4H"}, + {Description: "kline.SixHour ", Interval: kline.SixHour, Expected: "6H"}, + {Description: "kline.TwelveHour ", Interval: kline.TwelveHour, Expected: "12H"}, + {Description: "kline.OneDay ", Interval: kline.OneDay, Expected: "1D"}, + {Description: "kline.TwoDay ", Interval: kline.TwoDay, Expected: "2D"}, + {Description: "kline.ThreeDay ", Interval: kline.ThreeDay, Expected: "3D"}, + {Description: "kline.OneWeek ", Interval: kline.OneWeek, Expected: "1W"}, + {Description: "kline.FiveDay ", Interval: kline.FiveDay, Expected: "5D"}, + {Description: "kline.OneMonth ", Interval: kline.OneMonth, Expected: "1M"}, + {Description: "kline.ThreeMonth ", Interval: kline.ThreeMonth, Expected: "3M"}, + {Description: "kline.SixMonth ", Interval: kline.SixMonth, Expected: "6M"}, + {Description: "kline.OneYear ", Interval: kline.OneYear, Expected: "1Y"}, + {Description: "kline.SixHour + UTC", Interval: kline.SixHour, Expected: "6Hutc", AppendUTC: true}, + {Description: "kline.TwelveHour + UTC ", Interval: kline.TwelveHour, Expected: "12Hutc", AppendUTC: true}, + {Description: "kline.OneDay + UTC ", Interval: kline.OneDay, Expected: "1Dutc", AppendUTC: true}, + {Description: "kline.TwoDay + UTC ", Interval: kline.TwoDay, Expected: "2Dutc", AppendUTC: true}, + {Description: "kline.ThreeDay + UTC ", Interval: kline.ThreeDay, Expected: "3Dutc", AppendUTC: true}, + {Description: "kline.FiveDay + UTC ", Interval: kline.FiveDay, Expected: "5Dutc", AppendUTC: true}, + {Description: "kline.OneWeek + UTC ", Interval: kline.OneWeek, Expected: "1Wutc", AppendUTC: true}, + {Description: "kline.OneMonth + UTC ", Interval: kline.OneMonth, Expected: "1Mutc", AppendUTC: true}, + {Description: "kline.ThreeMonth + UTC ", Interval: kline.ThreeMonth, Expected: "3Mutc", AppendUTC: true}, + {Description: "kline.SixMonth + UTC ", Interval: kline.SixMonth, Expected: "6Mutc", AppendUTC: true}, + {Description: "kline.OneYear + UTC ", Interval: kline.OneYear, Expected: "1Yutc", AppendUTC: true}, + } + + for _, tt := range tests { + t.Run(tt.Description, func(t *testing.T) { + t.Parallel() + r := IntervalFromString(tt.Interval, tt.AppendUTC) + require.Equalf(t, tt.Expected, r, "%s: received: %s but expected: %s", tt.Description, r, tt.Expected) + }) } } -func TestGetHistoricTrades(t *testing.T) { +func TestInstrument(t *testing.T) { t.Parallel() - if _, err := ok.GetHistoricTrades(contextGenerate(), currency.NewPair(currency.BTC, currency.USDT), asset.Spot, time.Now().Add(-time.Minute*4), time.Now().Add(-time.Minute*2)); err != nil { - t.Errorf("%s GetHistoricTrades() error %v", ok.Name, err) + var i Instrument + err := json.Unmarshal([]byte(instrumentJSON), &i) + require.NoError(t, err) + require.Empty(t, i.Alias, "expected empty alias") + assert.Empty(t, i.BaseCurrency, "expected empty base currency") + assert.Equal(t, "1", i.Category, "expected 1 category") + assert.Equal(t, 1, int(i.ContractMultiplier.Int64()), "expected 1 contract multiplier") + assert.Equal(t, "linear", i.ContractType, "expected linear contract type") + assert.Equal(t, 0.0001, i.ContractValue.Float64(), "expected 0.0001 contract value") + assert.Equal(t, currency.BTC.String(), i.ContractValueCurrency, "expected BTC contract value currency") + assert.True(t, i.ExpTime.Time().IsZero(), "expected empty expiry time") + assert.Equal(t, "BTC-USDC", i.InstrumentFamily, "expected BTC-USDC instrument family") + assert.Equal(t, "BTC-USDC-SWAP", i.InstrumentID, "expected BTC-USDC-SWAP instrument ID") + + swap := GetInstrumentTypeFromAssetItem(asset.PerpetualSwap) + assert.Equal(t, swap, i.InstrumentType, "expected SWAP instrument type") + assert.Equal(t, 125, int(i.MaxLeverage), "expected 125 leverage") + assert.Equal(t, int64(1666076190000), i.ListTime.Time().UnixMilli(), "expected 1666076190000 listing time") + assert.Equal(t, 1, int(i.LotSize)) + assert.Equal(t, 100000000.0000000000000000, i.MaxSpotIcebergSize.Float64()) + assert.Equal(t, 100000000, int(i.MaxQuantityOfSpotLimitOrder)) + assert.Equal(t, 85000, int(i.MaxQuantityOfMarketLimitOrder)) + assert.Equal(t, 85000, int(i.MaxStopSize)) + assert.Equal(t, 100000000.0000000000000000, i.MaxTriggerSize.Float64()) + assert.Equal(t, 0, int(i.MaxQuantityOfSpotTwapLimitOrder), "expected empty max TWAP size") + assert.Equal(t, 1, int(i.MinimumOrderSize)) + assert.Empty(t, i.OptionType, "expected empty option type") + assert.Empty(t, i.QuoteCurrency, "expected empty quote currency") + assert.Equal(t, currency.USDC.String(), i.SettlementCurrency, "expected USDC settlement currency") + assert.Equal(t, "live", i.State) + assert.Empty(t, i.StrikePrice, "expected empty strike price") + assert.Equal(t, 0.1, i.TickSize.Float64()) + assert.Equal(t, "BTC-USDC", i.Underlying, "expected BTC-USDC underlying") +} + +func TestGetLatestFundingRate(t *testing.T) { + t.Parallel() + cp, err := currency.NewPairFromString("BTC-USD-SWAP") + require.NoError(t, err) + result, err := ok.GetLatestFundingRates(contextGenerate(), &fundingrate.LatestRateRequest{ + Asset: asset.PerpetualSwap, + Pair: cp, + IncludePredictedRate: true, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetHistoricalFundingRates(t *testing.T) { + t.Parallel() + cp, err := currency.NewPairFromString("BTC-USD-SWAP") + require.NoError(t, err) + r := &fundingrate.HistoricalRatesRequest{ + Asset: asset.PerpetualSwap, + Pair: cp, + PaymentCurrency: currency.USDT, + StartDate: time.Now().Add(-time.Hour * 24 * 2), + EndDate: time.Now(), + IncludePredictedRate: true, + } + + r.StartDate = time.Now().Add(-time.Hour * 24 * 120) + _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) + require.ErrorIs(t, err, fundingrate.ErrFundingRateOutsideLimits) + + if sharedtestvalues.AreAPICredentialsSet(ok) { + r.IncludePayments = true } + r.StartDate = time.Now().Add(-time.Hour * 24 * 12) + result, err := ok.GetHistoricalFundingRates(contextGenerate(), r) + require.NoError(t, err) + require.NotNil(t, result) + + r.RespectHistoryLimits = true + result, err = ok.GetHistoricalFundingRates(contextGenerate(), r) + require.NoError(t, err) + assert.NotNil(t, result) } -func setupWS() { - if !ok.Websocket.IsEnabled() { - return +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + is, err := ok.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT)) + require.NoError(t, err) + require.False(t, is) + + cp, err := currency.NewPairFromString("BTC-USD-SWAP") + require.NoError(t, err) + is, err = ok.IsPerpetualFutureCurrency(asset.PerpetualSwap, cp) + require.NoError(t, err) + assert.True(t, is, "expected true") +} + +func TestGetAssetsFromInstrumentTypeOrID(t *testing.T) { + t.Parallel() + + ok := new(Okx) //nolint:govet // Intentional shadow + require.NoError(t, testexch.Setup(ok), "Setup must not error") + + _, err := ok.getAssetsFromInstrumentID("") + assert.ErrorIs(t, err, errMissingInstrumentID) + + for _, a := range []asset.Item{asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options} { + assets, err2 := ok.getAssetsFromInstrumentID(ok.CurrencyPairs.Pairs[a].Enabled[0].String()) + require.NoErrorf(t, err2, "GetAssetsFromInstrumentTypeOrID must not error for asset: %s", a) + switch a { + case asset.Spot, asset.Margin: + // spot and margin instruments are similar + require.Len(t, assets, 2) + default: + require.Len(t, assets, 1) + } + assert.Contains(t, assets, a, "Should contain asset: %s", a) } - if !sharedtestvalues.AreAPICredentialsSet(ok) { - ok.Websocket.SetCanUseAuthenticatedEndpoints(false) + + _, err = ok.getAssetsFromInstrumentID("test") + assert.ErrorIs(t, err, currency.ErrCurrencyNotSupported) + _, err = ok.getAssetsFromInstrumentID("test-test") + assert.ErrorIs(t, err, asset.ErrNotSupported) + + for _, a := range []asset.Item{asset.Margin, asset.Spot} { + assets, err2 := ok.getAssetsFromInstrumentID(ok.CurrencyPairs.Pairs[a].Enabled[0].String()) + require.NoErrorf(t, err2, "GetAssetsFromInstrumentTypeOrID must not error for asset: %s", a) + assert.Contains(t, assets, a) } - err := ok.WsConnect() - if err != nil { - log.Fatal(err) +} + +func TestSetMarginType(t *testing.T) { + t.Parallel() + err := ok.SetMarginType(contextGenerate(), asset.Spot, currency.NewBTCUSDT(), margin.Isolated) + assert.ErrorIs(t, err, common.ErrFunctionNotSupported) +} + +func TestChangePositionMargin(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + cp, err := currency.NewPairFromString("eth/btc") + require.NoError(t, err) + result, err := ok.ChangePositionMargin(contextGenerate(), &margin.PositionChangeRequest{ + Pair: cp, + Asset: asset.Margin, + MarginType: margin.Isolated, + OriginalAllocatedMargin: 4.0695, + NewAllocatedMargin: 5, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetCollateralMode(t *testing.T) { + t.Parallel() + _, err := ok.GetCollateralMode(contextGenerate(), asset.USDTMarginedFutures) + require.ErrorIs(t, err, asset.ErrNotSupported) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetCollateralMode(contextGenerate(), asset.Spot) + assert.NoError(t, err) + assert.NotNil(t, result) + + _, err = ok.GetCollateralMode(contextGenerate(), asset.Futures) + assert.True(t, errors.Is(err, nil) || errors.Is(err, asset.ErrNotSupported)) +} + +func TestSetCollateralMode(t *testing.T) { + t.Parallel() + err := ok.SetCollateralMode(contextGenerate(), asset.Spot, collateral.SingleMode) + assert.ErrorIs(t, err, common.ErrFunctionNotSupported) +} + +func TestGetPositionSummary(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + pp, err := ok.CurrencyPairs.GetPairs(asset.PerpetualSwap, true) + require.NoError(t, err) + result, err := ok.GetFuturesPositionSummary(contextGenerate(), &futures.PositionSummaryRequest{ + Asset: asset.PerpetualSwap, + Pair: pp[0], + UnderlyingPair: currency.EMPTYPAIR, + }) + require.NoError(t, err) + require.NotNil(t, result) + pp, err = ok.CurrencyPairs.GetPairs(asset.Futures, true) + require.NoError(t, err) + _, err = ok.GetFuturesPositionSummary(contextGenerate(), &futures.PositionSummaryRequest{ + Asset: asset.Spot, + Pair: pp[0], + UnderlyingPair: currency.NewBTCUSDT(), + }) + require.ErrorIsf(t, err, futures.ErrNotFuturesAsset, "received '%v', expected '%v'", err, futures.ErrNotFuturesAsset) + + result, err = ok.GetFuturesPositionSummary(contextGenerate(), &futures.PositionSummaryRequest{ + Asset: asset.Futures, + Pair: pp[0], + UnderlyingPair: currency.EMPTYPAIR, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFuturesPositions(t *testing.T) { + t.Parallel() + pp, err := ok.CurrencyPairs.GetPairs(asset.Futures, true) + require.NoError(t, err) + _, err = ok.GetFuturesPositionOrders(contextGenerate(), &futures.PositionsRequest{ + Asset: asset.Spot, + Pairs: []currency.Pair{pp[0]}, + StartDate: time.Now().Add(time.Hour * 24 * -7), + }) + require.ErrorIs(t, err, asset.ErrNotSupported) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFuturesPositionOrders(contextGenerate(), &futures.PositionsRequest{ + Asset: asset.Futures, + Pairs: []currency.Pair{pp[0]}, + StartDate: time.Now().Add(time.Hour * 24 * -7), + EndDate: time.Now(), + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetLeverage(t *testing.T) { + t.Parallel() + pp, err := ok.CurrencyPairs.GetPairs(asset.Futures, true) + require.NoError(t, err) + _, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, order.UnknownSide) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Multi, order.UnknownSide) + require.NoError(t, err) + require.NotNil(t, result) + result, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, order.Long) + require.NoError(t, err) + require.NotNil(t, result) + + result, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, order.Short) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestSetLeverage(t *testing.T) { + t.Parallel() + pp, err := ok.CurrencyPairs.GetPairs(asset.Futures, true) + require.NoError(t, err) + err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.UnknownSide) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.CouldNotBuy) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + err = ok.SetLeverage(contextGenerate(), asset.Spot, pp[0], margin.Multi, 5, order.UnknownSide) + require.ErrorIs(t, err, asset.ErrNotSupported) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Multi, 5, order.UnknownSide) + require.NoError(t, err) + err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.Long) + require.NoError(t, err) + err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.Short) + assert.NoError(t, err) +} + +func TestGetFuturesContractDetails(t *testing.T) { + t.Parallel() + _, err := ok.GetFuturesContractDetails(contextGenerate(), asset.Spot) + require.ErrorIs(t, err, futures.ErrNotFuturesAsset) + _, err = ok.GetFuturesContractDetails(contextGenerate(), asset.USDTMarginedFutures) + require.ErrorIs(t, err, asset.ErrNotSupported) + + for _, a := range []asset.Item{asset.Futures, asset.PerpetualSwap, asset.Spread} { + result, err := ok.GetFuturesContractDetails(context.Background(), a) + require.NoError(t, err) + require.NotNil(t, result) } } -// ************************** Public Channel Subscriptions ***************************** - -func TestInstrumentsSubscription(t *testing.T) { +func TestWsProcessOrderbook5(t *testing.T) { + t.Parallel() + var ob5payload = []byte(`{"arg":{"channel":"books5","instId":"OKB-USDT"},"data":[{"asks":[["0.0000007465","2290075956","0","4"],["0.0000007466","1747284705","0","4"],["0.0000007467","1338861655","0","3"],["0.0000007468","1661668387","0","6"],["0.0000007469","2715477116","0","5"]],"bids":[["0.0000007464","15693119","0","1"],["0.0000007463","2330835024","0","4"],["0.0000007462","1182926517","0","2"],["0.0000007461","3818684357","0","4"],["0.000000746","6021641435","0","7"]],"instId":"OKB-USDT","ts":"1695864901807","seqId":4826378794}]}`) + err := ok.wsProcessOrderbook5(ob5payload) + require.NoError(t, err) + + required := currency.NewPairWithDelimiter("OKB", "USDT", "-") + got, err := orderbook.Get("okx", required, asset.Spot) + require.NoError(t, err) + + require.Len(t, got.Asks, 5) + require.Len(t, got.Bids, 5) + // Book replicated to margin + got, err = orderbook.Get("okx", required, asset.Margin) + require.NoError(t, err) + require.Len(t, got.Asks, 5) + assert.Len(t, got.Bids, 5) +} + +func TestGetLeverateEstimatedInfo(t *testing.T) { + t.Parallel() + _, err := ok.GetLeverageEstimatedInfo(contextGenerate(), "", "cross", "1", "", "BTC-USDT", currency.BTC) + require.ErrorIs(t, err, errInvalidInstrumentType) + _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "", "1", "", "BTC-USDT", currency.BTC) + require.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) + _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "", "", "BTC-USDT", currency.BTC) + require.ErrorIs(t, err, errInvalidLeverage) + _, err = ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "1", "", "BTC-USDT", currency.EMPTYCODE) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLeverageEstimatedInfo(contextGenerate(), "MARGIN", "cross", "1", "", "BTC-USDT", currency.BTC) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestManualBorrowAndRepayInQuickMarginMode(t *testing.T) { + t.Parallel() + _, err := ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ + InstrumentID: "BTC-USDT", + LoanCcy: currency.USDT, + Side: "borrow"}) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + _, err = ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ + Amount: 1, + InstrumentID: "BTC-USDT", + Side: "borrow", + }) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ + Amount: 1, + InstrumentID: "BTC-USDT", + LoanCcy: currency.USDT, + }) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + _, err = ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ + Amount: 1, + LoanCcy: currency.USDT, + Side: "borrow", + }) + require.ErrorIs(t, err, errMissingInstrumentID) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ + Amount: 1, + InstrumentID: "BTC-USDT", + LoanCcy: currency.USDT, + Side: "borrow", + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetBorrowAndRepayHistoryInQuickMarginMode(t *testing.T) { t.Parallel() - if err := ok.InstrumentsSubscription("subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)); err != nil { - t.Errorf("%s InstrumentsSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetBorrowAndRepayHistoryInQuickMarginMode(contextGenerate(), currency.EMPTYPAIR, currency.BTC, "borrow", "", "", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestTickersSubscription(t *testing.T) { +func TestGetVIPInterestAccruedData(t *testing.T) { t.Parallel() - if err := ok.TickersSubscription("subscribe", asset.Margin, currency.NewPair(currency.BTC, currency.USDT)); err != nil { - t.Errorf("%s TickersSubscription() error: %v", ok.Name, err) - } - if err := ok.TickersSubscription("unsubscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)); err != nil { - t.Errorf("%s TickersSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetVIPInterestAccruedData(contextGenerate(), currency.ETH, "", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestOpenInterestSubscription(t *testing.T) { +func TestGetVIPInterestDeductedData(t *testing.T) { t.Parallel() - if err := ok.OpenInterestSubscription("subscribe", asset.PerpetualSwap, currency.NewPair(currency.BTC, currency.NewCode("USD-SWAP"))); err != nil { - t.Errorf("%s OpenInterestSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetVIPInterestDeductedData(contextGenerate(), currency.ETH, "", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestCandlesticksSubscription(t *testing.T) { + +func TestGetVIPLoanOrderList(t *testing.T) { t.Parallel() - enabled, err := ok.GetEnabledPairs(asset.PerpetualSwap) - if err != nil { - t.Error("couldn't find enabled tradable pairs") - } - if len(enabled) == 0 { - t.SkipNow() - } - if err := ok.CandlesticksSubscription("subscribe", okxChannelCandle1m, asset.Futures, enabled[0]); err != nil { - t.Errorf("%s CandlesticksSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetVIPLoanOrderList(contextGenerate(), "", "1", currency.BTC, time.Time{}, time.Now(), 20) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestTradesSubscription(t *testing.T) { +func TestGetVIPLoanOrderDetail(t *testing.T) { t.Parallel() - if err := ok.TradesSubscription("subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)); err != nil { - t.Errorf("%s TradesSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetVIPLoanOrderDetail(contextGenerate(), "123456", currency.BTC, time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) } - -func TestEstimatedDeliveryExercisePriceSubscription(t *testing.T) { +func TestSetRiskOffsetType(t *testing.T) { t.Parallel() - futuresPairs, err := ok.FetchTradablePairs(contextGenerate(), asset.Futures) - if err != nil { - t.Errorf("%s error while fetching tradable pairs for instrument type %v: %v", ok.Name, asset.Futures, err) - } - if len(futuresPairs) == 0 { - t.SkipNow() - } - if err := ok.EstimatedDeliveryExercisePriceSubscription("subscribe", asset.Futures, futuresPairs[0]); err != nil { - t.Errorf("%s EstimatedDeliveryExercisePriceSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetRiskOffsetType(contextGenerate(), "3") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestMarkPriceSubscription(t *testing.T) { +func TestActivateOption(t *testing.T) { t.Parallel() - futuresPairs, err := ok.FetchTradablePairs(contextGenerate(), asset.Futures) - if err != nil { - t.Errorf("%s error while fetching tradable pairs for instrument type %v: %v", ok.Name, asset.Futures, err) - } - if len(futuresPairs) == 0 { - t.SkipNow() - } - if err := ok.MarkPriceSubscription("subscribe", asset.Futures, futuresPairs[0]); err != nil { - t.Errorf("%s MarkPriceSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.ActivateOption(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestMarkPriceCandlesticksSubscription(t *testing.T) { +func TestSetAutoLoan(t *testing.T) { t.Parallel() - enabled, err := ok.GetEnabledPairs(asset.Spot) - if err != nil { - t.Error("couldn't find enabled tradable pairs") - } - if len(enabled) == 0 { - t.SkipNow() - } - if err := ok.MarkPriceCandlesticksSubscription("subscribe", okxChannelMarkPriceCandle1Y, asset.Futures, enabled[0]); err != nil { - t.Errorf("%s MarkPriceCandlesticksSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.SetAutoLoan(contextGenerate(), true) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestPriceLimitSubscription(t *testing.T) { +func TestSetAccountMode(t *testing.T) { t.Parallel() - if err := ok.PriceLimitSubscription("subscribe", currency.Pair{Base: currency.NewCode("BTC"), Quote: currency.NewCode("USDT-SWAP")}); err != nil { - t.Errorf("%s PriceLimitSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetAccountMode(contextGenerate(), "1") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestOrderBooksSubscription(t *testing.T) { +func TestResetMMPStatus(t *testing.T) { t.Parallel() - enabled, err := ok.GetEnabledPairs(asset.Spot) - if err != nil { - t.Error("couldn't find enabled tradable pairs") - } - if len(enabled) == 0 { - t.SkipNow() - } - if err := ok.OrderBooksSubscription("subscribe", okxChannelOrderBooks, asset.Futures, enabled[0]); err != nil { - t.Errorf("%s OrderBooksSubscription() error: %v", ok.Name, err) - } - if err := ok.OrderBooksSubscription("unsubscribe", okxChannelOrderBooks, asset.Futures, enabled[0]); err != nil { - t.Errorf("%s OrderBooksSubscription() error: %v", ok.Name, err) - } + _, err := ok.ResetMMPStatus(contextGenerate(), instTypeOption, "") + require.ErrorIs(t, err, errInstrumentFamilyRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ResetMMPStatus(contextGenerate(), instTypeOption, "BTC-USD") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestOptionSummarySubscription(t *testing.T) { +func TestSetMMP(t *testing.T) { t.Parallel() - if err := ok.OptionSummarySubscription("subscribe", currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s OptionSummarySubscription() error: %v", ok.Name, err) - } - if err := ok.OptionSummarySubscription("unsubscribe", currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s OptionSummarySubscription() error: %v", ok.Name, err) - } + _, err := ok.SetMMP(contextGenerate(), &MMPConfig{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.SetMMP(contextGenerate(), &MMPConfig{ + TimeInterval: 5000, + }) + require.ErrorIs(t, err, errInstrumentFamilyRequired) + _, err = ok.SetMMP(contextGenerate(), &MMPConfig{ + InstrumentFamily: "BTC-USD", + }) + require.ErrorIs(t, err, errInvalidQuantityLimit) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetMMP(contextGenerate(), &MMPConfig{ + InstrumentFamily: "BTC-USD", + TimeInterval: 5000, + FrozenInterval: 2000, + QuantityLimit: 100, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestFundingRateSubscription(t *testing.T) { +func TestGetMMPConfig(t *testing.T) { t.Parallel() - if err := ok.FundingRateSubscription("subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP"))); err != nil { - t.Errorf("%s FundingRateSubscription() error: %v", ok.Name, err) - } - if err := ok.FundingRateSubscription("unsubscribe", asset.Spot, currency.NewPair(currency.BTC, currency.NewCode("USDT-SWAP"))); err != nil { - t.Errorf("%s FundingRateSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetMMPConfig(contextGenerate(), "BTC-USD") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestIndexCandlesticksSubscription(t *testing.T) { +func TestMassCancelOrder(t *testing.T) { t.Parallel() - if err := ok.IndexCandlesticksSubscription("subscribe", okxChannelIndexCandle6M, asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s IndexCandlesticksSubscription() error: %v", ok.Name, err) - } - if err := ok.IndexCandlesticksSubscription("unsubscribe", okxChannelIndexCandle6M, asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s IndexCandlesticksSubscription() error: %v", ok.Name, err) - } + _, err := ok.CancelAllMMPOrders(contextGenerate(), "", "BTC-USD", 2000) + require.ErrorIs(t, err, errInvalidInstrumentType) + _, err = ok.CancelAllMMPOrders(contextGenerate(), "OPTION", "", 2000) + require.ErrorIs(t, err, errInstrumentFamilyRequired) + _, err = ok.CancelAllMMPOrders(contextGenerate(), "OPTION", "BTC-USD", -1) + require.ErrorIs(t, err, errMissingIntervalValue) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelAllMMPOrders(contextGenerate(), "OPTION", "BTC-USD", 2000) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestIndexTickerChannelIndexTickerChannel(t *testing.T) { + +func TestCancelAllDelayed(t *testing.T) { t.Parallel() - if err := ok.IndexTickerChannel("subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s IndexTickerChannel() error: %v", ok.Name, err) - } - if err := ok.IndexTickerChannel("unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s IndexTickerChannel() error: %v", ok.Name, err) - } + _, err := ok.CancelAllDelayed(contextGenerate(), 2, "") + require.ErrorIs(t, err, errCountdownTimeoutRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelAllDelayed(contextGenerate(), 60, "") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestStatusSubscription(t *testing.T) { +func TestGetTradeAccountRateLimit(t *testing.T) { t.Parallel() - if err := ok.StatusSubscription("subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s StatusSubscription() error: %v", ok.Name, err) - } - if err := ok.StatusSubscription("unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s StatusSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetTradeAccountRateLimit(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestPublicStructureBlockTradesSubscription(t *testing.T) { +func TestPreCheckOrder(t *testing.T) { t.Parallel() - if err := ok.PublicStructureBlockTradesSubscription("subscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s PublicStructureBlockTradesSubscription() error: %v", ok.Name, err) - } - if err := ok.PublicStructureBlockTradesSubscription("unsubscribe", asset.Spot, currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s PublicStructureBlockTradesSubscription() error: %v", ok.Name, err) + _, err := ok.PreCheckOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) + + arg := &OrderPreCheckParams{ + ClientOrderID: "b15", } + _, err = ok.PreCheckOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + + arg.InstrumentID = "BTC-USDT" + _, err = ok.PreCheckOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + arg.TradeMode = "cash" + _, err = ok.PreCheckOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + arg.Side = "buy" + _, err = ok.PreCheckOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) + + arg.OrderType = "limit" + _, err = ok.PreCheckOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PreCheckOrder(contextGenerate(), &OrderPreCheckParams{ + InstrumentID: "BTC-USDT", + TradeMode: "cash", + ClientOrderID: "b15", + Side: order.Buy.Lower(), + OrderType: "limit", + Price: 2.15, + Size: 2, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestBlockTickerSubscription(t *testing.T) { + +func TestAmendAlgoOrder(t *testing.T) { t.Parallel() - if err := ok.BlockTickerSubscription("subscribe", asset.Options, currency.NewPair(currency.BTC, currency.USDT)); err != nil { - t.Errorf("%s BlockTickerSubscription() error: %v", ok.Name, err) - } - if err := ok.BlockTickerSubscription("unsubscribe", asset.Options, currency.NewPair(currency.BTC, currency.USDT)); err != nil { - t.Errorf("%s BlockTickerSubscription() error: %v", ok.Name, err) - } -} + _, err := ok.AmendAlgoOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrEmptyParams) -// ************ Authenticated Websocket endpoints Test ********************************************** + _, err = ok.AmendAlgoOrder(contextGenerate(), &AmendAlgoOrderParam{NewSize: 2}) + require.ErrorIs(t, err, errMissingInstrumentID) + _, err = ok.AmendAlgoOrder(contextGenerate(), &AmendAlgoOrderParam{ + InstrumentID: perpetualSwapTP.String(), + NewSize: 2, + }) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) -func TestWsAccountSubscription(t *testing.T) { + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendAlgoOrder(contextGenerate(), &AmendAlgoOrderParam{ + AlgoID: "2510789768709120", + InstrumentID: perpetualSwapTP.String(), + NewSize: 2, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetAlgoOrderDetail(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetAlgoOrderDetail(contextGenerate(), "", "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if err := ok.WsAccountSubscription("subscribe", asset.Spot, currency.NewPair(currency.BTC, currency.USDT)); err != nil { - t.Errorf("%s WsAccountSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetAlgoOrderDetail(contextGenerate(), "1234231231423", "") + require.NoError(t, err) + assert.NotNil(t, result) } -const placeOrderJSON = `{ "id": "1512", "op": "order", "args": [{ "instId":"BTC-USDC", "tdMode":"cash", "clOrdId":"b15", "side":"Buy", "ordType":"limit", "px":"2.15", "sz":"2"} ]}` - -func TestWsPlaceOrder(t *testing.T) { +func TestClosePositionForContractID(t *testing.T) { t.Parallel() + _, err := ok.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{AlgoID: "", MarketCloseAllPositions: true}) + require.ErrorIs(t, err, errAlgoIDRequired) + _, err = ok.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{AlgoID: "448965992920907776", MarketCloseAllPositions: false}) + require.ErrorIs(t, err, order.ErrAmountMustBeSet) + _, err = ok.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{AlgoID: "448965992920907776", MarketCloseAllPositions: false, Size: 123}) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{ + AlgoID: "448965992920907776", + MarketCloseAllPositions: true, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} - var resp WsPlaceOrderInput - err := json.Unmarshal([]byte(placeOrderArgs), &resp) - if err != nil { - t.Error(err) - } - var response OrderData - err = json.Unmarshal([]byte(placeOrderJSON), &response) - if err != nil { - t.Error(err) - } +func TestCancelClosePositionOrderForContractGrid(t *testing.T) { + t.Parallel() + _, err := ok.CancelClosePositionOrderForContractGrid(contextGenerate(), &CancelClosePositionOrder{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.CancelClosePositionOrderForContractGrid(contextGenerate(), &CancelClosePositionOrder{OrderID: "570627699870375936"}) + require.ErrorIs(t, err, errAlgoIDRequired) + _, err = ok.CancelClosePositionOrderForContractGrid(contextGenerate(), &CancelClosePositionOrder{AlgoID: "448965992920907776"}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.WsPlaceOrder(&PlaceOrderRequestParam{ - InstrumentID: "BTC-USDC", - TradeMode: "cross", - Side: "Buy", - OrderType: "limit", - Amount: 2.6, - Price: 2.1, - Currency: "BTC", - }); err != nil { - t.Errorf("%s WsPlaceOrder() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelClosePositionOrderForContractGrid(contextGenerate(), &CancelClosePositionOrder{ + AlgoID: "448965992920907776", + OrderID: "570627699870375936", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -const placeOrderArgs = `{ "id": "1513", "op": "batch-orders", "args": [ { "side": "buy", "instId": "BTC-USDT", "tdMode": "cash", "ordType": "market", "sz": "100" }, { "side": "buy", "instId": "LTC-USDT", "tdMode": "cash", "ordType": "market", "sz": "1" } ]}` - -func TestWsPlaceMultipleOrder(t *testing.T) { +func TestInstantTriggerGridAlgoOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.InstantTriggerGridAlgoOrder(contextGenerate(), "123456789") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestComputeMinInvestment(t *testing.T) { + t.Parallel() + arg := &ComputeInvestmentDataParam{ + RunType: "1", + } + _, err := ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) + arg.InstrumentID = "ETH-USDT" + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidAlgoOrderType) + arg.AlgoOrderType = "grid" + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.MaxPrice = 5000 + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + + arg.MinPrice = 5000 + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidGridQuantity) + + arg.GridNumber = 1234 + arg.RunType = "" + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, errRunTypeRequired) + + arg.RunType = "1" + arg.AlgoOrderType = "contract_grid" + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingRequiredArgumentDirection) + + arg.Direction = positionSideLong + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidLeverage) + + arg.Leverage = 5 + arg.InvestmentData = []InvestmentData{{Currency: currency.ETH}} + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.InvestmentData = []InvestmentData{{Amount: 0.01}} + _, err = ok.ComputeMinInvestment(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + result, err := ok.ComputeMinInvestment(contextGenerate(), &ComputeInvestmentDataParam{ + InstrumentID: "ETH-USDT", + AlgoOrderType: "grid", + GridNumber: 50, + MaxPrice: 5000, + MinPrice: 3000, + RunType: "1", + InvestmentData: []InvestmentData{ + { + Amount: 0.01, + Currency: currency.ETH, + }, + { + Amount: 100, + Currency: currency.USDT, + }, + }, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} - var resp WsPlaceOrderInput - if err := json.Unmarshal([]byte(placeOrderArgs), &resp); err != nil { - t.Error(err) - } - pairs, err := ok.FetchTradablePairs(contextGenerate(), asset.Spot) - if err != nil { - t.Fatal(err) - } else if len(pairs) == 0 { - t.Skip("no pairs found") - } - if _, err := ok.WsPlaceMultipleOrder(resp.Arguments); err != nil { - t.Error("Okx WsPlaceMultipleOrder() error", err) - } +func TestRSIBackTesting(t *testing.T) { + t.Parallel() + _, err := ok.RSIBackTesting(contextGenerate(), "", "", "", 50, 14, kline.FiveMin) + require.ErrorIs(t, err, errMissingInstrumentID) + result, err := ok.RSIBackTesting(contextGenerate(), "BTC-USDT", "", "", 50, 14, kline.FiveMin) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestWsCancelOrder(t *testing.T) { +func TestSignalBotTrading(t *testing.T) { t.Parallel() + _, err := ok.GetSignalBotOrderDetail(contextGenerate(), "", "623833708424069120") + require.ErrorIs(t, err, errInvalidAlgoOrderType) + _, err = ok.GetSignalBotOrderDetail(contextGenerate(), "contract", "") + require.ErrorIs(t, err, errAlgoIDRequired) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.GetSignalBotOrderDetail(contextGenerate(), "contract", "623833708424069120") + require.NoError(t, err) + assert.NotNil(t, result) +} - if _, err := ok.WsCancelOrder(CancelOrderRequestParam{ - InstrumentID: "BTC-USD-190927", - OrderID: "2510789768709120", - }); err != nil { - t.Error("Okx WsCancelOrder() error", err) - } +func TestGetSignalOrderPositions(t *testing.T) { + t.Parallel() + _, err := ok.GetSignalOrderPositions(contextGenerate(), "", "623833708424069120") + require.ErrorIs(t, err, errInvalidAlgoOrderType) + _, err = ok.GetSignalOrderPositions(contextGenerate(), "contract", "") + require.ErrorIs(t, err, errAlgoIDRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSignalOrderPositions(contextGenerate(), "contract", "623833708424069120") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestWsCancleMultipleOrder(t *testing.T) { +func TestGetSignalBotSubOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.GetSignalBotSubOrders(contextGenerate(), "", "contract", "filled", "", "", "", time.Time{}, time.Time{}, 0) + require.ErrorIs(t, err, errAlgoIDRequired) + _, err = ok.GetSignalBotSubOrders(contextGenerate(), "623833708424069120", "", "filled", "", "", "", time.Time{}, time.Time{}, 0) + require.ErrorIs(t, err, errInvalidAlgoOrderType) + _, err = ok.GetSignalBotSubOrders(contextGenerate(), "623833708424069120", "contract", "", "", "", "", time.Time{}, time.Time{}, 0) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) - if _, err := ok.WsCancelMultipleOrder([]CancelOrderRequestParam{{ - InstrumentID: "DCR-BTC", - OrderID: "2510789768709120", - }}); err != nil && !strings.Contains(err.Error(), "Cancellation failed as the order does not exist.") { - t.Error("Okx WsCancleMultipleOrder() error", err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSignalBotSubOrders(contextGenerate(), "623833708424069120", "contract", "filled", "", "", "", time.Time{}, time.Time{}, 0) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestWsAmendOrder(t *testing.T) { +func TestGetSignalBotEventHistory(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.GetSignalBotEventHistory(contextGenerate(), "", time.Time{}, time.Now(), 50) + require.ErrorIs(t, err, errAlgoIDRequired) - if _, err := ok.WsAmendOrder(&AmendOrderRequestParams{ - InstrumentID: "DCR-BTC", - OrderID: "2510789768709120", - NewPrice: 1233324.332, - NewQuantity: 1234, - }); err != nil && !strings.Contains(err.Error(), "order does not exist.") { - t.Errorf("%s WsAmendOrder() error %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSignalBotEventHistory(contextGenerate(), "12345", time.Time{}, time.Now(), 50) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestWsAmendMultipleOrders(t *testing.T) { +func TestPlaceRecurringBuyOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + _, err := ok.PlaceRecurringBuyOrder(contextGenerate(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) - if _, err := ok.WsAmendMultipleOrders([]AmendOrderRequestParams{ - { - InstrumentID: "DCR-BTC", - OrderID: "2510789768709120", - NewPrice: 1233324.332, - NewQuantity: 1234, - }, - }); err != nil && !strings.Contains(err.Error(), "Order modification failed as the order does not exist.") { - t.Errorf("%s WsAmendMultipleOrders() %v", ok.Name, err) + arg := &PlaceRecurringBuyOrderParam{ + TimeZone: "3", } + _, err = ok.PlaceRecurringBuyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errStrategyNameRequired) + + arg.StrategyName = "BTC|ETH recurring buy monthly" + _, err = ok.PlaceRecurringBuyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg.RecurringList = []RecurringListItem{{}} + _, err = ok.PlaceRecurringBuyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + arg.RecurringList = []RecurringListItem{{Currency: currency.BTC}} + _, err = ok.PlaceRecurringBuyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errRecurringDayRequired) + + arg.RecurringDay = "1" + arg.RecurringTime = -10 + _, err = ok.PlaceRecurringBuyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errRecurringBuyTimeRequired) + + arg.RecurringTime = 2 + _, err = ok.PlaceRecurringBuyOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errInvalidTradeModeValue) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceRecurringBuyOrder(contextGenerate(), &PlaceRecurringBuyOrderParam{ + StrategyName: "BTC|ETH recurring buy monthly", + Amount: 100, + RecurringList: []RecurringListItem{ + { + Currency: currency.BTC, + Ratio: 0.2, + }, + { + Currency: currency.ETH, + Ratio: 0.8, + }, + }, + Period: "monthly", + RecurringDay: "1", + RecurringTime: 0, + TimeZone: "8", // UTC +8 + TradeMode: "cross", + InvestmentCurrency: "USDT", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestWsPositionChannel(t *testing.T) { +func TestAmendRecurringBuyOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.AmendRecurringBuyOrder(contextGenerate(), &AmendRecurringOrderParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.AmendRecurringBuyOrder(contextGenerate(), &AmendRecurringOrderParam{StrategyName: "stg1"}) + require.ErrorIs(t, err, errAlgoIDRequired) + _, err = ok.AmendRecurringBuyOrder(contextGenerate(), &AmendRecurringOrderParam{AlgoID: "448965992920907776"}) + require.ErrorIs(t, err, errStrategyNameRequired) - if err := ok.WsPositionChannel("subscribe", asset.Options, currency.NewPair(currency.USD, currency.BTC)); err != nil { - t.Errorf("%s WsPositionChannel() error : %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendRecurringBuyOrder(contextGenerate(), &AmendRecurringOrderParam{ + AlgoID: "448965992920907776", + StrategyName: "stg1", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestBalanceAndPositionSubscription(t *testing.T) { +func TestStopRecurringBuyOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.StopRecurringBuyOrder(contextGenerate(), []StopRecurringBuyOrder{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.StopRecurringBuyOrder(contextGenerate(), []StopRecurringBuyOrder{{}}) + require.ErrorIs(t, err, errAlgoIDRequired) - if err := ok.BalanceAndPositionSubscription("subscribe", "1234"); err != nil { - t.Errorf("%s BalanceAndPositionSubscription() error %v", ok.Name, err) - } - if err := ok.BalanceAndPositionSubscription("unsubscribe", "1234"); err != nil { - t.Errorf("%s BalanceAndPositionSubscription() error %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.StopRecurringBuyOrder(contextGenerate(), []StopRecurringBuyOrder{{AlgoID: "1232323434234"}}) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestWsOrderChannel(t *testing.T) { +func TestGetRecurringBuyOrderList(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - - if err := ok.WsOrderChannel("subscribe", asset.Margin, currency.NewPair(currency.SOL, currency.USDT), ""); err != nil { - t.Errorf("%s WsOrderChannel() error: %v", ok.Name, err) - } - if err := ok.WsOrderChannel("unsubscribe", asset.Margin, currency.NewPair(currency.SOL, currency.USDT), ""); err != nil { - t.Errorf("%s WsOrderChannel() error: %v", ok.Name, err) - } + result, err := ok.GetRecurringBuyOrderList(contextGenerate(), "", "paused", time.Time{}, time.Time{}, 30) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestAlgoOrdersSubscription(t *testing.T) { +func TestGetRecurringBuyOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetRecurringBuyOrderHistory(contextGenerate(), "", time.Time{}, time.Time{}, 30) + require.NoError(t, err) + assert.NotNil(t, result) +} - if err := ok.AlgoOrdersSubscription("subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))); err != nil { - t.Errorf("%s AlgoOrdersSubscription() error: %v", ok.Name, err) - } - if err := ok.AlgoOrdersSubscription("unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))); err != nil { - t.Errorf("%s AlgoOrdersSubscription() error: %v", ok.Name, err) - } +func TestGetRecurringOrderDetails(t *testing.T) { + t.Parallel() + _, err := ok.GetRecurringOrderDetails(contextGenerate(), "", "") + require.ErrorIs(t, err, errAlgoIDRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetRecurringOrderDetails(contextGenerate(), "560473220642766848", "") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestAdvanceAlgoOrdersSubscription(t *testing.T) { +func TestGetRecurringSubOrders(t *testing.T) { t.Parallel() + _, err := ok.GetRecurringSubOrders(contextGenerate(), "", "123422", time.Time{}, time.Now(), 0) + require.ErrorIs(t, err, errAlgoIDRequired) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetRecurringSubOrders(contextGenerate(), "560473220642766848", "123422", time.Time{}, time.Now(), 0) + require.NoError(t, err) + assert.NotNil(t, result) +} - if err := ok.AdvanceAlgoOrdersSubscription("subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP")), ""); err != nil { - t.Errorf("%s AdvanceAlgoOrdersSubscription() error: %v", ok.Name, err) - } - if err := ok.AdvanceAlgoOrdersSubscription("unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP")), ""); err != nil { - t.Errorf("%s AdvanceAlgoOrdersSubscription() error: %v", ok.Name, err) - } +func TestGetExistingLeadingPositions(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetExistingLeadingPositions(contextGenerate(), instTypeSpot, "BTC-USDT", time.Now(), time.Time{}, 0) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestPositionRiskWarningSubscription(t *testing.T) { +func TestGetLeadingPositionsHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLeadingPositionsHistory(contextGenerate(), "OPTION", "", time.Time{}, time.Time{}, 0) + require.NoError(t, err) + assert.NotNil(t, result) +} - if err := ok.PositionRiskWarningSubscription("subscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))); err != nil { - t.Errorf("%s PositionRiskWarningSubscription() error: %v", ok.Name, err) - } - if err := ok.PositionRiskWarningSubscription("unsubscribe", asset.PerpetualSwap, currency.NewPair(currency.SOL, currency.NewCode("USD-SWAP"))); err != nil { - t.Errorf("%s PositionRiskWarningSubscription() error: %v", ok.Name, err) - } +func TestPlaceLeadingStopOrder(t *testing.T) { + t.Parallel() + arg := &TPSLOrderParam{} + _, err := ok.PlaceLeadingStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, common.ErrEmptyParams) + + arg.Tag = "1235454" + _, err = ok.PlaceLeadingStopOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errSubPositionIDRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceLeadingStopOrder(contextGenerate(), &TPSLOrderParam{ + SubPositionID: "1235454", + TakeProfitTriggerPrice: 123455, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestAccountGreeksSubscription(t *testing.T) { +func TestCloseLeadingPosition(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.CloseLeadingPosition(contextGenerate(), &CloseLeadingPositionParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.CloseLeadingPosition(contextGenerate(), &CloseLeadingPositionParam{Tag: "tag-here"}) + require.ErrorIs(t, err, errSubPositionIDRequired) - if err := ok.AccountGreeksSubscription("subscribe", currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s AccountGreeksSubscription() error: %v", ok.Name, err) - } - if err := ok.AccountGreeksSubscription("unsubscribe", currency.NewPair(currency.SOL, currency.USD)); err != nil { - t.Errorf("%s AccountGreeksSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CloseLeadingPosition(contextGenerate(), &CloseLeadingPositionParam{ + SubPositionID: "518541406042591232", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestRfqSubscription(t *testing.T) { +func TestGetLeadingInstrument(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLeadingInstrument(contextGenerate(), "SWAP") + require.NoError(t, err) + assert.NotNil(t, result) +} - if err := ok.RfqSubscription("subscribe", ""); err != nil { - t.Errorf("%s RfqSubscription() error: %v", ok.Name, err) - } - if err := ok.RfqSubscription("unsubscribe", ""); err != nil { - t.Errorf("%s RfqSubscription() error: %v", ok.Name, err) - } +func TestAmendLeadingInstruments(t *testing.T) { + t.Parallel() + _, err := ok.AmendLeadingInstruments(contextGenerate(), "", "") + require.ErrorIs(t, err, errMissingInstrumentID) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendLeadingInstruments(contextGenerate(), "BTC-USDT-SWAP", "") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestQuotesSubscription(t *testing.T) { +func TestGetProfitSharingDetails(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetProfitSharingDetails(contextGenerate(), "", time.Now(), time.Time{}, 0) + require.NoError(t, err) + assert.NotNil(t, result) +} - if err := ok.QuotesSubscription("subscribe"); err != nil { - t.Errorf("%s QuotesSubscription() error: %v", ok.Name, err) - } - if err := ok.QuotesSubscription("unsubscribe"); err != nil { - t.Errorf("%s QuotesSubscription() error: %v", ok.Name, err) - } +func TestGetTotalProfitSharing(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetTotalProfitSharing(contextGenerate(), "SWAP") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestStructureBlockTradesSubscription(t *testing.T) { +func TestGetUnrealizedProfitSharingDetails(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetUnrealizedProfitSharingDetails(contextGenerate(), "SWAP") + require.NoError(t, err) + assert.NotNil(t, result) +} - if err := ok.StructureBlockTradesSubscription("subscribe"); err != nil { - t.Errorf("%s StructureBlockTradesSubscription() error: %v", ok.Name, err) - } - if err := ok.StructureBlockTradesSubscription("unsubscribe"); err != nil { - t.Errorf("%s StructureBlockTradesSubscription() error: %v", ok.Name, err) - } +func TestSetFirstCopySettings(t *testing.T) { + t.Parallel() + _, err := ok.AmendCopySettings(contextGenerate(), &FirstCopySettings{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendCopySettings(contextGenerate(), &FirstCopySettings{ + InstrumentType: "SWAP", + UniqueCode: "25CD5A80241D6FE6", + CopyMarginMode: "cross", + CopyInstrumentIDType: "copy", + CopyMode: "ratio_copy", + CopyRatio: 1, + CopyTotalAmount: 500, + SubPosCloseType: "copy_close", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestSpotGridAlgoOrdersSubscription(t *testing.T) { +func TestAmendCopySettings(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.SetFirstCopySettings(contextGenerate(), &FirstCopySettings{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - if err := ok.SpotGridAlgoOrdersSubscription("subscribe", asset.Empty, currency.EMPTYPAIR, ""); err != nil { - t.Errorf("%s SpotGridAlgoOrdersSubscription() error: %v", ok.Name, err) - } - if err := ok.SpotGridAlgoOrdersSubscription("unsubscribe", asset.Empty, currency.EMPTYPAIR, ""); err != nil { - t.Errorf("%s SpotGridAlgoOrdersSubscription() error: %v", ok.Name, err) + arg := &FirstCopySettings{ + CopyMode: "ratio_copy", } + _, err = ok.SetFirstCopySettings(contextGenerate(), arg) + require.ErrorIs(t, err, errUniqueCodeRequired) + + arg.UniqueCode = "25CD5A80241D6FE6" + _, err = ok.SetFirstCopySettings(contextGenerate(), arg) + require.ErrorIs(t, err, errCopyInstrumentIDTypeRequired) + + arg.CopyInstrumentIDType = "copy" + _, err = ok.SetFirstCopySettings(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.CopyTotalAmount = 500 + _, err = ok.SetFirstCopySettings(contextGenerate(), arg) + require.ErrorIs(t, err, errSubPositionCloseTypeRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.SetFirstCopySettings(contextGenerate(), &FirstCopySettings{ + InstrumentType: "SWAP", + UniqueCode: "25CD5A80241D6FE6", + CopyMarginMode: "cross", + CopyInstrumentIDType: "copy", + CopyMode: "ratio_copy", + CopyRatio: 1, + CopyTotalAmount: 500, + SubPosCloseType: "copy_close", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestContractGridAlgoOrders(t *testing.T) { +func TestStopCopying(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.StopCopying(contextGenerate(), &StopCopyingParameter{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - if err := ok.ContractGridAlgoOrders("subscribe", asset.Empty, currency.EMPTYPAIR, ""); err != nil { - t.Errorf("%s ContractGridAlgoOrders() error: %v", ok.Name, err) - } - if err := ok.ContractGridAlgoOrders("unsubscribe", asset.Empty, currency.EMPTYPAIR, ""); err != nil { - t.Errorf("%s ContractGridAlgoOrders() error: %v", ok.Name, err) - } + _, err = ok.StopCopying(contextGenerate(), &StopCopyingParameter{ + InstrumentType: "SWAP", + SubPositionCloseType: "manual_close", + }) + require.ErrorIs(t, err, errUniqueCodeRequired) + _, err = ok.StopCopying(contextGenerate(), &StopCopyingParameter{InstrumentType: "SWAP", + UniqueCode: "25CD5A80241D6FE6"}) + require.ErrorIs(t, err, errSubPositionCloseTypeRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.StopCopying(contextGenerate(), &StopCopyingParameter{ + InstrumentType: "SWAP", + UniqueCode: "25CD5A80241D6FE6", + SubPositionCloseType: "manual_close", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGridPositionsSubscription(t *testing.T) { +func TestGetCopySettings(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + _, err := ok.GetCopySettings(contextGenerate(), "SWAP", "") + require.ErrorIs(t, err, errUniqueCodeRequired) - if err := ok.GridPositionsSubscription("subscribe", "1234"); err != nil && !strings.Contains(err.Error(), "channel:grid-positions doesn't exist") { - t.Errorf("%s GridPositionsSubscription() error: %v", ok.Name, err) - } - if err := ok.GridPositionsSubscription("unsubscribe", "1234"); err != nil && !strings.Contains(err.Error(), "channel:grid-positions doesn't exist") { - t.Errorf("%s GridPositionsSubscription() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetCopySettings(contextGenerate(), "SWAP", "213E8C92DC61EFAC") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGridSubOrders(t *testing.T) { +func TestGetMultipleLeverages(t *testing.T) { t.Parallel() + _, err := ok.GetMultipleLeverages(contextGenerate(), "", "213E8C92DC61EFAC", "") + require.ErrorIs(t, err, margin.ErrInvalidMarginType) + _, err = ok.GetMultipleLeverages(contextGenerate(), "isolated", "", "") + require.ErrorIs(t, err, errUniqueCodeRequired) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetMultipleLeverages(contextGenerate(), "isolated", "213E8C92DC61EFAC", "") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestSetMultipleLeverages(t *testing.T) { + t.Parallel() + _, err := ok.SetMultipleLeverages(contextGenerate(), &SetLeveragesParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.SetMultipleLeverages(contextGenerate(), &SetLeveragesParam{Leverage: 5}) + require.ErrorIs(t, err, margin.ErrInvalidMarginType) + _, err = ok.SetMultipleLeverages(contextGenerate(), &SetLeveragesParam{MarginMode: "cross"}) + require.ErrorIs(t, err, errInvalidLeverage) + _, err = ok.SetMultipleLeverages(contextGenerate(), &SetLeveragesParam{ + MarginMode: "cross", + Leverage: 5, + }) + require.ErrorIs(t, err, errMissingInstrumentID) - if err := ok.GridSubOrders("subscribe", ""); err != nil && !strings.Contains(err.Error(), "grid-sub-orders doesn't exist") { - t.Errorf("%s GridSubOrders() error: %v", ok.Name, err) - } - if err := ok.GridSubOrders("unsubscribe", ""); err != nil && !strings.Contains(err.Error(), "grid-sub-orders doesn't exist") { - t.Errorf("%s GridSubOrders() error: %v", ok.Name, err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.SetMultipleLeverages(contextGenerate(), &SetLeveragesParam{ + MarginMode: "cross", + Leverage: 5, + InstrumentID: "BTC-USDT-SWAP", + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetServerTime(t *testing.T) { +func TestGetMyLeadTraders(t *testing.T) { t.Parallel() - if _, err := ok.GetServerTime(contextGenerate(), asset.Empty); err != nil { - t.Error(err) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetMyLeadTraders(contextGenerate(), "SWAP") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetAvailableTransferChains(t *testing.T) { +func TestGetHistoryLeadTraders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - if _, err := ok.GetAvailableTransferChains(contextGenerate(), currency.BTC); err != nil { - t.Error(err) - } + result, err := ok.GetHistoryLeadTraders(contextGenerate(), "", "", "", 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetIntervalEnum(t *testing.T) { +func TestGetWeeklyTraderProfitAndLoss(t *testing.T) { t.Parallel() + _, err := ok.GetWeeklyTraderProfitAndLoss(contextGenerate(), "", "") + require.ErrorIs(t, err, errUniqueCodeRequired) - tests := []struct { - Description string - Interval kline.Interval - Expected string - AppendUTC bool - }{ - {Description: "4hr with UTC", Interval: kline.FourHour, Expected: "4H", AppendUTC: true}, - {Description: "6H without UTC", Interval: kline.SixHour, Expected: "6H"}, - {Description: "6H with UTC", Interval: kline.SixHour, Expected: "6Hutc", AppendUTC: true}, - {Description: "Unsupported interval with UTC", Expected: "", AppendUTC: true}, - } + mainResult, err := ok.GetWeeklyTraderProfitAndLoss(contextGenerate(), "", leadTraderUniqueID) + require.NoError(t, err) + assert.NotNil(t, mainResult) +} - for _, tt := range tests { - t.Run(tt.Description, func(t *testing.T) { - t.Parallel() +func TestGetDailyLeadTraderPNL(t *testing.T) { + t.Parallel() + _, err := ok.GetDailyLeadTraderPNL(contextGenerate(), "SWAP", "", "2") + require.ErrorIs(t, err, errUniqueCodeRequired) + _, err = ok.GetDailyLeadTraderPNL(contextGenerate(), "SWAP", leadTraderUniqueID, "") + require.ErrorIs(t, err, errLastDaysRequired) - if r := ok.GetIntervalEnum(tt.Interval, tt.AppendUTC); r != tt.Expected { - t.Errorf("%s: received: %s but expected: %s", tt.Description, r, tt.Expected) - } - }) - } + mainResult, err := ok.GetDailyLeadTraderPNL(contextGenerate(), "SWAP", leadTraderUniqueID, "2") + require.NoError(t, err) + assert.NotNil(t, mainResult) } -const instrumentJSON = `{"alias":"","baseCcy":"","category":"1","ctMult":"1","ctType":"linear","ctVal":"0.0001","ctValCcy":"BTC","expTime":"","instFamily":"BTC-USDC","instId":"BTC-USDC-SWAP","instType":"SWAP","lever":"125","listTime":"1666076190000","lotSz":"1","maxIcebergSz":"100000000.0000000000000000","maxLmtSz":"100000000","maxMktSz":"85000","maxStopSz":"85000","maxTriggerSz":"100000000.0000000000000000","maxTwapSz":"","minSz":"1","optType":"","quoteCcy":"","settleCcy":"USDC","state":"live","stk":"","tickSz":"0.1","uly":"BTC-USDC"}` - -func TestInstrument(t *testing.T) { +func TestGetLeadTraderStats(t *testing.T) { t.Parallel() + _, err := ok.GetLeadTraderStats(contextGenerate(), "SWAP", "", "2") + require.ErrorIs(t, err, errUniqueCodeRequired) + _, err = ok.GetLeadTraderStats(contextGenerate(), "SWAP", leadTraderUniqueID, "") + require.ErrorIs(t, err, errLastDaysRequired) - var i Instrument - err := json.Unmarshal([]byte(instrumentJSON), &i) - if err != nil { - t.Error(err) - } - - if i.Alias != "" { - t.Error("expected empty alias") - } - if i.BaseCurrency != "" { - t.Error("expected empty base currency") - } - if i.Category != "1" { - t.Error("expected 1 category") - } - if i.ContractMultiplier != 1 { - t.Error("expected 1 contract multiplier") - } - if i.ContractType != "linear" { - t.Error("expected linear contract type") - } - if i.ContractValue.Float64() != 0.0001 { - t.Error("expected 0.0001 contract value") - } - if i.ContractValueCurrency != currency.BTC.String() { - t.Error("expected BTC contract value currency") - } - if !i.ExpTime.IsZero() { - t.Error("expected empty expiry time") - } - if i.InstrumentFamily != "BTC-USDC" { - t.Error("expected BTC-USDC instrument family") - } - if i.InstrumentID != "BTC-USDC-SWAP" { - t.Error("expected BTC-USDC-SWAP instrument ID") - } - swap := ok.GetInstrumentTypeFromAssetItem(asset.PerpetualSwap) - if i.InstrumentType != swap { - t.Error("expected SWAP instrument type") - } - if i.MaxLeverage != 125 { - t.Error("expected 125 leverage") - } - if i.ListTime.UnixMilli() != 1666076190000 { - t.Error("expected 1666076190000 listing time") - } - if i.LotSize != 1 { - t.Error("expected 1 lot size") - } - if i.MaxSpotIcebergSize != 100000000.0000000000000000 { - t.Error("expected 100000000.0000000000000000 max iceberg order size") - } - if i.MaxQuantityOfSpotLimitOrder != 100000000 { - t.Error("expected 100000000 max limit order size") - } - if i.MaxQuantityOfMarketLimitOrder != 85000 { - t.Error("expected 85000 max market order size") - } - if i.MaxStopSize != 85000 { - t.Error("expected 85000 max stop order size") - } - if i.MaxTriggerSize != 100000000.0000000000000000 { - t.Error("expected 100000000.0000000000000000 max trigger order size") - } - if i.MaxQuantityOfSpotTwapLimitOrder != 0 { - t.Error("expected empty max TWAP size") - } - if i.MinimumOrderSize != 1 { - t.Error("expected 1 min size") - } - if i.OptionType != "" { - t.Error("expected empty option type") - } - if i.QuoteCurrency != "" { - t.Error("expected empty quote currency") - } - if i.SettlementCurrency != currency.USDC.String() { - t.Error("expected USDC settlement currency") - } - if i.State != "live" { - t.Error("expected live state") - } - if i.StrikePrice != "" { - t.Error("expected empty strike price") - } - if i.TickSize != 0.1 { - t.Error("expected 0.1 tick size") - } - if i.Underlying != "BTC-USDC" { - t.Error("expected BTC-USDC underlying") - } + result, err := ok.GetLeadTraderStats(contextGenerate(), "SWAP", leadTraderUniqueID, "2") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetLatestFundingRate(t *testing.T) { +func TestGetLeadTraderCurrencyPreferences(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD-SWAP") - if err != nil { - t.Error(err) - } - _, err = ok.GetLatestFundingRates(contextGenerate(), &fundingrate.LatestRateRequest{ - Asset: asset.PerpetualSwap, - Pair: cp, - IncludePredictedRate: true, - }) - if err != nil { - t.Error(err) - } + _, err := ok.GetLeadTraderCurrencyPreferences(contextGenerate(), "SWAP", "", "2") + require.ErrorIs(t, err, errUniqueCodeRequired) + _, err = ok.GetLeadTraderCurrencyPreferences(contextGenerate(), "SWAP", leadTraderUniqueID, "") + require.ErrorIs(t, err, errLastDaysRequired) + + result, err := ok.GetLeadTraderCurrencyPreferences(contextGenerate(), "SWAP", leadTraderUniqueID, "2") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetHistoricalFundingRates(t *testing.T) { +func TestGetLeadTraderCurrentLeadPositions(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString("BTC-USD-SWAP") - if err != nil { - t.Error(err) - } - r := &fundingrate.HistoricalRatesRequest{ - Asset: asset.PerpetualSwap, - Pair: cp, - PaymentCurrency: currency.USDT, - StartDate: time.Now().Add(-time.Hour * 24 * 7), - EndDate: time.Now(), - IncludePredictedRate: true, - } - if sharedtestvalues.AreAPICredentialsSet(ok) { - r.IncludePayments = true - } - _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) - if err != nil { - t.Error(err) - } - - r.StartDate = time.Now().Add(-time.Hour * 24 * 120) - _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) - if !errors.Is(err, fundingrate.ErrFundingRateOutsideLimits) { - t.Error(err) - } + _, err := ok.GetLeadTraderCurrentLeadPositions(contextGenerate(), instTypeSwap, "", "", "", 10) + require.ErrorIs(t, err, errUniqueCodeRequired) - r.RespectHistoryLimits = true - _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) - if err != nil { - t.Error(err) - } + result, err := ok.GetLeadTraderCurrentLeadPositions(contextGenerate(), "SWAP", leadTraderUniqueID, "", "", 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestIsPerpetualFutureCurrency(t *testing.T) { +func TestGetLeadTraderLeadPositionHistory(t *testing.T) { t.Parallel() - is, err := ok.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT)) - if err != nil { - t.Error(err) - } - if is { - t.Error("expected false") - } + _, err := ok.GetLeadTraderLeadPositionHistory(contextGenerate(), "SWAP", "", "", "", 10) + require.ErrorIs(t, err, errUniqueCodeRequired) - cp, err := currency.NewPairFromString("BTC-USD-SWAP") - if err != nil { - t.Error(err) - } - is, err = ok.IsPerpetualFutureCurrency(asset.PerpetualSwap, cp) - if err != nil { - t.Error(err) - } - if !is { - t.Error("expected true") - } + result, err := ok.GetLeadTraderLeadPositionHistory(contextGenerate(), "SWAP", leadTraderUniqueID, "", "", 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetAssetsFromInstrumentTypeOrID(t *testing.T) { +func TestPlaceSpreadOrder(t *testing.T) { t.Parallel() + _, err := ok.PlaceSpreadOrder(contextGenerate(), &SpreadOrderParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - ok := new(Okx) //nolint:govet // Intentional shadow - require.NoError(t, testexch.Setup(ok), "Setup must not error") + arg := &SpreadOrderParam{Tag: "tag-here"} + _, err = ok.PlaceSpreadOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errMissingInstrumentID) - _, err := ok.GetAssetsFromInstrumentTypeOrID("", "") - assert.ErrorIs(t, err, errEmptyArgument) + arg.SpreadID = spreadTP.String() + _, err = ok.PlaceSpreadOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrTypeIsInvalid) - for _, a := range []asset.Item{asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options} { - symbol := "" - if a != asset.Spot { - symbol = ok.CurrencyPairs.Pairs[a].Enabled[0].String() - } - assets, err2 := ok.GetAssetsFromInstrumentTypeOrID(a.String(), symbol) - require.NoErrorf(t, err2, "GetAssetsFromInstrumentTypeOrID must not error for asset: %s", a) - require.Len(t, assets, 1) - assert.Equalf(t, a, assets[0], "Should contain asset: %s", a) - } + arg.OrderType = "limit" + _, err = ok.PlaceSpreadOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) - _, err = ok.GetAssetsFromInstrumentTypeOrID("", "test") - assert.ErrorIs(t, err, currency.ErrCurrencyNotSupported) - _, err = ok.GetAssetsFromInstrumentTypeOrID("", "test-test") - assert.ErrorIs(t, err, asset.ErrNotSupported) + arg.Size = 1 + _, err = ok.PlaceSpreadOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrPriceBelowMin) - for _, a := range []asset.Item{asset.Margin, asset.Spot} { - assets, err2 := ok.GetAssetsFromInstrumentTypeOrID("", ok.CurrencyPairs.Pairs[a].Enabled[0].String()) - require.NoErrorf(t, err2, "GetAssetsFromInstrumentTypeOrID must not error for asset: %s", a) - assert.Contains(t, assets, a) - } + arg.Price = 12345 + _, err = ok.PlaceSpreadOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceSpreadOrder(contextGenerate(), &SpreadOrderParam{ + InstrumentID: spreadTP.String(), + SpreadID: "1234", + ClientOrderID: "12354123523", + Side: order.Buy.Lower(), + OrderType: "limit", + Size: 1, + Price: 12345, + }) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestSetMarginType(t *testing.T) { +func TestCancelSpreadOrder(t *testing.T) { t.Parallel() - err := ok.SetMarginType(contextGenerate(), asset.Spot, currency.NewBTCUSDT(), margin.Isolated) - if !errors.Is(err, common.ErrFunctionNotSupported) { - t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported) - } + _, err := ok.CancelSpreadOrder(contextGenerate(), "", "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelSpreadOrder(contextGenerate(), "12345", "") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestChangePositionMargin(t *testing.T) { +func TestWsCancelSpreadOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - cp, err := currency.NewPairFromString("eth/btc") - if err != nil { - t.Error(err) - } - _, err = ok.ChangePositionMargin(contextGenerate(), &margin.PositionChangeRequest{ - Pair: cp, - Asset: asset.Margin, - MarginType: margin.Isolated, - OriginalAllocatedMargin: 4.0695, - NewAllocatedMargin: 5, - }) - if err != nil { - t.Error(err) - } + _, err := ok.WsCancelSpreadOrder(contextGenerate(), "", "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsCancelSpreadOrder(contextGenerate(), "1234", "") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetCollateralMode(t *testing.T) { +func TestCancelAllSpreadOrders(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - _, err := ok.GetCollateralMode(contextGenerate(), asset.Spot) - if !errors.Is(err, nil) { - t.Errorf("received '%v', expected '%v'", err, nil) - } - _, err = ok.GetCollateralMode(contextGenerate(), asset.Futures) - if !errors.Is(err, nil) { - t.Errorf("received '%v', expected '%v'", err, nil) - } - _, err = ok.GetCollateralMode(contextGenerate(), asset.USDTMarginedFutures) - if !errors.Is(err, asset.ErrNotSupported) { - t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelAllSpreadOrders(contextGenerate(), "123456") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestSetCollateralMode(t *testing.T) { +func TestWsCancelAllSpreadOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - err := ok.SetCollateralMode(contextGenerate(), asset.Spot, collateral.SingleMode) - if !errors.Is(err, common.ErrFunctionNotSupported) { - t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported) - } + result, err := ok.WsCancelAllSpreadOrders(contextGenerate(), "BTC-USDT_BTC-USDT-SWAP") + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetPositionSummary(t *testing.T) { +func TestAmendSpreadOrder(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - pp, err := ok.CurrencyPairs.GetPairs(asset.PerpetualSwap, true) - if err != nil { - t.Error(err) - } - _, err = ok.GetFuturesPositionSummary(contextGenerate(), &futures.PositionSummaryRequest{ - Asset: asset.PerpetualSwap, - Pair: pp[0], - UnderlyingPair: currency.EMPTYPAIR, - }) - if err != nil { - t.Error(err) - } + _, err := ok.AmendSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.AmendSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{NewSize: 2}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = ok.AmendSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{OrderID: "2510789768709120"}) + require.ErrorIs(t, err, errSizeOrPriceIsRequired) - pp, err = ok.CurrencyPairs.GetPairs(asset.Futures, true) - if err != nil { - t.Error(err) - } - _, err = ok.GetFuturesPositionSummary(contextGenerate(), &futures.PositionSummaryRequest{ - Asset: asset.Futures, - Pair: pp[0], - UnderlyingPair: currency.EMPTYPAIR, + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{ + OrderID: "2510789768709120", + NewSize: 2, }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + assert.NotNil(t, result) +} - _, err = ok.GetFuturesPositionSummary(contextGenerate(), &futures.PositionSummaryRequest{ - Asset: asset.Spot, - Pair: pp[0], - UnderlyingPair: currency.NewBTCUSDT(), +func TestWsAmandSpreadOrder(t *testing.T) { + t.Parallel() + _, err := ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) + _, err = ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{NewSize: 2}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{OrderID: "2510789768709120"}) + require.ErrorIs(t, err, errSizeOrPriceIsRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsAmandSpreadOrder(contextGenerate(), &AmendSpreadOrderParam{ + OrderID: "2510789768709120", + NewSize: 2, }) - if !errors.Is(err, futures.ErrNotFuturesAsset) { - t.Errorf("received '%v', expected '%v'", err, futures.ErrNotFuturesAsset) - } + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetFuturesPositions(t *testing.T) { +func TestGetSpreadOrderDetails(t *testing.T) { t.Parallel() + _, err := ok.GetSpreadOrderDetails(contextGenerate(), "", "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - pp, err := ok.CurrencyPairs.GetPairs(asset.Futures, true) - if err != nil { - t.Error(err) - } - _, err = ok.GetFuturesPositionOrders(contextGenerate(), &futures.PositionsRequest{ - Asset: asset.Futures, - Pairs: []currency.Pair{pp[0]}, - StartDate: time.Now().Add(time.Hour * 24 * -7), - }) - if err != nil { - t.Error(err) - } + result, err := ok.GetSpreadOrderDetails(contextGenerate(), "1234567", "") + require.NoError(t, err) + assert.NotNil(t, result) +} - _, err = ok.GetFuturesPositionOrders(contextGenerate(), &futures.PositionsRequest{ - Asset: asset.Spot, - Pairs: []currency.Pair{pp[0]}, - StartDate: time.Now().Add(time.Hour * 24 * -7), - }) - if !errors.Is(err, futures.ErrNotFuturesAsset) { - t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported) - } +func TestGetActiveSpreadOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetActiveSpreadOrders(contextGenerate(), "", "post_only", "partially_filled", "", "", 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestGetLeverage(t *testing.T) { +func TestGetCompletedSpreadOrdersLast7Days(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) - pp, err := ok.CurrencyPairs.GetPairs(asset.Futures, true) - if err != nil { - t.Error(err) - } - _, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Multi, order.UnknownSide) - if err != nil { - t.Error(err) - } + result, err := ok.GetCompletedSpreadOrdersLast7Days(contextGenerate(), "", "limit", "canceled", "", "", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} - _, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, order.UnknownSide) - if !errors.Is(err, errOrderSideRequired) { - t.Errorf("received '%v', expected '%v'", err, errOrderSideRequired) - } +func TestGetSpreadTradesOfLast7Days(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetSpreadTradesOfLast7Days(contextGenerate(), "", "", "", "", "", time.Time{}, time.Time{}, 0) + require.NoError(t, err) + assert.NotNil(t, result) +} - _, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, order.Long) - if err != nil { - t.Error(err) - } +func TestGetSpreads(t *testing.T) { + t.Parallel() + result, err := ok.GetPublicSpreads(contextGenerate(), "", "", "", "") + require.NoError(t, err) + assert.NotNil(t, result) +} - _, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, order.Short) - if err != nil { - t.Error(err) - } +func TestGetSpreadOrderBooks(t *testing.T) { + t.Parallel() + _, err := ok.GetPublicSpreadOrderBooks(contextGenerate(), "", 0) + require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.GetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, order.CouldNotBuy) - if !errors.Is(err, errInvalidOrderSide) { - t.Errorf("received '%v', expected '%v'", err, errInvalidOrderSide) - } + result, err := ok.GetPublicSpreadOrderBooks(contextGenerate(), "BTC-USDT_BTC-USDT-SWAP", 0) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestSetLeverage(t *testing.T) { +func TestGetSpreadTickers(t *testing.T) { t.Parallel() - sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) - pp, err := ok.CurrencyPairs.GetPairs(asset.Futures, true) - if err != nil { - t.Error(err) - } - err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Multi, 5, order.UnknownSide) - if err != nil { - t.Error(err) - } + _, err := ok.GetPublicSpreadTickers(contextGenerate(), "") + require.ErrorIs(t, err, errMissingInstrumentID) - err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.UnknownSide) - if !errors.Is(err, errOrderSideRequired) { - t.Errorf("received '%v', expected '%v'", err, errOrderSideRequired) - } + result, err := ok.GetPublicSpreadTickers(contextGenerate(), "BTC-USDT_BTC-USDT-SWAP") + require.NoError(t, err) + assert.NotNil(t, result) +} - err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.Long) - if err != nil { - t.Error(err) - } +func TestGetPublicSpreadTrades(t *testing.T) { + t.Parallel() + result, err := ok.GetPublicSpreadTrades(contextGenerate(), "") + require.NoError(t, err) + assert.NotNil(t, result) +} - err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.Short) - if err != nil { - t.Error(err) - } +func TestGetOptionsTickBands(t *testing.T) { + t.Parallel() + _, err := ok.GetOptionsTickBands(contextGenerate(), "", "") + require.ErrorIs(t, err, errInvalidInstrumentType) - err = ok.SetLeverage(contextGenerate(), asset.Futures, pp[0], margin.Isolated, 5, order.CouldNotBuy) - if !errors.Is(err, errInvalidOrderSide) { - t.Errorf("received '%v', expected '%v'", err, errInvalidOrderSide) - } + result, err := ok.GetOptionsTickBands(contextGenerate(), "OPTION", "") + require.NoError(t, err) + assert.NotNil(t, result) +} - err = ok.SetLeverage(contextGenerate(), asset.Spot, pp[0], margin.Multi, 5, order.UnknownSide) - if !errors.Is(err, asset.ErrNotSupported) { - t.Errorf("received '%v', expected '%v'", err, asset.ErrNotSupported) - } +func TestExtractIndexCandlestick(t *testing.T) { + t.Parallel() + data := `[ [ "1597026383085", "3.721", "3.743", "3.677", "3.708", "1" ], [ "1597026383085", "3.731", "3.799", "3.494", "3.72", "1" ]]` + var resp []CandlestickHistoryItem + err := json.Unmarshal([]byte(data), &resp) + require.NoError(t, err) + require.Len(t, resp, 2) + require.Equal(t, 3.743, resp[0].HighestPrice.Float64()) + require.Equal(t, StateCompleted, resp[0].Confirm) } -func TestGetFuturesContractDetails(t *testing.T) { +func TestGetHistoricIndexAndMarkPriceCandlesticks(t *testing.T) { t.Parallel() - _, err := ok.GetFuturesContractDetails(context.Background(), asset.Spot) - if !errors.Is(err, futures.ErrNotFuturesAsset) { - t.Error(err) - } - _, err = ok.GetFuturesContractDetails(context.Background(), asset.USDTMarginedFutures) - if !errors.Is(err, asset.ErrNotSupported) { - t.Error(err) - } + _, err := ok.GetHistoricIndexCandlesticksHistory(contextGenerate(), "", time.Time{}, time.Time{}, kline.FiveMin, 10) + require.ErrorIs(t, err, errMissingInstrumentID) - _, err = ok.GetFuturesContractDetails(context.Background(), asset.Futures) - if !errors.Is(err, nil) { - t.Error(err) - } - _, err = ok.GetFuturesContractDetails(context.Background(), asset.PerpetualSwap) - if !errors.Is(err, nil) { - t.Error(err) - } + result, err := ok.GetHistoricIndexCandlesticksHistory(contextGenerate(), "BTC-USD", time.Time{}, time.Time{}, kline.FiveMin, 10) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.GetMarkPriceCandlestickHistory(contextGenerate(), "BTC-USD-SWAP", time.Time{}, time.Time{}, kline.FiveMin, 10) + require.NoError(t, err) + assert.NotNil(t, result) } -func TestWsProcessOrderbook5(t *testing.T) { +func TestGetEconomicCanendarData(t *testing.T) { t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetEconomicCalendarData(contextGenerate(), "", "", time.Now(), time.Time{}, 0) + require.NoError(t, err) + assert.NotNil(t, result) +} - var ob5payload = []byte(`{"arg":{"channel":"books5","instId":"OKB-USDT"},"data":[{"asks":[["0.0000007465","2290075956","0","4"],["0.0000007466","1747284705","0","4"],["0.0000007467","1338861655","0","3"],["0.0000007468","1661668387","0","6"],["0.0000007469","2715477116","0","5"]],"bids":[["0.0000007464","15693119","0","1"],["0.0000007463","2330835024","0","4"],["0.0000007462","1182926517","0","2"],["0.0000007461","3818684357","0","4"],["0.000000746","6021641435","0","7"]],"instId":"OKB-USDT","ts":"1695864901807","seqId":4826378794}]}`) - err := ok.wsProcessOrderbook5(ob5payload) - if err != nil { - t.Error(err) - } +func TestGetDepositWithdrawalStatus(t *testing.T) { + t.Parallel() + _, err := ok.GetDepositWithdrawalStatus(contextGenerate(), currency.EMPTYCODE, "", "", "", "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = ok.GetDepositWithdrawalStatus(contextGenerate(), currency.EMPTYCODE, "", "1244", "", "") + require.ErrorIs(t, err, errMissingValidWithdrawalID) - required := currency.NewPairWithDelimiter("OKB", "USDT", "-") + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetDepositWithdrawalStatus(contextGenerate(), currency.EMPTYCODE, "1244", "", "", "") + require.NoError(t, err) + assert.NotNil(t, result) +} - got, err := orderbook.Get("okx", required, asset.Spot) - if err != nil { - t.Fatal(err) - } +func TestGetPublicExchangeList(t *testing.T) { + t.Parallel() + result, err := ok.GetPublicExchangeList(contextGenerate()) + require.NoError(t, err) + assert.NotNil(t, result) +} - if len(got.Asks) != 5 { - t.Errorf("expected %v, received %v", 5, len(got.Asks)) - } +func TestWsPlaceSpreadOrder(t *testing.T) { + t.Parallel() + _, err := ok.WsPlaceSpreadOrder(contextGenerate(), &SpreadOrderParam{}) + require.ErrorIs(t, err, common.ErrNilPointer) - if len(got.Bids) != 5 { - t.Errorf("expected %v, received %v", 5, len(got.Bids)) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.WsPlaceSpreadOrder(contextGenerate(), &SpreadOrderParam{ + SpreadID: "BTC-USDT_BTC-USDT-SWAP", + ClientOrderID: "b15", + Side: order.Buy.Lower(), + OrderType: "limit", + Price: 2.15, + Size: 2, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} - // Book replicated to margin - got, err = orderbook.Get("okx", required, asset.Margin) - if err != nil { - t.Fatal(err) - } +func TestGetInviteesDetail(t *testing.T) { + t.Parallel() + _, err := ok.GetInviteesDetail(contextGenerate(), "") + require.ErrorIs(t, err, errUserIDRequired) - if len(got.Asks) != 5 { - t.Errorf("expected %v, received %v", 5, len(got.Asks)) - } + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetInviteesDetail(contextGenerate(), "1234") + require.NoError(t, err) + assert.NotNil(t, result) +} - if len(got.Bids) != 5 { - t.Errorf("expected %v, received %v", 5, len(got.Bids)) - } +func TestGetUserAffilateRebateInformation(t *testing.T) { + t.Parallel() + _, err := ok.GetUserAffiliateRebateInformation(contextGenerate(), "") + require.ErrorIs(t, err, errInvalidAPIKey) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetUserAffiliateRebateInformation(contextGenerate(), "1234") + require.NoError(t, err) + assert.NotNil(t, result) } func TestGetOpenInterest(t *testing.T) { t.Parallel() - _, err := ok.GetOpenInterest(context.Background(), key.PairAsset{ + _, err := ok.GetOpenInterest(contextGenerate(), key.PairAsset{ Base: currency.ETH.Item, Quote: currency.USDT.Item, Asset: asset.USDTMarginedFutures, }) - assert.ErrorIs(t, err, asset.ErrNotSupported) + require.ErrorIs(t, err, asset.ErrNotSupported) usdSwapCode := currency.NewCode("USD-SWAP") - resp, err := ok.GetOpenInterest(context.Background(), key.PairAsset{ - Base: currency.BTC.Item, - Quote: usdSwapCode.Item, + resp, err := ok.GetOpenInterest(contextGenerate(), key.PairAsset{ + Base: perpetualSwapTP.Base.Item, + Quote: perpetualSwapTP.Quote.Item, Asset: asset.PerpetualSwap, }) assert.NoError(t, err) @@ -3679,7 +6128,7 @@ func TestGetOpenInterest(t *testing.T) { cp1 := currency.NewPair(currency.DOGE, usdSwapCode) sharedtestvalues.SetupCurrencyPairsForExchangeAsset(t, ok, asset.PerpetualSwap, cp1) - resp, err = ok.GetOpenInterest(context.Background(), + resp, err = ok.GetOpenInterest(contextGenerate(), key.PairAsset{ Base: currency.BTC.Item, Quote: usdSwapCode.Item, @@ -3693,8 +6142,7 @@ func TestGetOpenInterest(t *testing.T) { ) assert.NoError(t, err) assert.NotEmpty(t, resp) - - resp, err = ok.GetOpenInterest(context.Background()) + resp, err = ok.GetOpenInterest(contextGenerate()) assert.NoError(t, err) assert.NotEmpty(t, resp) } @@ -3704,19 +6152,357 @@ func TestGetCurrencyTradeURL(t *testing.T) { testexch.UpdatePairsOnce(t, ok) for _, a := range ok.GetAssetTypes(false) { pairs, err := ok.CurrencyPairs.GetPairs(a, false) - require.NoError(t, err, "cannot get pairs for %s", a) - require.NotEmpty(t, pairs, "no pairs for %s", a) - resp, err := ok.GetCurrencyTradeURL(context.Background(), a, pairs[0]) - require.NoError(t, err) + assert.NoError(t, err) + assert.NotEmpty(t, pairs) + + resp, err := ok.GetCurrencyTradeURL(contextGenerate(), a, pairs[0]) + assert.NoError(t, err) assert.NotEmpty(t, resp) } } -func TestGenerateSubscriptions(t *testing.T) { +func TestPlaceLendingOrder(t *testing.T) { t.Parallel() + _, err := ok.PlaceLendingOrder(contextGenerate(), &LendingOrderParam{}) + require.ErrorIs(t, err, common.ErrEmptyParams) - ok := new(Okx) - require.NoError(t, testexch.Setup(ok), "Test instance Setup must not error") + arg := &LendingOrderParam{AutoRenewal: true} + _, err = ok.PlaceLendingOrder(contextGenerate(), arg) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + arg.Currency = currency.USDT + _, err = ok.PlaceLendingOrder(contextGenerate(), arg) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + + arg.Amount = 1 + _, err = ok.PlaceLendingOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errRateRequired) + + arg.Rate = 0.01 + _, err = ok.PlaceLendingOrder(contextGenerate(), arg) + require.ErrorIs(t, err, errLendingTermIsRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.PlaceLendingOrder(contextGenerate(), &LendingOrderParam{ + Currency: currency.USDT, + Amount: 1, + Rate: 0.01, + Term: "30D", + AutoRenewal: true, + }) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestAmendLendingOrder(t *testing.T) { + t.Parallel() + _, err := ok.AmendLendingOrder(contextGenerate(), "", 0, 0, false) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.AmendLendingOrder(contextGenerate(), "12312312", 1., 2., true) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetLendingOrders(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLendingOrders(contextGenerate(), "", "pending", currency.ETH, time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetLendingSubOrderList(t *testing.T) { + t.Parallel() + _, err := ok.GetLendingSubOrderList(contextGenerate(), "", "pending", time.Time{}, time.Time{}, 10) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetLendingSubOrderList(contextGenerate(), "12345", "", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestCancelAllSpreadOrdersAfterCountdown(t *testing.T) { + t.Parallel() + _, err := ok.CancelAllSpreadOrdersAfterCountdown(contextGenerate(), 2) + require.ErrorIs(t, err, errCountdownTimeoutRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CancelAllSpreadOrdersAfterCountdown(contextGenerate(), 12) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetContractsOpenInterestHistory(t *testing.T) { + t.Parallel() + _, err := ok.GetFuturesContractsOpenInterestHistory(contextGenerate(), "", kline.FiveMin, time.Time{}, time.Time{}, 10) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetFuturesContractsOpenInterestHistory(contextGenerate(), futuresTP.String(), kline.FiveMin, time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFuturesContractTakerVolume(t *testing.T) { + t.Parallel() + _, err := ok.GetFuturesContractTakerVolume(contextGenerate(), "", kline.FiveMin, 1, 10, time.Time{}, time.Time{}) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetFuturesContractTakerVolume(contextGenerate(), futuresTP.String(), kline.FiveMin, 1, 10, time.Time{}, time.Time{}) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFuturesContractLongShortAccountRatio(t *testing.T) { + t.Parallel() + _, err := ok.GetFuturesContractLongShortAccountRatio(contextGenerate(), "", kline.FiveMin, time.Time{}, time.Time{}, 10) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetFuturesContractLongShortAccountRatio(contextGenerate(), futuresTP.String(), kline.FiveMin, time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetTopTradersFuturesContractLongShortRatio(t *testing.T) { + t.Parallel() + _, err := ok.GetTopTradersFuturesContractLongShortAccountRatio(contextGenerate(), "", kline.FiveMin, time.Time{}, time.Time{}, 10) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetTopTradersFuturesContractLongShortAccountRatio(contextGenerate(), futuresTP.String(), kline.FiveMin, time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetTopTradersFuturesContractLongShortPositionRatio(t *testing.T) { + t.Parallel() + _, err := ok.GetTopTradersFuturesContractLongShortPositionRatio(contextGenerate(), "", kline.FiveMin, time.Time{}, time.Time{}, 10) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetTopTradersFuturesContractLongShortPositionRatio(contextGenerate(), futuresTP.String(), kline.FiveMin, time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetAccountInstruments(t *testing.T) { + t.Parallel() + _, err := ok.GetAccountInstruments(contextGenerate(), asset.Empty, "", "", spotTP.String()) + require.ErrorIs(t, err, errInvalidInstrumentType) + _, err = ok.GetAccountInstruments(contextGenerate(), asset.Futures, "", "", spotTP.String()) + require.ErrorIs(t, err, errInvalidUnderlying) + _, err = ok.GetAccountInstruments(contextGenerate(), asset.Options, "", "", spotTP.String()) + require.ErrorIs(t, err, errInstrumentFamilyOrUnderlyingRequired) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetAccountInstruments(contextGenerate(), asset.Spot, "", "", spotTP.String()) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.GetAccountInstruments(contextGenerate(), asset.Options, "", "BTC-USD", optionsTP.String()) + assert.NoError(t, err) + assert.NotNil(t, result) + + result, err = ok.GetAccountInstruments(contextGenerate(), asset.Futures, "BTC-USD", "", futuresTP.String()) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestOrderTypeString(t *testing.T) { + t.Parallel() + var orderTypesToStringMap = map[order.Type]struct { + Expected string + Error error + }{ + order.Market: {Expected: orderMarket}, + order.Limit: {Expected: orderLimit}, + order.PostOnly: {Expected: orderPostOnly}, + order.FillOrKill: {Expected: orderFOK}, + order.ImmediateOrCancel: {Expected: orderIOC}, + order.OptimalLimitIOC: {Expected: orderOptimalLimitIOC}, + order.MarketMakerProtection: {Expected: "mmp"}, + order.MarketMakerProtectionAndPostOnly: {Expected: "mmp_and_post_only"}, + order.Liquidation: {Error: order.ErrUnsupportedOrderType}, + order.OCO: {Expected: "oco"}, + order.TrailingStop: {Expected: "move_order_stop"}, + order.Chase: {Expected: "chase"}, + order.TWAP: {Expected: "twap"}, + order.ConditionalStop: {Expected: "conditional"}, + order.Trigger: {Expected: "trigger"}, + } + for oType, val := range orderTypesToStringMap { + orderTypeString, err := orderTypeString(oType) + require.ErrorIs(t, err, val.Error) + assert.Equal(t, val.Expected, orderTypeString) + } +} + +func TestGetMarkPriceCandlesticks(t *testing.T) { + t.Parallel() + _, err := ok.GetMarkPriceCandlesticks(contextGenerate(), "", kline.FiveMin, time.Time{}, time.Time{}, 10) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetMarkPriceCandlesticks(contextGenerate(), spotTP.String(), kline.FiveMin, time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetHistoricIndexCandlesticksHistory(t *testing.T) { + t.Parallel() + _, err := ok.GetHistoricIndexCandlesticksHistory(contextGenerate(), "", time.Time{}, time.Time{}, kline.TenMin, 10) + require.ErrorIs(t, err, errMissingInstrumentID) + + result, err := ok.GetHistoricIndexCandlesticksHistory(contextGenerate(), spotTP.String(), time.Time{}, time.Time{}, kline.FiveMin, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestAssetTypeString(t *testing.T) { + t.Parallel() + _, err := assetTypeString(asset.LinearContract) + require.ErrorIs(t, err, asset.ErrNotSupported) + + assetTypes := ok.GetAssetTypes(false) + for a := range assetTypes { + if assetTypes[a] == asset.Spread { + continue + } + _, err := assetTypeString(assetTypes[a]) + assert.NoError(t, err) + } +} + +func TestGetAnnouncements(t *testing.T) { + t.Parallel() + result, err := ok.GetAnnouncements(contextGenerate(), "", 0) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetAnnouncementTypes(t *testing.T) { + t.Parallel() + results, err := ok.GetAnnouncementTypes(contextGenerate()) + require.NoError(t, err) + assert.NotEmpty(t, results) +} + +func TestGetDepositOrderDetail(t *testing.T) { + t.Parallel() + _, err := ok.GetDepositOrderDetail(contextGenerate(), "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetDepositOrderDetail(contextGenerate(), "12312312") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFiatDepositOrderHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFiatDepositOrderHistory(contextGenerate(), currency.USDT, "TR_BANKS", "failed", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetWithdrawalOrderDetail(t *testing.T) { + t.Parallel() + _, err := ok.GetWithdrawalOrderDetail(contextGenerate(), "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetWithdrawalOrderDetail(contextGenerate(), "024041201450544699") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFiatWithdrawalOrderHistory(t *testing.T) { + t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFiatWithdrawalOrderHistory(contextGenerate(), currency.USDT, "SEPA", "failed", time.Time{}, time.Time{}, 10) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestCancelWithdrawalOrder(t *testing.T) { + t.Parallel() + _, err := ok.CancelWithdrawalOrder(contextGenerate(), "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.CancelWithdrawalOrder(contextGenerate(), "124041201450544699") + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestCreateWithdrawalOrder(t *testing.T) { + t.Parallel() + _, err := ok.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "", "SEPA", "194a6975e98246538faeb0fab0d502df", 1000) + require.ErrorIs(t, err, errIDNotSet) + _, err = ok.CreateWithdrawalOrder(contextGenerate(), currency.EMPTYCODE, "1231312312", "SEPA", "194a6975e98246538faeb0fab0d502df", 1000) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + _, err = ok.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "1231312312", "SEPA", "194a6975e98246538faeb0fab0d502df", 0) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + _, err = ok.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "1231312312", "", "194a6975e98246538faeb0fab0d502df", 1000) + require.ErrorIs(t, err, errPaymentMethodRequired) + _, err = ok.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "1231312312", "SEPA", "", 1000) + require.ErrorIs(t, err, errIDNotSet) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok, canManipulateRealOrders) + result, err := ok.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "1231312312", "SEPA", "194a6975e98246538faeb0fab0d502df", 1000) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFiatWithdrawalPaymentMethods(t *testing.T) { + t.Parallel() + _, err := ok.GetFiatWithdrawalPaymentMethods(contextGenerate(), currency.EMPTYCODE) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFiatWithdrawalPaymentMethods(contextGenerate(), currency.TRY) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func TestGetFiatDepositPaymentMethods(t *testing.T) { + t.Parallel() + _, err := ok.GetFiatDepositPaymentMethods(contextGenerate(), currency.EMPTYCODE) + require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + result, err := ok.GetFiatDepositPaymentMethods(contextGenerate(), currency.TRY) + require.NoError(t, err) + assert.NotNil(t, result) +} + +func (ok *Okx) instrumentFamilyFromInstID(instrumentType, instID string) (string, error) { + ok.instrumentsInfoMapLock.Lock() + defer ok.instrumentsInfoMapLock.Unlock() + if instrumentType != "" { + insts, okay := ok.instrumentsInfoMap[instrumentType] + if !okay { + return "", errInvalidInstrumentType + } + for a := range insts { + if insts[a].InstrumentID == instID { + return insts[a].InstrumentFamily, nil + } + } + } else { + for _, insts := range ok.instrumentsInfoMap { + for a := range insts { + if insts[a].InstrumentID == instID { + return insts[a].InstrumentFamily, nil + } + } + } + } + return "", fmt.Errorf("instrument family not found for instrument %s", instID) +} + +func TestGenerateSubscriptions(t *testing.T) { + t.Parallel() ok.Websocket.SetCanUseAuthenticatedEndpoints(true) subs, err := ok.generateSubscriptions() @@ -3724,12 +6510,13 @@ func TestGenerateSubscriptions(t *testing.T) { exp := subscription.List{ {Channel: subscription.MyAccountChannel, QualifiedChannel: `{"channel":"account"}`, Authenticated: true}, } + var pairs currency.Pairs for _, s := range ok.Features.Subscriptions { for _, a := range ok.GetAssetTypes(true) { if s.Asset != asset.All && s.Asset != a { continue } - pairs, err := ok.GetEnabledPairs(a) + pairs, err = ok.GetEnabledPairs(a) require.NoErrorf(t, err, "GetEnabledPairs %s must not error", a) pairs = common.SortStrings(pairs).Format(currency.PairFormat{Uppercase: true, Delimiter: "-"}) s := s.Clone() //nolint:govet // Intentional lexical scope shadow @@ -3745,7 +6532,7 @@ func TestGenerateSubscriptions(t *testing.T) { } else { s := s.Clone() //nolint:govet // Intentional lexical scope shadow if isAssetChannel(s) { - s.QualifiedChannel = fmt.Sprintf(`{"channel":%q,"instType":%q}`, name, ok.GetInstrumentTypeFromAssetItem(s.Asset)) + s.QualifiedChannel = fmt.Sprintf(`{"channel":%q,"instType":%q}`, name, GetInstrumentTypeFromAssetItem(s.Asset)) } else { s.QualifiedChannel = `{"channel":"` + name + `"}` } @@ -3755,23 +6542,135 @@ func TestGenerateSubscriptions(t *testing.T) { } } testsubs.EqualLists(t, exp, subs) -} - -func TestGenerateGridSubscriptions(t *testing.T) { - t.Parallel() - ok := new(Okx) - require.NoError(t, testexch.Setup(ok), "Test instance Setup must not error") - - ok.Features.Subscriptions = subscription.List{{Channel: okxChannelGridPositions, Params: map[string]any{"algoId": "42"}}} - subs, err := ok.generateSubscriptions() + ok.Features.Subscriptions = subscription.List{{Channel: channelGridPositions, Params: map[string]any{"algoId": "42"}}} + subs, err = ok.generateSubscriptions() require.NoError(t, err, "generateSubscriptions must not error") - exp := subscription.List{{Channel: okxChannelGridPositions, Params: map[string]any{"algoId": "42"}, QualifiedChannel: `{"channel":"grid-positions","algoId":"42"}`}} + exp = subscription.List{{Channel: channelGridPositions, Params: map[string]any{"algoId": "42"}, QualifiedChannel: `{"channel":"grid-positions","algoId":"42"}`}} testsubs.EqualLists(t, exp, subs) - ok.Features.Subscriptions = subscription.List{{Channel: okxChannelGridPositions}} + ok.Features.Subscriptions = subscription.List{{Channel: channelGridPositions}} subs, err = ok.generateSubscriptions() require.NoError(t, err, "generateSubscriptions must not error") - exp = subscription.List{{Channel: okxChannelGridPositions, QualifiedChannel: `{"channel":"grid-positions"}`}} + exp = subscription.List{{Channel: channelGridPositions, QualifiedChannel: `{"channel":"grid-positions"}`}} testsubs.EqualLists(t, exp, subs) } + +const ( + processSpreadOrderbookJSON = `{"arg":{"channel":"sprd-books5", "sprdId": "BTC-USDT_BTC-USDT-SWAP" }, "data": [ { "asks": [ ["111.06","55154","2"], ["111.07","53276","2"], ["111.08","72435","2"], ["111.09","70312","2"], ["111.1","67272","2"]], "bids": [ ["111.05","57745","2"], ["111.04","57109","2"], ["111.03","69563","2"], ["111.02","71248","2"], ["111.01","65090","2"]], "ts": "1670324386802"}]}` + wsProcessPublicSpreadTradesJSON = `{"arg":{"channel":"sprd-public-trades", "sprdId": "BTC-USDT_BTC-USDT-SWAP" }, "data": [ { "sprdId": "BTC-USDT_BTC-USDT-SWAP", "tradeId": "2499206329160695808", "px": "-10", "sz": "0.001", "side": "sell", "ts": "1726801105519"}]}` + okxSpreadPublicTickerJSON = `{"arg":{"channel":"sprd-tickers", "sprdId": "BTC-USDT_BTC-USDT-SWAP" }, "data": [ { "sprdId": "BTC-USDT_BTC-USDT-SWAP", "last": "4", "lastSz": "0.01", "askPx": "19.7", "askSz": "5.79", "bidPx": "5.9", "bidSz": "5.79", "open24h": "-7", "high24h": "19.6", "low24h": "-7", "vol24h": "9.87", "ts": "1715247061026"}]}` + wsProcessSpreadOrdersJSON = `{"arg":{"channel":"sprd-orders","sprdId": "BTC-USDT_BTC-USDT-SWAP", "uid": "614488474791936"}, "data": [{"sprdId": "BTC-USDT_BTC-UST-SWAP", "ordId": "312269865356374016", "clOrdId": "b1", "tag": "", "px": "999", "sz": "3", "ordType": "limit", "side": "buy", "fillSz": "0", "fillPx": "", "tradeId": "", "accFillSz": "0", "pendingFillSz": "2", "pendingSettleSz": "1", "canceledSz": "1", "state": "live", "avgPx": "0", "cancelSource": "", "uTime": "1597026383085", "cTime": "1597026383085", "code": "0", "msg": "", "reqId": "", "amendResult": ""}]}` + wsProcessSpreadTradesJSON = `{"arg":{"channel":"sprd-trades", "sprdId": "BTC-USDT_BTC-USDT-SWAP", "uid": "614488474791936" }, "data":[ { "sprdId":"BTC-USDT-SWAP_BTC-USDT-200329", "tradeId":"123", "ordId":"123445", "clOrdId": "b16", "tag":"", "fillPx":"999", "fillSz":"3", "state": "filled", "side":"buy", "execType":"M", "ts":"1597026383085", "legs": [ { "instId": "BTC-USDT-SWAP", "px": "20000", "sz": "3", "szCont": "0.03", "side": "buy", "fee": "", "feeCcy": "", "tradeId": "1232342342" }, { "instId": "BTC-USDT-200329", "px": "21000", "sz": "3", "szCont": "0.03", "side": "sell", "fee": "", "feeCcy": "", "tradeId": "5345646634" } ], "code": "", "msg":""}]}` +) + +func TestWsProcessSpreadOrderbook(t *testing.T) { + t.Parallel() + err := ok.wsProcessSpreadOrderbook([]byte(processSpreadOrderbookJSON)) + assert.NoError(t, err) +} + +func TestWsProcessPublicSpreadTrades(t *testing.T) { + t.Parallel() + err := ok.wsProcessPublicSpreadTrades([]byte(wsProcessPublicSpreadTradesJSON)) + assert.NoError(t, err) +} + +func TestWsProcessPublicSpreadTicker(t *testing.T) { + t.Parallel() + err := ok.wsProcessPublicSpreadTicker([]byte(okxSpreadPublicTickerJSON)) + assert.NoError(t, err) +} + +func TestWsProcessSpreadOrders(t *testing.T) { + t.Parallel() + err := ok.wsProcessSpreadOrders([]byte(wsProcessSpreadOrdersJSON)) + assert.NoError(t, err) +} + +func TestWsProcessSpreadTradesJSON(t *testing.T) { + t.Parallel() + err := ok.wsProcessSpreadTrades([]byte(wsProcessSpreadTradesJSON)) + assert.NoError(t, err) +} + +func TestOrderTypeFromString(t *testing.T) { + t.Parallel() + orderTypeStrings := map[string]struct { + OType order.Type + Error error + }{ + "market": {OType: order.Market}, + "LIMIT": {OType: order.Limit}, + "limit": {OType: order.Limit}, + "post_only": {OType: order.PostOnly}, + "fok": {OType: order.FillOrKill}, + "ioc": {OType: order.ImmediateOrCancel}, + "optimal_limit_ioc": {OType: order.OptimalLimitIOC}, + "mmp": {OType: order.MarketMakerProtection}, + "mmp_and_post_only": {OType: order.MarketMakerProtectionAndPostOnly}, + "trigger": {OType: order.UnknownType, Error: order.ErrTypeIsInvalid}, + "chase": {OType: order.Chase}, + "move_order_stop": {OType: order.TrailingStop}, + "twap": {OType: order.TWAP}, + "abcd": {OType: order.UnknownType, Error: order.ErrTypeIsInvalid}, + } + for a := range orderTypeStrings { + oType, err := orderTypeFromString(a) + assert.ErrorIs(t, err, orderTypeStrings[a].Error) + assert.Equal(t, oType, orderTypeStrings[a].OType) + } +} + +func TestGetFee(t *testing.T) { + t.Parallel() + // CryptocurrencyWithdrawalFee Basic + feeBuilder := &exchange.FeeBuilder{ + Amount: 1, + FeeType: exchange.CryptocurrencyWithdrawalFee, + Pair: spotTP, + PurchasePrice: 1, + } + _, err := ok.GetFee(contextGenerate(), feeBuilder) + require.ErrorIs(t, err, errFeeTypeUnsupported) + + feeBuilder.FeeType = exchange.OfflineTradeFee + _, err = ok.GetFee(contextGenerate(), feeBuilder) + assert.NoError(t, err) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ok) + feeBuilder.FeeType = exchange.CryptocurrencyTradeFee + _, err = ok.GetFee(contextGenerate(), feeBuilder) + require.NoError(t, err) +} + +func TestPriceTypeString(t *testing.T) { + t.Parallel() + priceTypeToStringMap := map[order.PriceType]string{ + order.LastPrice: "last", + order.IndexPrice: "index", + order.MarkPrice: "mark", + order.UnknownPriceType: "", + } + var priceTString string + for x := range priceTypeToStringMap { + priceTString = priceTypeString(x) + assert.Equal(t, priceTString, priceTypeToStringMap[x]) + } +} + +func TestMarginTypeToString(t *testing.T) { + t.Parallel() + marginTypeToStringMap := map[margin.Type]string{ + margin.Isolated: "isolated", + margin.Multi: "cross", + margin.NoMargin: "cash", + margin.SpotIsolated: "spot_isolated", + margin.Unset: "", + } + var marginTypeString string + for m := range marginTypeToStringMap { + marginTypeString = ok.marginTypeToString(m) + assert.Equal(t, marginTypeString, marginTypeToStringMap[m]) + } +} diff --git a/exchanges/okx/okx_type_convert.go b/exchanges/okx/okx_type_convert.go index 791b9c680de..ae8efeedd71 100644 --- a/exchanges/okx/okx_type_convert.go +++ b/exchanges/okx/okx_type_convert.go @@ -2,60 +2,11 @@ package okx import ( "encoding/json" - "strconv" "strings" - "time" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) -type okxUnixMilliTime int64 - -// UnmarshalJSON deserializes byte data to okxunixMilliTime instance. -func (a *okxUnixMilliTime) UnmarshalJSON(data []byte) error { - var num string - err := json.Unmarshal(data, &num) - if err != nil { - return err - } - if num == "" { - return nil - } - value, err := strconv.ParseInt(num, 10, 64) - if err != nil { - return err - } - *a = okxUnixMilliTime(value) - return nil -} - -// Time returns the time instance from unix value of integer. -func (a *okxUnixMilliTime) Time() time.Time { - return time.UnixMilli(int64(*a)) -} - -type okxTime struct { - time.Time -} - -// UnmarshalJSON deserializes byte data to okxTime instance. -func (t *okxTime) UnmarshalJSON(data []byte) error { - var num string - err := json.Unmarshal(data, &num) - if err != nil { - return err - } - if num == "" { - return nil - } - value, err := strconv.ParseInt(num, 10, 64) - if err != nil { - return err - } - t.Time = time.UnixMilli(value) - return nil -} - // UnmarshalJSON decoder for OpenInterestResponse instance. func (a *OpenInterest) UnmarshalJSON(data []byte) error { type Alias OpenInterest @@ -68,110 +19,8 @@ func (a *OpenInterest) UnmarshalJSON(data []byte) error { return err } chil.InstrumentType = strings.ToUpper(chil.InstrumentType) - a.InstrumentType = GetAssetTypeFromInstrumentType(chil.InstrumentType) - return nil -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *LimitPriceResponse) UnmarshalJSON(data []byte) error { - type Alias LimitPriceResponse - chil := &struct { - *Alias - Timestamp int64 `json:"ts,string"` - }{ - Alias: (*Alias)(a), - } - err := json.Unmarshal(data, chil) - if err != nil { - return err - } - return nil -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *OrderDetail) UnmarshalJSON(data []byte) error { - type Alias OrderDetail - chil := &struct { - *Alias - UpdateTime int64 `json:"uTime,string"` - CreationTime int64 `json:"cTime,string"` - FillTime string `json:"fillTime"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - var err error - a.UpdateTime = time.UnixMilli(chil.UpdateTime) - a.CreationTime = time.UnixMilli(chil.CreationTime) - if chil.FillTime == "" { - a.FillTime = time.Time{} - } else { - var value int64 - value, err = strconv.ParseInt(chil.FillTime, 10, 64) - if err != nil { - return err - } - a.FillTime = time.UnixMilli(value) - } - if err != nil { - return err - } - return nil -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *RfqTradeResponse) UnmarshalJSON(data []byte) error { - type Alias RfqTradeResponse - chil := &struct { - *Alias - CreationTime int64 `json:"cTime,string"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - a.CreationTime = time.UnixMilli(chil.CreationTime) - return nil -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *BlockTicker) UnmarshalJSON(data []byte) error { - type Alias BlockTicker - chil := &struct { - *Alias - Timestamp int64 `json:"ts,string"` - }{ - Alias: (*Alias)(a), - } - err := json.Unmarshal(data, chil) - if err != nil { - return err - } - return nil -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *UnitConvertResponse) UnmarshalJSON(data []byte) error { - type Alias UnitConvertResponse - chil := &struct { - *Alias - ConvertType int `json:"type,string"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - switch chil.ConvertType { - case 1: - a.ConvertType = 1 - case 2: - a.ConvertType = 2 - } - return nil + a.InstrumentType, err = assetTypeFromInstrumentType(chil.InstrumentType) + return err } // MarshalJSON serialized QuoteLeg instance into bytes @@ -190,67 +39,3 @@ func (a *QuoteLeg) MarshalJSON() ([]byte, error) { } return json.Marshal(chil) } - -// MarshalJSON serialized CreateQuoteParams instance into bytes -func (a *CreateQuoteParams) MarshalJSON() ([]byte, error) { - type Alias CreateQuoteParams - chil := &struct { - *Alias - QuoteSide string `json:"quoteSide"` - }{ - Alias: (*Alias)(a), - } - if a.QuoteSide == order.Buy { - chil.QuoteSide = "buy" - } else { - chil.QuoteSide = "sell" - } - return json.Marshal(chil) -} - -// MarshalJSON serializes the WebsocketLoginData object -func (a *WebsocketLoginData) MarshalJSON() ([]byte, error) { - type Alias WebsocketLoginData - return json.Marshal(struct { - Timestamp int64 `json:"timestamp"` - *Alias - }{ - Timestamp: a.Timestamp.UTC().Unix(), - Alias: (*Alias)(a), - }) -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *WebsocketLoginData) UnmarshalJSON(data []byte) error { - type Alias WebsocketLoginData - chil := &struct { - *Alias - Timestamp int64 `json:"timestamp"` - }{ - Alias: (*Alias)(a), - } - if err := json.Unmarshal(data, chil); err != nil { - return err - } - a.Timestamp = time.UnixMilli(chil.Timestamp) - return nil -} - -// UnmarshalJSON deserializes JSON, and timestamp information. -func (a *CurrencyOneClickRepay) UnmarshalJSON(data []byte) error { - type Alias CurrencyOneClickRepay - chil := &struct { - *Alias - UpdateTime int64 `json:"uTime,string"` - FillToSize string `json:"fillToSz"` - FillFromSize string `json:"fillFromSz"` - }{ - Alias: (*Alias)(a), - } - err := json.Unmarshal(data, chil) - if err != nil { - return err - } - a.UpdateTime = time.Unix(chil.UpdateTime, 0) - return nil -} diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index b331a7847e9..82e11a5c45c 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -2,13 +2,17 @@ package okx import ( "encoding/json" + "errors" + "reflect" "strconv" "time" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/types" ) @@ -36,34 +40,125 @@ const ( ) const ( - // OkxOrderLimit Limit order - OkxOrderLimit = "limit" - // OkxOrderMarket Market order - OkxOrderMarket = "market" - // OkxOrderPostOnly POST_ONLY order type - OkxOrderPostOnly = "post_only" - // OkxOrderFOK fill or kill order type - OkxOrderFOK = "fok" - // OkxOrderIOC IOC (immediate or cancel) - OkxOrderIOC = "ioc" - // OkxOrderOptimalLimitIOC OPTIMAL_LIMIT_IOC - OkxOrderOptimalLimitIOC = "optimal_limit_ioc" + // orderLimit Limit order + orderLimit = "limit" + // orderMarket Market order + orderMarket = "market" + // orderPostOnly POST_ONLY order type + orderPostOnly = "post_only" + // orderFOK fill or kill order type + orderFOK = "fok" + // orderIOC IOC (immediate or cancel) + orderIOC = "ioc" + // orderOptimalLimitIOC OPTIMAL_LIMIT_IOC + orderOptimalLimitIOC = "optimal_limit_ioc" + + // represents a margin balance type + marginBalanceReduce = "reduce" + marginBalanceAdd = "add" // Instrument Types ( Asset Types ) - okxInstTypeFutures = "FUTURES" // Okx Instrument Type "futures" - okxInstTypeANY = "ANY" // Okx Instrument Type "" - okxInstTypeSpot = "SPOT" // Okx Instrument Type "spot" - okxInstTypeSwap = "SWAP" // Okx Instrument Type "swap" - okxInstTypeOption = "OPTION" // Okx Instrument Type "option" - okxInstTypeMargin = "MARGIN" // Okx Instrument Type "margin" - okxInstTypeContract = "CONTRACT" // Okx Instrument Type "contract" + instTypeFutures = "FUTURES" // instrument type "futures" + instTypeANY = "ANY" // instrument type "" + instTypeSpot = "SPOT" // instrument type "spot" + instTypeSwap = "SWAP" // instrument type "swap" + instTypeOption = "OPTION" // instrument type "option" + instTypeMargin = "MARGIN" // instrument type "margin" + instTypeContract = "CONTRACTS" // instrument type "contract" operationSubscribe = "subscribe" operationUnsubscribe = "unsubscribe" operationLogin = "login" ) +var ( + errIndexComponentNotFound = errors.New("unable to fetch index components") + errLimitValueExceedsMaxOf100 = errors.New("limit value exceeds the maximum value 100") + errMissingInstrumentID = errors.New("missing instrument ID") + errEitherInstIDOrCcyIsRequired = errors.New("either parameter instId or ccy is required") + errInvalidTradeMode = errors.New("unacceptable required argument, trade mode") + errMissingExpiryTimeParameter = errors.New("missing expiry date parameter") + errInvalidTradeModeValue = errors.New("invalid trade mode value") + errCurrencyQuantityTypeRequired = errors.New("only base_ccy and quote_ccy quantity types are supported") + errWebsocketStreamNotAuthenticated = errors.New("websocket stream not authenticated") + errInvalidNewSizeOrPriceInformation = errors.New("invalid new size or price information") + errSizeOrPriceIsRequired = errors.New("either size or price is required") + errInvalidPriceLimit = errors.New("invalid price limit value") + errMissingIntervalValue = errors.New("missing interval value") + errMissingSizeLimit = errors.New("missing required parameter 'szLimit'") + errMissingEitherAlgoIDOrState = errors.New("either algo ID or order state is required") + errAlgoIDRequired = errors.New("algo ID is required") + errMissingValidWithdrawalID = errors.New("missing valid withdrawal ID") + errInstrumentFamilyRequired = errors.New("instrument family is required") + errCountdownTimeoutRequired = errors.New("countdown timeout is required") + errInstrumentIDorFamilyRequired = errors.New("either instrument ID or instrument family is required") + errInvalidQuantityLimit = errors.New("invalid quantity limit") + errInvalidInstrumentType = errors.New("invalid instrument type") + errMissingValidGreeksType = errors.New("missing valid greeks type") + errMissingIsolatedMarginTradingSetting = errors.New("missing isolated margin trading setting, isolated margin trading settings automatic:Auto transfers autonomy:Manual transfers") + errInvalidCounterParties = errors.New("missing counter parties") + errMissingRFQIDOrQuoteID = errors.New("either RFQ ID or Quote ID is missing") + errMissingRFQID = errors.New("error missing rfq ID") + errMissingLegs = errors.New("missing legs") + errMissingSizeOfQuote = errors.New("missing size of quote leg") + errMissingLegsQuotePrice = errors.New("error missing quote price") + errInvalidLoanAllocationValue = errors.New("invalid loan allocation value, must be between 0 to 100") + errInvalidSubaccount = errors.New("invalid sub-account type") + errMissingAlgoOrderType = errors.New("missing algo order type 'grid': Spot grid, \"contract_grid\": Contract grid") + errInvalidGridQuantity = errors.New("invalid grid quantity (grid number)") + errRunTypeRequired = errors.New("runType is required; possible values are 1: Arithmetic, 2: Geometric") + errMissingRequiredArgumentDirection = errors.New("missing required argument, direction") + errInvalidLeverage = errors.New("invalid leverage value") + errMissingValidStopType = errors.New("invalid grid order stop type, only values are \"1\" and \"2\" ") + errMissingSubOrderType = errors.New("missing sub order type") + errMissingQuantity = errors.New("invalid quantity to buy or sell") + errAddressRequired = errors.New("address is required") + errInvalidWebsocketEvent = errors.New("invalid websocket event") + errMissingValidChannelInformation = errors.New("missing channel information") + errMaxRFQOrdersToCancel = errors.New("no more than 100 RFQ cancel order parameter is allowed") + errInvalidUnderlying = errors.New("invalid underlying") + errInstrumentFamilyOrUnderlyingRequired = errors.New("either underlying or instrument family is required") + errMissingRequiredParameter = errors.New("missing required parameter") + errMissingMakerInstrumentSettings = errors.New("missing maker instrument settings") + errInvalidSubAccountName = errors.New("invalid sub-account name") + errInvalidAPIKey = errors.New("invalid api key") + errInvalidMarginTypeAdjust = errors.New("invalid margin type adjust, only 'add' and 'reduce' are allowed") + errInvalidAlgoOrderType = errors.New("invalid algo order type") + errInvalidIPAddress = errors.New("invalid ip address") + errInvalidAPIKeyPermission = errors.New("invalid API Key permission") + errInvalidResponseParam = errors.New("invalid response parameter, response must be non-nil pointer") + errInvalidDuration = errors.New("invalid grid contract duration, only '7D', '30D', and '180D' are allowed") + errInvalidProtocolType = errors.New("invalid protocol type, only 'staking' and 'defi' allowed") + errExceedLimit = errors.New("limit exceeded") + errOnlyThreeMonthsSupported = errors.New("only three months of trade data retrieval supported") + errOnlyOneResponseExpected = errors.New("one response item expected") + errStrategyNameRequired = errors.New("strategy name required") + errRecurringDayRequired = errors.New("recurring day is required") + errRecurringBuyTimeRequired = errors.New("recurring buy time, the value range is an integer with value between 0 and 23") + errSubPositionIDRequired = errors.New("sub position ID is required") + errUserIDRequired = errors.New("uid is required") + errSubPositionCloseTypeRequired = errors.New("sub position close type") + errUniqueCodeRequired = errors.New("unique code is required") + errLastDaysRequired = errors.New("last days required") + errCopyInstrumentIDTypeRequired = errors.New("copy instrument ID type is required") + errInvalidChecksum = errors.New("invalid checksum") + errInvalidPositionMode = errors.New("invalid position mode") + errLendingTermIsRequired = errors.New("lending term is required") + errRateRequired = errors.New("lending rate is required") + errQuarterValueRequired = errors.New("quarter is required") + errYearRequired = errors.New("year is required") + errBorrowTypeRequired = errors.New("borrow type is required") + errMaxRateRequired = errors.New("max rate is required") + errLendingSideRequired = errors.New("lending side is required") + errPaymentMethodRequired = errors.New("payment method required") + errIDNotSet = errors.New("ID is not set") + errMonthNameRequired = errors.New("month name is required") + errPriceTrackingNotSet = errors.New("price tracking value not set") + errInvoiceTextMissing = errors.New("missing invoice text") + errFeeTypeUnsupported = errors.New("fee type is not supported") +) + // testNetKey this key is designed for using the testnet endpoints // setting context.WithValue(ctx, testNetKey("testnet"), useTestNet) // will ensure the appropriate headers are sent to OKx to use the testnet @@ -71,9 +166,22 @@ type testNetKey string var testNetVal = testNetKey("testnet") -// Market Data Endpoints +type candlestickState int8 + +// candlestickState represents index candlestick states. +const ( + StateUncompleted candlestickState = iota + StateCompleted +) + +// PremiumInfo represents data on premiums for the past 6 months. +type PremiumInfo struct { + InstrumentID string `json:"instId"` + Premium string `json:"premium"` + Timestamp string `json:"ts"` +} -// TickerResponse represents the market data endpoint ticker detail +// TickerResponse represents the detailed data from the market ticker endpoint. type TickerResponse struct { InstrumentType string `json:"instType"` InstrumentID string `json:"instId"` @@ -89,166 +197,144 @@ type TickerResponse struct { VolCcy24H types.Number `json:"volCcy24h"` Vol24H types.Number `json:"vol24h"` - OpenPriceInUTC0 string `json:"sodUtc0"` - OpenPriceInUTC8 string `json:"sodUtc8"` - TickerDataGenerationTime okxUnixMilliTime `json:"ts"` + OpenPriceInUTC0 string `json:"sodUtc0"` + OpenPriceInUTC8 string `json:"sodUtc8"` + TickerDataGenerationTime types.Time `json:"ts"` } -// IndexTicker represents Index ticker data. +// IndexTicker represents data from the index ticker. type IndexTicker struct { - InstID string `json:"instId"` - IdxPx types.Number `json:"idxPx"` - High24H types.Number `json:"high24h"` - SodUtc0 types.Number `json:"sodUtc0"` - Open24H types.Number `json:"open24h"` - Low24H types.Number `json:"low24h"` - SodUtc8 types.Number `json:"sodUtc8"` - Timestamp okxUnixMilliTime `json:"ts"` + InstID string `json:"instId"` + IdxPx types.Number `json:"idxPx"` + High24H types.Number `json:"high24h"` + SodUtc0 types.Number `json:"sodUtc0"` + Open24H types.Number `json:"open24h"` + Low24H types.Number `json:"low24h"` + SodUtc8 types.Number `json:"sodUtc8"` + Timestamp types.Time `json:"ts"` } -// OrderBookResponse holds the order asks and bids at a specific timestamp -type OrderBookResponse struct { - Asks [][4]string `json:"asks"` - Bids [][4]string `json:"bids"` - GenerationTimeStamp okxUnixMilliTime `json:"ts"` -} - -// OrderBookResponseDetail holds the order asks and bids in a struct field with the corresponding order generation timestamp. +// OrderBookResponseDetail contains the ask and bid orders, structured with fields that include the timestamp of order generation. type OrderBookResponseDetail struct { - Asks []OrderAsk - Bids []OrderBid + Asks []OrderbookItemDetail + Bids []OrderbookItemDetail GenerationTimestamp time.Time } -// OrderAsk represents currencies bid detailed information. -type OrderAsk struct { - DepthPrice float64 - NumberOfContracts float64 - LiquidationOrders int64 - NumberOfOrders int64 +// OrderbookItemDetail represents detailed information about currency bids. +type OrderbookItemDetail struct { + DepthPrice types.Number + Amount types.Number + LiquidationOrders types.Number + NumberOfOrders types.Number } -// OrderBid represents currencies bid detailed information. -type OrderBid struct { - DepthPrice float64 - BaseCurrencies float64 - LiquidationOrders int64 - NumberOfOrders int64 +// UnmarshalJSON deserializes byte data into OrderbookItemDetail instance +func (o *OrderbookItemDetail) UnmarshalJSON(data []byte) error { + target := [4]any{&o.DepthPrice, &o.Amount, &o.LiquidationOrders, &o.NumberOfOrders} + return json.Unmarshal(data, &target) } -// GetOrderBookResponseDetail returns the OrderBookResponseDetail instance from OrderBookResponse object. -func (a *OrderBookResponse) GetOrderBookResponseDetail() (*OrderBookResponseDetail, error) { - asks, er := a.GetAsks() - if er != nil { - return nil, er - } - bids, er := a.GetBids() - if er != nil { - return nil, er - } - return &OrderBookResponseDetail{ - Asks: asks, - Bids: bids, - GenerationTimestamp: a.GenerationTimeStamp.Time(), - }, nil +// CandlestickHistoryItem retrieves historical candlestick charts for the index or mark price from recent years. +type CandlestickHistoryItem struct { + Timestamp types.Time + OpenPrice types.Number + HighestPrice types.Number + LowestPrice types.Number + ClosePrice types.Number + Confirm candlestickState } -// GetAsks returns list of asks from an order book response instance. -func (a *OrderBookResponse) GetAsks() ([]OrderAsk, error) { - asks := make([]OrderAsk, len(a.Asks)) - for x := range a.Asks { - depthPrice, er := strconv.ParseFloat(a.Asks[x][0], 64) - if er != nil { - return nil, er - } - contracts, er := strconv.ParseFloat(a.Asks[x][1], 64) - if er != nil { - return nil, er - } - liquidation, er := strconv.ParseInt(a.Asks[x][2], 10, 64) - if er != nil { - return nil, er - } - orders, er := strconv.ParseInt(a.Asks[x][3], 10, 64) - if er != nil { - return nil, er - } - asks[x] = OrderAsk{ - DepthPrice: depthPrice, - NumberOfContracts: contracts, - LiquidationOrders: liquidation, - NumberOfOrders: orders, - } +// UnmarshalJSON converts the data slice into a CandlestickHistoryItem instance. +func (c *CandlestickHistoryItem) UnmarshalJSON(data []byte) error { + var state string + target := []any{&c.Timestamp, &c.OpenPrice, &c.HighestPrice, &c.LowestPrice, &c.ClosePrice, &state} + err := json.Unmarshal(data, &target) + if err != nil { + return err } - return asks, nil -} - -// GetBids returns list of order bids instance from list of slice. -func (a *OrderBookResponse) GetBids() ([]OrderBid, error) { - bids := make([]OrderBid, len(a.Bids)) - for x := range a.Bids { - depthPrice, er := strconv.ParseFloat(a.Bids[x][0], 64) - if er != nil { - return nil, er - } - currencies, er := strconv.ParseFloat(a.Bids[x][1], 64) - if er != nil { - return nil, er - } - liquidation, er := strconv.ParseInt(a.Bids[x][2], 10, 64) - if er != nil { - return nil, er - } - orders, er := strconv.ParseInt(a.Bids[x][3], 10, 64) - if er != nil { - return nil, er - } - bids[x] = OrderBid{ - DepthPrice: depthPrice, - BaseCurrencies: currencies, - LiquidationOrders: liquidation, - NumberOfOrders: orders, - } + if state == "1" { + c.Confirm = StateCompleted + } else { + c.Confirm = StateUncompleted } - return bids, nil + return nil } -// CandleStick holds candlestick price data +// CandleStick stores candlestick price data. type CandleStick struct { - OpenTime time.Time - OpenPrice float64 - HighestPrice float64 - LowestPrice float64 - ClosePrice float64 - Volume float64 - QuoteAssetVolume float64 + OpenTime types.Time + OpenPrice types.Number + HighestPrice types.Number + LowestPrice types.Number + ClosePrice types.Number + Volume types.Number + QuoteAssetVolume types.Number +} + +// UnmarshalJSON deserializes slice of data into Candlestick structure +func (c *CandleStick) UnmarshalJSON(data []byte) error { + target := [7]any{&c.OpenTime, &c.OpenPrice, &c.HighestPrice, &c.LowestPrice, &c.ClosePrice, &c.Volume, &c.QuoteAssetVolume} + return json.Unmarshal(data, &target) } -// TradeResponse represents the recent transaction instance. +// TradeResponse represents the recent transaction instance type TradeResponse struct { - InstrumentID string `json:"instId"` - TradeID string `json:"tradeId"` - Price types.Number `json:"px"` - Quantity types.Number `json:"sz"` - Side order.Side `json:"side"` - Timestamp okxUnixMilliTime `json:"ts"` + InstrumentID string `json:"instId"` + TradeID string `json:"tradeId"` + Price types.Number `json:"px"` + Quantity types.Number `json:"sz"` + Side order.Side `json:"side"` + Timestamp types.Time `json:"ts"` + Count string `json:"count"` +} + +// InstrumentFamilyTrade represents transaction information of instrument. +// instrument family, e.g. BTC-USD Applicable to OPTION +type InstrumentFamilyTrade struct { + Vol24H types.Number `json:"vol24h"` + TradeInfo []struct { + InstrumentID string `json:"instId"` + TradeID string `json:"tradeId"` + Side string `json:"side"` + Size types.Number `json:"sz"` + Price types.Number `json:"px"` + Timestamp types.Time `json:"ts"` + } `json:"tradeInfo"` + OptionType string `json:"optType"` +} + +// OptionTrade holds option trade item +type OptionTrade struct { + FillVolume types.Number `json:"fillVol"` + ForwardPrice types.Number `json:"fwdPx"` + IndexPrice types.Number `json:"idxPx"` + MarkPrice types.Number `json:"markPx"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + InstrumentFamily string `json:"instFamily"` + InstrumentID string `json:"instId"` + OptionType string `json:"optType"` + Side string `json:"side"` + TradeID string `json:"tradeId"` + Timestamp types.Time `json:"ts"` } -// TradingVolumeIn24HR response model. +// TradingVolumeIn24HR response model type TradingVolumeIn24HR struct { - BlockVolumeInCNY types.Number `json:"blockVolCny"` - BlockVolumeInUSD types.Number `json:"blockVolUsd"` - TradingVolumeInUSD types.Number `json:"volUsd"` - TradingVolumeInCny types.Number `json:"volCny"` - Timestamp okxUnixMilliTime `json:"ts"` + BlockVolumeInCNY types.Number `json:"blockVolCny"` + BlockVolumeInUSD types.Number `json:"blockVolUsd"` + TradingVolumeInUSD types.Number `json:"volUsd"` + TradingVolumeInCny types.Number `json:"volCny"` + Timestamp types.Time `json:"ts"` } -// OracleSmartContractResponse returns the crypto price of signing using Open Oracle smart contract. +// OracleSmartContractResponse represents the cryptocurrency price signed using the Open Oracle smart contract. type OracleSmartContractResponse struct { Messages []string `json:"messages"` Prices map[string]string `json:"prices"` Signatures []string `json:"signatures"` - Timestamp okxUnixMilliTime `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` } // UsdCnyExchangeRate the exchange rate for converting from USD to CNV @@ -258,10 +344,10 @@ type UsdCnyExchangeRate struct { // IndexComponent represents index component data on the market type IndexComponent struct { + Index string `json:"index"` Components []IndexComponentItem `json:"components"` Last types.Number `json:"last"` - Index string `json:"index"` - Timestamp okxUnixMilliTime `json:"ts"` + Timestamp types.Time `json:"ts"` } // IndexComponentItem an item representing the index component item @@ -275,12 +361,13 @@ type IndexComponentItem struct { // InstrumentsFetchParams request params for requesting list of instruments type InstrumentsFetchParams struct { - InstrumentType string // Mandatory - Underlying string // Optional - InstrumentID string // Optional + InstrumentType string // Mandatory + Underlying string // Optional + InstrumentFamily string + InstrumentID string // Optional } -// Instrument representing an instrument with open contract. +// Instrument representing an instrument with open contract type Instrument struct { InstrumentType string `json:"instType"` InstrumentID string `json:"instId"` @@ -294,9 +381,9 @@ type Instrument struct { ContractMultiplier types.Number `json:"ctMult"` ContractValueCurrency string `json:"ctValCcy"` OptionType string `json:"optType"` - StrikePrice string `json:"stk"` - ListTime okxTime `json:"listTime"` - ExpTime okxTime `json:"expTime"` + StrikePrice types.Number `json:"stk"` + ListTime types.Time `json:"listTime"` + ExpTime types.Time `json:"expTime"` MaxLeverage types.Number `json:"lever"` TickSize types.Number `json:"tickSz"` LotSize types.Number `json:"lotSz"` @@ -312,7 +399,7 @@ type Instrument struct { MaxStopSize types.Number `json:"maxStopSz"` } -// DeliveryHistoryDetail holds instrument id and delivery price information detail +// DeliveryHistoryDetail holds instrument ID and delivery price information detail type DeliveryHistoryDetail struct { Type string `json:"type"` InstrumentID string `json:"insId"` @@ -321,87 +408,97 @@ type DeliveryHistoryDetail struct { // DeliveryHistory represents list of delivery history detail items and timestamp information type DeliveryHistory struct { - Timestamp okxUnixMilliTime `json:"ts"` + Timestamp types.Time `json:"ts"` Details []DeliveryHistoryDetail `json:"details"` } -// OpenInterest Retrieve the total open interest for contracts on OKX. +// OpenInterest Retrieve the total open interest for contracts on OKX type OpenInterest struct { - InstrumentType asset.Item `json:"instType"` - InstrumentID string `json:"instId"` - OpenInterest types.Number `json:"oi"` - OpenInterestCurrency types.Number `json:"oiCcy"` - Timestamp okxUnixMilliTime `json:"ts"` + InstrumentType asset.Item `json:"instType"` + InstrumentID string `json:"instId"` + OpenInterest types.Number `json:"oi"` + OpenInterestCurrency types.Number `json:"oiCcy"` + Timestamp types.Time `json:"ts"` } // FundingRateResponse response data for the Funding Rate for an instruction type type FundingRateResponse struct { - FundingRate types.Number `json:"fundingRate"` - RealisedRate types.Number `json:"realizedRate"` - FundingTime okxUnixMilliTime `json:"fundingTime"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - NextFundingRate types.Number `json:"nextFundingRate"` - NextFundingTime okxUnixMilliTime `json:"nextFundingTime"` + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + FundingRateMethod string `json:"method"` + FundingRate types.Number `json:"fundingRate"` + NextFundingRate types.Number `json:"nextFundingRate"` + FundingTime types.Time `json:"fundingTime"` + NextFundingTime types.Time `json:"nextFundingTime"` + MinFundingRate types.Number `json:"minFundingRate"` + MaxFundingRate types.Number `json:"maxFundingRate"` + SettlementStateOfFundingRate string `json:"settState"` + SettlementFundingRate types.Number `json:"settFundingRate"` + Premium string `json:"premium"` + Timestamp types.Time `json:"ts"` } // LimitPriceResponse hold an information for type LimitPriceResponse struct { - InstrumentType string `json:"instType"` - InstID string `json:"instId"` - BuyLimit types.Number `json:"buyLmt"` - SellLimit types.Number `json:"sellLmt"` - Timestamp okxUnixMilliTime `json:"ts"` + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + BuyLimit types.Number `json:"buyLmt"` + SellLimit types.Number `json:"sellLmt"` + Timestamp types.Time `json:"ts"` } // OptionMarketDataResponse holds response data for option market data type OptionMarketDataResponse struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - Underlying string `json:"uly"` - Delta types.Number `json:"delta"` - Gamma types.Number `json:"gamma"` - Theta types.Number `json:"theta"` - Vega types.Number `json:"vega"` - DeltaBS types.Number `json:"deltaBS"` - GammaBS types.Number `json:"gammaBS"` - ThetaBS types.Number `json:"thetaBS"` - VegaBS types.Number `json:"vegaBS"` - RealVol string `json:"realVol"` - BidVolatility string `json:"bidVol"` - AskVolatility types.Number `json:"askVol"` - MarkVolatility types.Number `json:"markVol"` - Leverage types.Number `json:"lever"` - ForwardPrice string `json:"fwdPx"` - Timestamp okxUnixMilliTime `json:"ts"` -} - -// DeliveryEstimatedPrice holds an estimated delivery or exercise price response. + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + Underlying string `json:"uly"` + Delta types.Number `json:"delta"` + Gamma types.Number `json:"gamma"` + Theta types.Number `json:"theta"` + Vega types.Number `json:"vega"` + DeltaBS types.Number `json:"deltaBS"` + GammaBS types.Number `json:"gammaBS"` + ThetaBS types.Number `json:"thetaBS"` + VegaBS types.Number `json:"vegaBS"` + RealVol types.Number `json:"realVol"` + BidVolatility types.Number `json:"bidVol"` + AskVolatility types.Number `json:"askVol"` + MarkVolatility types.Number `json:"markVol"` + Leverage types.Number `json:"lever"` + ForwardPrice types.Number `json:"fwdPx"` + Timestamp types.Time `json:"ts"` +} + +// DeliveryEstimatedPrice holds an estimated delivery or exercise price response type DeliveryEstimatedPrice struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - EstimatedDeliveryPrice string `json:"settlePx"` - Timestamp okxUnixMilliTime `json:"ts"` + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + EstimatedDeliveryPrice types.Number `json:"settlePx"` + Timestamp types.Time `json:"ts"` } -// DiscountRate represents the discount rate amount, currency, and other discount related information. +// DiscountRate represents the discount rate amount, currency, and other discount related information type DiscountRate struct { - Amount string `json:"amt"` + Amount types.Number `json:"amt"` Currency string `json:"ccy"` - DiscountInfo []DiscountRateInfoItem `json:"discountInfo"` DiscountRateLevel string `json:"discountLv"` + MinDiscountRate types.Number `json:"minDiscountRate"` + DiscountInfo []DiscountRateInfoItem `json:"discountInfo"` } // DiscountRateInfoItem represents discount info list item for discount rate response type DiscountRateInfoItem struct { - DiscountRate string `json:"discountRate"` - MaxAmount types.Number `json:"maxAmt"` - MinAmount types.Number `json:"minAmt"` + DiscountRate types.Number `json:"discountRate"` + MaxAmount types.Number `json:"maxAmt"` + MinAmount types.Number `json:"minAmt"` + Tiers string `json:"tier"` + LiqPenaltyRate types.Number `json:"liqPenaltyRate"` + DiscountCurrencyEquity types.Number `json:"disCcyEq"` } -// ServerTime returning the server time instance. +// ServerTime returning the server time instance type ServerTime struct { - Timestamp okxUnixMilliTime `json:"ts"` + Timestamp types.Time `json:"ts"` } // LiquidationOrderRequestParams holds information to request liquidation orders @@ -420,33 +517,34 @@ type LiquidationOrderRequestParams struct { // LiquidationOrder represents liquidation order item detailed information type LiquidationOrder struct { - Details []LiquidationOrderDetailItem `json:"details"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - TotalLoss string `json:"totalLoss"` - Underlying string `json:"uly"` + Details []LiquidationOrderDetailItem `json:"details"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + TotalLoss types.Number `json:"totalLoss"` + Underlying string `json:"uly"` + InstrumentFamily string `json:"instFamily"` } // LiquidationOrderDetailItem represents the detail information of liquidation order type LiquidationOrderDetailItem struct { - BankruptcyLoss string `json:"bkLoss"` - BankruptcyPx string `json:"bkPx"` - Currency string `json:"ccy"` - PosSide string `json:"posSide"` - Side string `json:"side"` // May be empty - QuantityOfLiquidation types.Number `json:"sz"` - Timestamp okxUnixMilliTime `json:"ts"` + BankruptcyLoss string `json:"bkLoss"` + BankruptcyPrice types.Number `json:"bkPx"` + Currency string `json:"ccy"` + PositionSide string `json:"posSide"` + Side string `json:"side"` // May be empty + QuantityOfLiquidation types.Number `json:"sz"` + Timestamp types.Time `json:"ts"` } -// MarkPrice represents a mark price information for a single instrument id +// MarkPrice represents a mark price information for a single instrument ID type MarkPrice struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - MarkPrice string `json:"markPx"` - Timestamp okxUnixMilliTime `json:"ts"` + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + MarkPrice types.Number `json:"markPx"` + Timestamp types.Time `json:"ts"` } -// PositionTiers represents position tier detailed information. +// PositionTiers represents position tier detailed information type PositionTiers struct { BaseMaxLoan string `json:"baseMaxLoan"` InitialMarginRequirement string `json:"imr"` @@ -461,22 +559,28 @@ type PositionTiers struct { Underlying string `json:"uly"` } -// InterestRateLoanQuotaBasic holds the basic Currency, loan,and interest rate information. +// InterestRateLoanQuotaBasic holds the basic Currency, loan,and interest rate information type InterestRateLoanQuotaBasic struct { Currency string `json:"ccy"` LoanQuota string `json:"quota"` InterestRate types.Number `json:"rate"` } -// InterestRateLoanQuotaItem holds the basic Currency, loan,interest rate, and other level and VIP related information. +// InterestRateLoanQuotaItem holds the basic Currency, loan,interest rate, and other level and VIP related information type InterestRateLoanQuotaItem struct { - InterestRateLoanQuotaBasic - InterestRateDiscount types.Number `json:"0.7"` + Basic []InterestRateLoanQuotaBasic `json:"basic"` + VIP []InterestAndLoanDetail `json:"vip"` + Regular []InterestAndLoanDetail `json:"regular"` +} + +// InterestAndLoanDetail represents an interest rate and loan quota information +type InterestAndLoanDetail struct { + InterestRateDiscount types.Number `json:"irDiscount"` LoanQuotaCoefficient types.Number `json:"loanQuotaCoef"` - Level string `json:"level"` + UserLevel string `json:"level"` } -// VIPInterestRateAndLoanQuotaInformation holds interest rate and loan quoata information for VIP users. +// VIPInterestRateAndLoanQuotaInformation holds interest rate and loan quoata information for VIP users type VIPInterestRateAndLoanQuotaInformation struct { InterestRateLoanQuotaBasic LevelList []struct { @@ -485,34 +589,41 @@ type VIPInterestRateAndLoanQuotaInformation struct { } `json:"levelList"` } -// InsuranceFundInformationRequestParams insurance fund balance information. +// InsuranceFundInformationRequestParams insurance fund balance information type InsuranceFundInformationRequestParams struct { - InstrumentType string `json:"instType"` - Type string `json:"type"` // Type values allowed are `liquidation_balance_deposit, bankruptcy_loss, and platform_revenue` - Underlying string `json:"uly"` - Currency string `json:"ccy"` - Before time.Time `json:"before"` - After time.Time `json:"after"` - Limit int64 `json:"limit"` + InstrumentType string `json:"instType"` + InsuranceType string `json:"type"` // Type values allowed are `liquidation_balance_deposit, bankruptcy_loss, and platform_revenue` + Underlying string `json:"uly"` + InstrumentFamily string `json:"instFamily"` + Currency currency.Code `json:"ccy"` + Before time.Time `json:"before"` + After time.Time `json:"after"` + Limit int64 `json:"limit"` } -// InsuranceFundInformation holds insurance fund information data. +// InsuranceFundInformation holds insurance fund information data type InsuranceFundInformation struct { - Details []InsuranceFundInformationDetail `json:"details"` - Total types.Number `json:"total"` + Details []InsuranceFundInformationDetail `json:"details"` + InstrumentFamily string `json:"instFamily"` + InstrumentType string `json:"instType"` + Total types.Number `json:"total"` } // InsuranceFundInformationDetail represents an Insurance fund information item for a // single currency and type type InsuranceFundInformationDetail struct { - Amount types.Number `json:"amt"` - Balance types.Number `json:"balance"` - Currency string `json:"ccy"` - Timestamp okxUnixMilliTime `json:"ts"` - Type string `json:"type"` -} - -// SupportedCoinsData holds information about currencies supported by the trading data endpoints. + Timestamp types.Time `json:"ts"` + Amount types.Number `json:"amt"` + Balance types.Number `json:"balance"` + Currency string `json:"ccy"` + InsuranceType string `json:"type"` + MaxBalance types.Number `json:"maxBal"` + MaxBalTimestamp types.Time `json:"maxBalTs"` + RealTimeInsuranceDeclineRate types.Number `json:"decRate"` + ADLType string `json:"adlType"` +} + +// SupportedCoinsData holds information about currencies supported by the trading data endpoints type SupportedCoinsData struct { Contract []string `json:"contract"` TradingOptions []string `json:"option"` @@ -521,69 +632,152 @@ type SupportedCoinsData struct { // TakerVolume represents taker volume information with creation timestamp type TakerVolume struct { - Timestamp time.Time `json:"ts"` - SellVolume float64 - BuyVolume float64 + Timestamp types.Time + SellVolume types.Number + BuyVolume types.Number +} + +// UnmarshalJSON deserializes a slice of data into TakerVolume +func (t *TakerVolume) UnmarshalJSON(data []byte) error { + deploy := [3]any{&t.Timestamp, &t.SellVolume, &t.BuyVolume} + return json.Unmarshal(data, &deploy) } // MarginLendRatioItem represents margin lend ration information and creation timestamp type MarginLendRatioItem struct { - Timestamp time.Time `json:"ts"` - MarginLendRatio float64 `json:"ratio"` + Timestamp types.Time + MarginLendRatio types.Number } -// LongShortRatio represents the ratio of users with net long vs net short positions for futures and perpetual swaps. +// UnmarshalJSON deserializes a slice of data into MarginLendRatio +func (m *MarginLendRatioItem) UnmarshalJSON(data []byte) error { + target := [2]any{&m.Timestamp, &m.MarginLendRatio} + return json.Unmarshal(data, &target) +} + +// LongShortRatio represents the ratio of users with net long vs net short positions for futures and perpetual swaps type LongShortRatio struct { - Timestamp time.Time `json:"ts"` - MarginLendRatio float64 `json:"ratio"` + Timestamp types.Time + MarginLendRatio types.Number +} + +// UnmarshalJSON deserializes a slice of data into LongShortRatio +func (l *LongShortRatio) UnmarshalJSON(data []byte) error { + target := [2]any{&l.Timestamp, &l.MarginLendRatio} + return json.Unmarshal(data, &target) } -// OpenInterestVolume represents open interest and trading volume item for currencies of futures and perpetual swaps. +// OpenInterestVolume represents open interest and trading volume item for currencies of futures and perpetual swaps type OpenInterestVolume struct { - Timestamp time.Time `json:"ts"` - OpenInterest float64 `json:"oi"` - Volume float64 `json:"vol"` + Timestamp types.Time + OpenInterest types.Number + Volume types.Number +} + +// UnmarshalJSON deserializes json data into OpenInterestVolume struct +func (p *OpenInterestVolume) UnmarshalJSON(data []byte) error { + deploy := [3]any{&p.Timestamp, &p.OpenInterest, &p.Volume} + return json.Unmarshal(data, &deploy) } -// OpenInterestVolumeRatio represents open interest and trading volume ratio for currencies of futures and perpetual swaps. +// OpenInterestVolumeRatio represents open interest and trading volume ratio for currencies of futures and perpetual swaps type OpenInterestVolumeRatio struct { - Timestamp time.Time `json:"ts"` - OpenInterestRatio float64 `json:"oiRatio"` - VolumeRatio float64 `json:"volRatio"` + Timestamp types.Time + OpenInterestRatio types.Number + VolumeRatio types.Number +} + +// UnmarshalJSON deserializes json data into OpenInterestVolumeRatio +func (o *OpenInterestVolumeRatio) UnmarshalJSON(data []byte) error { + deploy := [3]any{&o.Timestamp, &o.OpenInterestRatio, &o.VolumeRatio} + return json.Unmarshal(data, &deploy) } -// ExpiryOpenInterestAndVolume represents open interest and trading volume of calls and puts for each upcoming expiration. +// ExpiryOpenInterestAndVolume represents open interest and trading volume of calls and puts for each upcoming expiration type ExpiryOpenInterestAndVolume struct { - Timestamp time.Time + Timestamp types.Time ExpiryTime time.Time - CallOpenInterest float64 - PutOpenInterest float64 - CallVolume float64 - PutVolume float64 + CallOpenInterest types.Number + PutOpenInterest types.Number + CallVolume types.Number + PutVolume types.Number +} + +// UnmarshalJSON deserializes slice of data into ExpiryOpenInterestAndVolume structure +func (e *ExpiryOpenInterestAndVolume) UnmarshalJSON(data []byte) error { + var expiryTimeString string + target := [6]any{&e.Timestamp, &expiryTimeString, &e.CallOpenInterest, &e.PutOpenInterest, &e.CallVolume, &e.PutVolume} + err := json.Unmarshal(data, &target) + if err != nil { + return err + } + if expiryTimeString != "" && len(expiryTimeString) == 8 { + year, err := strconv.ParseInt(expiryTimeString[0:4], 10, 64) + if err != nil { + return err + } + month, err := strconv.ParseInt(expiryTimeString[4:6], 10, 64) + if err != nil { + return err + } + var months string + var days string + if month <= 9 { + months = "0" + strconv.FormatInt(month, 10) + } else { + months = strconv.FormatInt(month, 10) + } + day, err := strconv.ParseInt(expiryTimeString[6:], 10, 64) + if err != nil { + return err + } + if day <= 9 { + days = "0" + strconv.FormatInt(day, 10) + } else { + days = strconv.FormatInt(day, 10) + } + e.ExpiryTime, err = time.Parse("2006-01-02", strconv.FormatInt(year, 10)+"-"+months+"-"+days) + if err != nil { + return err + } + } + return nil } -// StrikeOpenInterestAndVolume represents open interest and volume for both buyers and sellers of calls and puts. +// StrikeOpenInterestAndVolume represents open interest and volume for both buyers and sellers of calls and puts type StrikeOpenInterestAndVolume struct { - Timestamp time.Time - Strike int64 - CallOpenInterest float64 - PutOpenInterest float64 - CallVolume float64 - PutVolume float64 + Timestamp types.Time + Strike types.Number + CallOpenInterest types.Number + PutOpenInterest types.Number + CallVolume types.Number + PutVolume types.Number +} + +// UnmarshalJSON deserializes slice of byte data into StrikeOpenInterestAndVolume +func (s *StrikeOpenInterestAndVolume) UnmarshalJSON(data []byte) error { + target := [6]any{&s.Timestamp, &s.Strike, &s.CallOpenInterest, &s.PutOpenInterest, &s.CallVolume, &s.PutVolume} + return json.Unmarshal(data, &target) } -// CurrencyTakerFlow holds the taker volume information for a single currency. +// CurrencyTakerFlow holds the taker volume information for a single currency type CurrencyTakerFlow struct { - Timestamp time.Time - CallBuyVolume float64 - CallSellVolume float64 - PutBuyVolume float64 - PutSellVolume float64 - CallBlockVolume float64 - PutBlockVolume float64 + Timestamp types.Time + CallBuyVolume types.Number + CallSellVolume types.Number + PutBuyVolume types.Number + PutSellVolume types.Number + CallBlockVolume types.Number + PutBlockVolume types.Number +} + +// UnmarshalJSON deserializes a slice of byte data into CurrencyTakerFlow +func (c *CurrencyTakerFlow) UnmarshalJSON(data []byte) error { + target := [7]any{&c.Timestamp, &c.CallBuyVolume, &c.CallSellVolume, &c.PutBuyVolume, &c.PutSellVolume, &c.CallBlockVolume, &c.PutBlockVolume} + return json.Unmarshal(data, &target) } -// PlaceOrderRequestParam requesting parameter for placing an order. +// PlaceOrderRequestParam requesting parameter for placing an order type PlaceOrderRequestParam struct { AssetType asset.Item `json:"-"` InstrumentID string `json:"instId"` @@ -599,8 +793,8 @@ type PlaceOrderRequestParam struct { ReduceOnly bool `json:"reduceOnly,string,omitempty"` QuantityType string `json:"tgtCcy,omitempty"` // values base_ccy and quote_ccy // Added in the websocket requests - BanAmend bool `json:"banAmend,omitempty"` // Whether the SPOT Market Order size can be amended by the system. - ExpiryTime okxUnixMilliTime `json:"expTime,omitempty"` + BanAmend bool `json:"banAmend,omitempty"` // Whether the SPOT Market Order size can be amended by the system. + ExpiryTime types.Time `json:"expTime,omitempty"` } // OrderData response message for place, cancel, and amend an order requests. @@ -609,26 +803,63 @@ type OrderData struct { RequestID string `json:"reqId,omitempty"` ClientOrderID string `json:"clOrdId,omitempty"` Tag string `json:"tag,omitempty"` - SCode string `json:"sCode,omitempty"` - SMessage string `json:"sMsg,omitempty"` + StatusCode string `json:"sCode,omitempty"` + StatusMessage string `json:"sMsg,omitempty"` +} + +// ResponseSuccess holds responses having a status result value +type ResponseSuccess struct { + Result bool `json:"result"` + + StatusCode string `json:"sCode,omitempty"` + StatusMessage string `json:"sMsg,omitempty"` } -// CancelOrderRequestParam represents order parameters to cancel an order. +// CancelOrderRequestParam represents order parameters to cancel an order type CancelOrderRequestParam struct { InstrumentID string `json:"instId"` OrderID string `json:"ordId"` ClientOrderID string `json:"clOrdId,omitempty"` } -// AmendOrderRequestParams represents amend order requesting parameters. +// CancelMassReqParam holds MMP batch cancel request parameters +type CancelMassReqParam struct { + InstrumentType string `json:"instType"` + InstrumentFamily string `json:"instFamily"` +} + +// AmendOrderRequestParams represents amend order requesting parameters type AmendOrderRequestParams struct { InstrumentID string `json:"instId"` CancelOnFail bool `json:"cxlOnFail,omitempty"` OrderID string `json:"ordId,omitempty"` ClientOrderID string `json:"clOrdId,omitempty"` ClientRequestID string `json:"reqId,omitempty"` - NewQuantity float64 `json:"newSz,string,omitempty"` - NewPrice float64 `json:"newPx,string,omitempty"` + NewQuantity float64 `json:"newSz,omitempty,string"` + NewPrice float64 `json:"newPx,omitempty,string"` + + // Modify options orders using USD prices + // Only applicable to options. + // When modifying options orders, users can only fill in one of the following: newPx, newPxUsd, or newPxVol. + NewPriceInUSD float64 `json:"newPxUsd,omitempty,string"` + + NewPriceVolatility float64 `json:"newPxVol,omitempty"` // Modify options orders based on implied volatility, where 1 represents 100%. Only applicable to options. + AttachAlgoOrders []AlgoOrdInfo `json:"attachAlgoOrds,omitempty"` +} + +// AlgoOrdInfo represents TP/SL info attached when placing an order +type AlgoOrdInfo struct { + AttachAlgoID string `json:"attachAlgoId,omitempty"` + AttachAlgoClientOrderID string `json:"attachAlgoClOrdId,omitempty"` + NewTakeProfitTriggerPrice float64 `json:"newTpTriggerPx,omitempty"` + NewTakeProfitOrderPrice float64 `json:"newTpOrdPx,omitempty"` + NewTakeProfitOrderKind string `json:"newTpOrdKind,omitempty"` // possible values are 'condition' and 'limit' + NewStopLossTriggerPrice float64 `json:"newSlTriggerPx,omitempty"` + NewStopLossOrderPrice float64 `json:"newSlOrdPx,omitempty"` + NewTakkeProfitTriggerPriceType string `json:"newTpTriggerPxType,omitempty"` + NewStopLossTriggerPriceType string `json:"newSlTriggerPxType,omitempty"` // possible values are 'last', 'index', and 'mark' + NewSize float64 `json:"sz,omitempty"` + AmendPriceOnTriggerType string `json:"amendPxOnTriggerType,omitempty"` // Whether to enable Cost-price SL. Only applicable to SL order of split TPs. '0': disable, the default value '1': Enable } // ClosePositionsRequestParams input parameters for close position endpoints @@ -642,7 +873,7 @@ type ClosePositionsRequestParams struct { Tag string `json:"tag,omitempty"` } -// ClosePositionResponse response data for close position. +// ClosePositionResponse response data for close position type ClosePositionResponse struct { InstrumentID string `json:"instId"` PositionSide string `json:"posSide"` @@ -655,7 +886,7 @@ type OrderDetailRequestParam struct { ClientOrderID string `json:"clOrdId"` } -// OrderDetail returns a order detail information +// OrderDetail holds detailed information about an order. type OrderDetail struct { InstrumentType string `json:"instType"` InstrumentID string `json:"instId"` @@ -663,18 +894,18 @@ type OrderDetail struct { OrderID string `json:"ordId"` ClientOrderID string `json:"clOrdId"` Tag string `json:"tag"` - ProfitAndLoss string `json:"pnl"` + ProfitAndLoss types.Number `json:"pnl"` OrderType string `json:"ordType"` Side order.Side `json:"side"` PositionSide string `json:"posSide"` TradeMode string `json:"tdMode"` TradeID string `json:"tradeId"` - FillTime time.Time `json:"fillTime"` + FillTime types.Time `json:"fillTime"` Source string `json:"source"` State string `json:"state"` TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` StopLossTriggerPriceType string `json:"slTriggerPxType"` - StopLossOrdPx string `json:"slOrdPx"` + StopLossOrderPrice types.Number `json:"slOrdPx"` RebateCurrency string `json:"rebateCcy"` QuantityType string `json:"tgtCcy"` // base_ccy and quote_ccy Category string `json:"category"` // normal, twap, adl, full_liquidation, partial_liquidation, delivery, ddh @@ -691,11 +922,28 @@ type OrderDetail struct { TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` TakeProfitOrderPrice types.Number `json:"tpOrdPx"` StopLossTriggerPrice types.Number `json:"slTriggerPx"` - UpdateTime time.Time `json:"uTime"` - CreationTime time.Time `json:"cTime"` -} - -// OrderListRequestParams represents order list requesting parameters. + UpdateTime types.Time `json:"uTime"` + CreationTime types.Time `json:"cTime"` + AlgoClOrdID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + AttachAlgoClOrdID string `json:"attachAlgoClOrdId"` + AttachAlgoOrds []any `json:"attachAlgoOrds"` + CancelSource string `json:"cancelSource"` + CancelSourceReason string `json:"cancelSourceReason"` + IsTakeProfitLimit string `json:"isTpLimit"` + LinkedAlgoOrd struct { + AlgoID string `json:"algoId"` + } `json:"linkedAlgoOrd"` + PriceType string `json:"pxType"` + PriceVolume types.Number `json:"pxVol"` + PriceUSD types.Number `json:"pxUsd"` + QuickMgnType string `json:"quickMgnType"` + ReduceOnly bool `json:"reduceOnly,string,omitempty"` + SelfTradePreventionID string `json:"stpId"` + SelfTradePreventionMode string `json:"stpMode"` +} + +// OrderListRequestParams represents order list requesting parameters type OrderListRequestParams struct { InstrumentType string `json:"instType"` // SPOT , MARGIN, SWAP, FUTURES , OPTIONS Underlying string `json:"uly"` @@ -709,59 +957,60 @@ type OrderListRequestParams struct { Limit int64 `json:"limit,omitempty"` } -// OrderHistoryRequestParams holds parameters to request order data history of last 7 days. +// OrderHistoryRequestParams holds parameters to request order data history of last 7 days type OrderHistoryRequestParams struct { OrderListRequestParams Category string `json:"category"` // twap, adl, full_liquidation, partial_liquidation, delivery, ddh } -// PendingOrderItem represents a pending order Item in pending orders list. +// PendingOrderItem represents a pending order Item in pending orders list type PendingOrderItem struct { - AccumulatedFillSize types.Number `json:"accFillSz"` - AveragePrice types.Number `json:"avgPx"` - CreationTime okxUnixMilliTime `json:"cTime"` - Category string `json:"category"` - Currency string `json:"ccy"` - ClientOrderID string `json:"clOrdId"` - Fee types.Number `json:"fee"` - FeeCurrency currency.Code `json:"feeCcy"` - LastFilledPrice types.Number `json:"fillPx"` - LastFilledSize types.Number `json:"fillSz"` - FillTime okxUnixMilliTime `json:"fillTime"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Leverage types.Number `json:"lever"` - OrderID string `json:"ordId"` - OrderType string `json:"ordType"` - ProfitAndLoss string `json:"pnl"` - PositionSide string `json:"posSide"` - RebateAmount types.Number `json:"rebate"` - RebateCurrency string `json:"rebateCcy"` - Side order.Side `json:"side"` - StopLossOrdPrice types.Number `json:"slOrdPx"` - StopLossTriggerPrice types.Number `json:"slTriggerPx"` - StopLossTriggerPriceType string `json:"slTriggerPxType"` - State string `json:"state"` - Price types.Number `json:"px"` - Size types.Number `json:"sz"` - Tag string `json:"tag"` - SizeType string `json:"tgtCcy"` - TradeMode string `json:"tdMode"` - Source string `json:"source"` - TakeProfitOrdPrice types.Number `json:"tpOrdPx"` - TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` - TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` - TradeID string `json:"tradeId"` - UpdateTime okxUnixMilliTime `json:"uTime"` -} - -// TransactionDetailRequestParams retrieve recently-filled transaction details in the last 3 day. + AccumulatedFillSize types.Number `json:"accFillSz"` + AveragePrice types.Number `json:"avgPx"` + CreationTime types.Time `json:"cTime"` + Category string `json:"category"` + Currency string `json:"ccy"` + ClientOrderID string `json:"clOrdId"` + Fee types.Number `json:"fee"` + FeeCurrency currency.Code `json:"feeCcy"` + LastFilledPrice types.Number `json:"fillPx"` + LastFilledSize types.Number `json:"fillSz"` + FillTime types.Time `json:"fillTime"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage types.Number `json:"lever"` + OrderID string `json:"ordId"` + OrderType string `json:"ordType"` + ProfitAndLoss types.Number `json:"pnl"` + PositionSide string `json:"posSide"` + RebateAmount types.Number `json:"rebate"` + RebateCurrency string `json:"rebateCcy"` + Side order.Side `json:"side"` + StopLossOrdPrice types.Number `json:"slOrdPx"` + StopLossTriggerPrice types.Number `json:"slTriggerPx"` + StopLossTriggerPriceType string `json:"slTriggerPxType"` + State string `json:"state"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + Tag string `json:"tag"` + SizeType string `json:"tgtCcy"` + TradeMode string `json:"tdMode"` + Source string `json:"source"` + TakeProfitOrdPrice types.Number `json:"tpOrdPx"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` + TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` + TradeID string `json:"tradeId"` + UpdateTime types.Time `json:"uTime"` +} + +// TransactionDetailRequestParams retrieve recently-filled transaction details in the last 3 day type TransactionDetailRequestParams struct { InstrumentType string `json:"instType"` // SPOT , MARGIN, SWAP, FUTURES , option Underlying string `json:"uly"` InstrumentID string `json:"instId"` OrderID string `json:"ordId"` OrderType string `json:"orderType"` + SubType string `json:"subType,omitempty"` After string `json:"after"` // after billid Before string `json:"before"` // before billid Begin time.Time `json:"begin"` @@ -769,50 +1018,74 @@ type TransactionDetailRequestParams struct { Limit int64 `json:"limit"` } -// TransactionDetail holds ecently-filled transaction detail data. +// FillArchiveParam transaction detail param for 2 year +type FillArchiveParam struct { + Year int64 `json:"year,string"` + Quarter string `json:"quarter"` +} + +// ArchiveReference holds recently-filled transaction details archive link and timestamp information +type ArchiveReference struct { + FileHref string `json:"fileHref"` + State string `json:"state"` + Timestamp types.Time `json:"ts"` +} + +// TransactionDetail holds recently-filled transaction detail data type TransactionDetail struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - TradeID string `json:"tradeId"` - OrderID string `json:"ordId"` - ClientOrderID string `json:"clOrdId"` - BillID string `json:"billId"` - Tag string `json:"tag"` - FillPrice types.Number `json:"fillPx"` - FillSize types.Number `json:"fillSz"` - Side order.Side `json:"side"` - PositionSide string `json:"posSide"` - ExecType string `json:"execType"` - FeeCurrency string `json:"feeCcy"` - Fee string `json:"fee"` - Timestamp okxUnixMilliTime `json:"ts"` -} - -// AlgoOrderParams holds algo order information. + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + TradeID string `json:"tradeId"` + OrderID string `json:"ordId"` + ClientOrderID string `json:"clOrdId"` + TransactionType string `json:"subType"` + BillID string `json:"billId"` + Tag string `json:"tag"` + FillPrice types.Number `json:"fillPx"` + FillSize types.Number `json:"fillSz"` + FillIndexPrice types.Number `json:"fillIdxPx"` + FillProfitAndLoss types.Number `json:"fillPnl"` + FillPriceVolatility types.Number `json:"fillPxVol"` + FillPriceUSD types.Number `json:"fillPxUsd"` + MarkVolatilityWhenFilled types.Number `json:"fillMarkVol"` + ForwardPriceWhenFilled types.Number `json:"fillFwdPx"` + MarkPriceWhenFilled types.Number `json:"fillMarkPx"` + Side order.Side `json:"side"` + PositionSide string `json:"posSide"` + ExecType string `json:"execType"` + FeeCurrency string `json:"feeCcy"` + Fee types.Number `json:"fee"` + FillTime types.Time `json:"fillTime"` + Timestamp types.Time `json:"ts"` +} + +// AlgoOrderParams holds algo order information type AlgoOrderParams struct { - InstrumentID string `json:"instId"` // Required - TradeMode string `json:"tdMode"` // Required - Currency string `json:"ccy,omitempty"` - Side order.Side `json:"side"` // Required - PositionSide string `json:"posSide,omitempty"` - OrderType string `json:"ordType"` // Required - Size float64 `json:"sz,string"` // Required - ReduceOnly bool `json:"reduceOnly,omitempty"` - OrderTag string `json:"tag,omitempty"` - QuantityType string `json:"tgtCcy,omitempty"` + InstrumentID string `json:"instId"` // Required + TradeMode string `json:"tdMode"` // Required + Currency string `json:"ccy,omitempty"` + Side string `json:"side"` // Required + PositionSide string `json:"posSide,omitempty"` + OrderType string `json:"ordType"` // Required + Size float64 `json:"sz,string"` // Required + ReduceOnly bool `json:"reduceOnly,omitempty"` + OrderTag string `json:"tag,omitempty"` + QuantityType string `json:"tgtCcy,omitempty"` + AlgoClientOrderID string `json:"algoClOrdId,omitempty"` // Place Stop Order params - TakeProfitTriggerPrice float64 `json:"tpTriggerPx,string,omitempty"` TakeProfitOrderPrice float64 `json:"tpOrdPx,string,omitempty"` + TakeProfitTriggerPrice float64 `json:"tpTriggerPx,string,omitempty"` + TakeProfitTriggerPriceType string `json:"tpTriggerPxType,omitempty"` StopLossTriggerPrice float64 `json:"slTriggerPx,string,omitempty"` StopLossOrderPrice float64 `json:"slOrdPx,string,omitempty"` StopLossTriggerPriceType string `json:"slTriggerPxType,omitempty"` - TakeProfitTriggerPriceType string `json:"tpTriggerPxType,omitempty"` + CancelOnClosePosition bool `json:"cxlOnClosePos,omitempty"` - // Trigger Price Or TrailingStopOrderRequestParam + // For trigger and trailing stop order CallbackRatio float64 `json:"callbackRatio,omitempty,string"` - ActivePrice float64 `json:"activePx,string,omitempty"` - CallbackSpreadVariance string `json:"callbackSpread,omitempty"` + ActivePrice float64 `json:"activePx,omitempty,string"` + CallbackSpreadVariance float64 `json:"callbackSpread,omitempty,string"` // trigger algo orders params. // notice: Trigger orders are not available in the net mode of futures and perpetual swaps @@ -820,31 +1093,122 @@ type AlgoOrderParams struct { OrderPrice float64 `json:"orderPx,string,omitempty"` // if the price i -1, then the order will be executed on the market. TriggerPriceType string `json:"triggerPxType,omitempty"` // last, index, and mark - PriceVariance string `json:"pxVar,omitempty"` // Optional - PriceSpread string `json:"pxSpread,omitempty"` // Optional - SizeLimit float64 `json:"szLimit,string,omitempty"` // Required - PriceLimit float64 `json:"pxLimit,string,omitempty"` // Required + PriceVariance float64 `json:"pxVar,omitempty,string"` // Optional + PriceSpread float64 `json:"pxSpread,omitempty,string"` // Optional + SizeLimit float64 `json:"szLimit,string,omitempty"` // Required + LimitPrice float64 `json:"pxLimit,string,omitempty"` // Required // TWAPOrder TimeInterval kline.Interval `json:"interval,omitempty"` // Required -} -// StopOrderParams holds stop order request payload. -type StopOrderParams struct { - AlgoOrderParams - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` - TakeProfitOrderType string `json:"tpOrdPx"` - StopLossTriggerPrice string `json:"slTriggerPx"` - StopLossTriggerPriceType string `json:"slTriggerPxType"` - StopLossOrderPrice string `json:"slOrdPx"` + // Chase order + ChaseType string `json:"chaseType,omitempty"` // Possible values: "distance" and "ratio" + ChaseValue float64 `json:"chaseVal,omitempty,string"` + MaxChaseType string `json:"maxChaseType,omitempty"` + MaxChaseValue float64 `json:"maxChaseVal,omitempty,string"` } -// AlgoOrder algo order requests response. +// AlgoOrder algo order requests response type AlgoOrder struct { - AlgoID string `json:"algoId"` - StatusCode string `json:"sCode"` - StatusMsg string `json:"sMsg"` + AlgoID string `json:"algoId"` + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` + ClientOrderID string `json:"clOrdId"` + AlgoClientOrderID string `json:"algoClOrdId"` + Tag string `json:"tag"` +} + +// AmendAlgoOrderParam request parameter to amend an algo order +type AmendAlgoOrderParam struct { + InstrumentID string `json:"instId"` + AlgoID string `json:"algoId,omitempty"` + ClientSuppliedAlgoOrderID string `json:"algoClOrdId,omitempty"` + CancelOrderWhenFail bool `json:"cxlOnFail,omitempty"` // Whether the order needs to be automatically canceled when the order amendment fails Valid options: false or true, the default is false. + RequestID string `json:"reqId,omitempty"` + NewSize float64 `json:"newSz,omitempty,string"` + + // Take Profit Stop Loss Orders + NewTakeProfitTriggerPrice float64 `json:"newTpTriggerPx,omitempty,string"` + NewTakeProfitOrderPrice float64 `json:"newTpOrdPx,omitempty,string"` + NewStopLossTriggerPrice float64 `json:"newSlTriggerPx,omitempty,string"` + NewStopLossOrderPrice float64 `json:"newSlOrdPx,omitempty,string"` // Stop-loss order price If the price is -1, stop-loss will be executed at the market price. + NewTakeProfitTriggerPriceType string `json:"newTpTriggerPxType,omitempty"` // Take-profit trigger price type'last': last price 'index': index price 'mark': mark price + NewStopLossTriggerPriceType string `json:"newSlTriggerPxType,omitempty"` // Stop-loss trigger price type 'last': last price 'index': index price 'mark': mark price + + // Trigger Order parameters + NewTriggerPrice float64 `json:"newTriggerPx,omitempty"` + NewOrderPrice float64 `json:"newOrdPx,omitempty"` + NewTriggerPriceType string `json:"newTriggerPxType,omitempty"` + + AttachAlgoOrders []SubTPSLParams `json:"attachAlgoOrds,omitempty"` +} + +// SubTPSLParams represents take-profit and stop-loss price parameters to be used by algo orders +type SubTPSLParams struct { + NewTakeProfitTriggerPrice float64 `json:"newTpTriggerPx,omitempty,string"` + NewTakeProfitOrderPrice float64 `json:"newTpOrdPx,omitempty,string"` + NewStopLossTriggerPrice float64 `json:"newSlTriggerPx,omitempty,string"` + NewStopLossOrderPrice float64 `json:"newSlOrdPx,omitempty,string"` // Stop-loss order price If the price is -1, stop-loss will be executed at the market price. + NewTakeProfitTriggerPriceType string `json:"newTpTriggerPxType,omitempty"` // Take-profit trigger price type'last': last price 'index': index price 'mark': mark price + NewStopLossTriggerPriceType string `json:"newSlTriggerPxType,omitempty"` // Stop-loss trigger price type 'last': last price 'index': index price 'mark': mark price +} + +// AmendAlgoResponse holds response information of amending an algo order +type AmendAlgoResponse struct { + AlgoClientOrderID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + ReqID string `json:"reqId"` + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` +} + +// AlgoOrderDetail represents an algo order detail +type AlgoOrderDetail struct { + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + OrderID string `json:"ordId"` + OrderIDList []string `json:"ordIdList"` + Currency string `json:"ccy"` + ClientOrderID string `json:"clOrdId"` + AlgoID string `json:"algoId"` + AttachAlgoOrds []string `json:"attachAlgoOrds"` + Size types.Number `json:"sz"` + CloseFraction string `json:"closeFraction"` + OrderType string `json:"ordType"` + Side string `json:"side"` + PositionSide string `json:"posSide"` + TradeMode string `json:"tdMode"` + TargetCurrency string `json:"tgtCcy"` + State string `json:"state"` + Leverage types.Number `json:"lever"` + TpTriggerPrice types.Number `json:"tpTriggerPx"` + TpTriggerPriceType string `json:"tpTriggerPxType"` + TpOrdPrice types.Number `json:"tpOrdPx"` + SlTriggerPrice types.Number `json:"slTriggerPx"` + SlTriggerPriceType string `json:"slTriggerPxType"` + TriggerPrice types.Number `json:"triggerPx"` + TriggerPriceType string `json:"triggerPxType"` + OrderPrice types.Number `json:"ordPx"` + ActualSize types.Number `json:"actualSz"` + ActualPrice types.Number `json:"actualPx"` + ActualSide string `json:"actualSide"` + PriceVar string `json:"pxVar"` + PriceSpread types.Number `json:"pxSpread"` + PriceLimit types.Number `json:"pxLimit"` + SizeLimit types.Number `json:"szLimit"` + Tag string `json:"tag"` + TimeInterval string `json:"timeInterval"` + CallbackRatio types.Number `json:"callbackRatio"` + CallbackSpread string `json:"callbackSpread"` + ActivePrice types.Number `json:"activePx"` + MoveTriggerPrice types.Number `json:"moveTriggerPx"` + ReduceOnly string `json:"reduceOnly"` + TriggerTime types.Time `json:"triggerTime"` + Last types.Number `json:"last"` // Last filled price while placing + FailCode string `json:"failCode"` + AlgoClOrdID string `json:"algoClOrdId"` + AmendPriceOnTriggerType string `json:"amendPxOnTriggerType"` + CreationTime types.Time `json:"cTime"` } // AlgoOrderCancelParams algo order request parameter @@ -853,47 +1217,48 @@ type AlgoOrderCancelParams struct { InstrumentID string `json:"instId"` } -// AlgoOrderResponse holds algo order information. +// AlgoOrderResponse holds algo order information type AlgoOrderResponse struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - OrderID string `json:"ordId"` - Currency string `json:"ccy"` - AlgoOrderID string `json:"algoId"` - Quantity string `json:"sz"` - OrderType string `json:"ordType"` - Side order.Side `json:"side"` - PositionSide string `json:"posSide"` - TradeMode string `json:"tdMode"` - QuantityType string `json:"tgtCcy"` - State string `json:"state"` - Lever string `json:"lever"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` - TakeProfitOrdPrice string `json:"tpOrdPx"` - StopLossTriggerPriceType string `json:"slTriggerPxType"` - StopLossTriggerPrice string `json:"slTriggerPx"` - TriggerPrice string `json:"triggerPx"` - TriggerPriceType string `json:"triggerPxType"` - OrdPrice string `json:"ordPx"` - ActualSize string `json:"actualSz"` - ActualPrice string `json:"actualPx"` - ActualSide string `json:"actualSide"` - PriceVar string `json:"pxVar"` - PriceSpread string `json:"pxSpread"` - PriceLimit string `json:"pxLimit"` - SizeLimit string `json:"szLimit"` - TimeInterval string `json:"timeInterval"` - TriggerTime okxUnixMilliTime `json:"triggerTime"` - CallbackRatio string `json:"callbackRatio"` - CallbackSpread string `json:"callbackSpread"` - ActivePrice string `json:"activePx"` - MoveTriggerPrice string `json:"moveTriggerPx"` - CreationTime okxUnixMilliTime `json:"cTime"` -} - -// CurrencyResponse represents a currency item detail response data. + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + OrderID string `json:"ordId"` + Currency string `json:"ccy"` + AlgoOrderID string `json:"algoId"` + Quantity types.Number `json:"sz"` + OrderType string `json:"ordType"` + Side order.Side `json:"side"` + PositionSide string `json:"posSide"` + TradeMode string `json:"tdMode"` + QuantityType string `json:"tgtCcy"` + State string `json:"state"` + Lever types.Number `json:"lever"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` + TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` + TakeProfitOrdPrice types.Number `json:"tpOrdPx"` + StopLossTriggerPriceType string `json:"slTriggerPxType"` + StopLossTriggerPrice types.Number `json:"slTriggerPx"` + TriggerPrice types.Number `json:"triggerPx"` + TriggerPriceType string `json:"triggerPxType"` + OrderPrice types.Number `json:"ordPx"` + ActualSize types.Number `json:"actualSz"` + ActualPrice types.Number `json:"actualPx"` + ActualSide string `json:"actualSide"` + PriceVar types.Number `json:"pxVar"` + PriceSpread types.Number `json:"pxSpread"` + PriceLimit types.Number `json:"pxLimit"` + SizeLimit types.Number `json:"szLimit"` + TimeInterval string `json:"timeInterval"` + TriggerTime types.Time `json:"triggerTime"` + CallbackRatio types.Number `json:"callbackRatio"` + CallbackSpread string `json:"callbackSpread"` + ActivePrice types.Number `json:"activePx"` + MoveTriggerPrice types.Number `json:"moveTriggerPx"` + CreationTime types.Time `json:"cTime"` +} + +// CurrencyResponse represents a currency item detail response data type CurrencyResponse struct { + Name string `json:"name"` // Chinese name of currency CanDeposit bool `json:"canDep"` // Availability to deposit from chain. false: not available true: available CanInternalTransfer bool `json:"canInternal"` // Availability to internal transfer. CanWithdraw bool `json:"canWd"` // Availability to withdraw to chain. @@ -904,21 +1269,38 @@ type CurrencyResponse struct { MaxFee types.Number `json:"maxFee"` // Minimum withdrawal fee MaxWithdrawal types.Number `json:"maxWd"` // Minimum amount of currency withdrawal in a single transaction MinFee types.Number `json:"minFee"` // Minimum withdrawal fee - MinWithdrawal string `json:"minWd"` // Minimum amount of currency withdrawal in a single transaction - Name string `json:"name"` // Chinese name of currency - UsedWithdrawalQuota string `json:"usedWdQuota"` // Amount of currency withdrawal used in the past 24 hours, unit in BTC - WithdrawalQuota string `json:"wdQuota"` // Minimum amount of currency withdrawal in a single transaction - WithdrawalTickSize string `json:"wdTickSz"` // Withdrawal precision, indicating the number of digits after the decimal point + MinWithdrawal types.Number `json:"minWd"` // Minimum amount of currency withdrawal in a single transaction + UsedWithdrawalQuota types.Number `json:"usedWdQuota"` // Amount of currency withdrawal used in the past 24 hours, unit in BTC + WithdrawalQuota types.Number `json:"wdQuota"` // Minimum amount of currency withdrawal in a single transaction + WithdrawalTickSize types.Number `json:"wdTickSz"` // Withdrawal precision, indicating the number of digits after the decimal point } // AssetBalance represents account owner asset balance type AssetBalance struct { + Currency string `json:"ccy"` AvailBal types.Number `json:"availBal"` Balance types.Number `json:"bal"` - Currency string `json:"ccy"` FrozenBalance types.Number `json:"frozenBal"` } +// NonTradableAsset holds non-tradable asset detail +type NonTradableAsset struct { + Balance types.Number `json:"bal"` + CanWithdraw bool `json:"canWd"` + Currency string `json:"ccy"` + Chain string `json:"chain"` + CtAddr string `json:"ctAddr"` + LogoLink string `json:"logoLink"` + Name string `json:"name"` + NeedTag bool `json:"needTag"` + WithdrawAll bool `json:"wdAll"` + FeeCurrency currency.Code `json:"feeCcy"` + Fee types.Number `json:"fee"` + MinWithdrawal types.Number `json:"minWd"` + WithdrawTickSize types.Number `json:"wdTickSz"` + BurningFeeRate types.Number `json:"burningFeeRate"` +} + // AccountAssetValuation represents view account asset valuation data type AccountAssetValuation struct { Details struct { @@ -927,33 +1309,34 @@ type AccountAssetValuation struct { Funding types.Number `json:"funding"` Trading types.Number `json:"trading"` } `json:"details"` - TotalBalance types.Number `json:"totalBal"` - Timestamp okxUnixMilliTime `json:"ts"` + TotalBalance types.Number `json:"totalBal"` + Timestamp types.Time `json:"ts"` } -// FundingTransferRequestInput represents funding account request input. +// FundingTransferRequestInput represents funding account request input type FundingTransferRequestInput struct { - Currency string `json:"ccy"` - Type int `json:"type,string"` - Amount float64 `json:"amt,string"` - From string `json:"from"` // "6": Funding account, "18": Trading account - To string `json:"to"` - SubAccount string `json:"subAcct"` - LoanTransfer bool `json:"loanTrans,string"` - ClientID string `json:"clientId"` // Client-supplied ID A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters. -} - -// FundingTransferResponse represents funding transfer and trading account transfer response. + Currency currency.Code `json:"ccy"` + TransferType int64 `json:"type,string"` + Amount float64 `json:"amt,string"` + RemittingAccountType string `json:"from"` // "6": Funding account, "18": Trading account + BeneficiaryAccountType string `json:"to"` + SubAccount string `json:"subAcct"` + LoanTransfer bool `json:"loanTrans,string"` + OmitPositionRisk bool `json:"omitPosRisk,omitempty,string"` + ClientID string `json:"clientId"` // Client-supplied ID A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters. +} + +// FundingTransferResponse represents funding transfer and trading account transfer response type FundingTransferResponse struct { TransferID string `json:"transId"` Currency string `json:"ccy"` ClientID string `json:"clientId"` - From int64 `json:"from,string"` + From types.Number `json:"from"` Amount types.Number `json:"amt"` - To int64 `json:"to,string"` + To types.Number `json:"to"` } -// TransferFundRateResponse represents funcing transfer rate response +// TransferFundRateResponse represents funding transfer rate response type TransferFundRateResponse struct { Amount types.Number `json:"amt"` Currency string `json:"ccy"` @@ -965,27 +1348,27 @@ type TransferFundRateResponse struct { To string `json:"to"` ToInstrumentID string `json:"toInstId"` TransferID string `json:"transId"` - Type int `json:"type,string"` + Type types.Number `json:"type"` } // AssetBillDetail represents the billing record type AssetBillDetail struct { - BillID string `json:"billId"` - Currency string `json:"ccy"` - ClientID string `json:"clientId"` - BalanceChange string `json:"balChg"` - AccountBalance string `json:"bal"` - Type int `json:"type,string"` - Timestamp okxUnixMilliTime `json:"ts"` + BillID string `json:"billId"` + Currency string `json:"ccy"` + ClientID string `json:"clientId"` + BalanceChange types.Number `json:"balChg"` + AccountBalance types.Number `json:"bal"` + Type types.Number `json:"type"` + Timestamp types.Time `json:"ts"` } -// LightningDepositItem for creating an invoice. +// LightningDepositItem for creating an invoice type LightningDepositItem struct { - CreationTime okxUnixMilliTime `json:"cTime"` - Invoice string `json:"invoice"` + CreationTime types.Time `json:"cTime"` + Invoice string `json:"invoice"` } -// CurrencyDepositResponseItem represents the deposit address information item. +// CurrencyDepositResponseItem represents the deposit address information item type CurrencyDepositResponseItem struct { Tag string `json:"tag"` Chain string `json:"chain"` @@ -997,30 +1380,48 @@ type CurrencyDepositResponseItem struct { Memo string `json:"memo"` DepositAddressAttachment map[string]string `json:"addrEx"` PaymentID string `json:"pmtId"` + VerifiedName string `json:"verifiedName"` } -// DepositHistoryResponseItem deposit history response item. +// DepositHistoryResponseItem deposit history response item type DepositHistoryResponseItem struct { - Amount types.Number `json:"amt"` - TransactionID string `json:"txId"` // Hash record of the deposit - Currency string `json:"ccy"` - Chain string `json:"chain"` - From string `json:"from"` - ToDepositAddress string `json:"to"` - Timestamp okxUnixMilliTime `json:"ts"` - State int `json:"state,string"` - DepositID string `json:"depId"` + Amount types.Number `json:"amt"` + TransactionID string `json:"txId"` // Hash record of the deposit + Currency string `json:"ccy"` + Chain string `json:"chain"` + From string `json:"from"` + ToDepositAddress string `json:"to"` + Timestamp types.Time `json:"ts"` + State types.Number `json:"state"` + DepositID string `json:"depId"` + AreaCodeFrom string `json:"areaCodeFrom"` + FromWithdrawalID string `json:"fromWdId"` + ActualDepBlkConfirm string `json:"actualDepBlkConfirm"` } // WithdrawalInput represents request parameters for cryptocurrency withdrawal type WithdrawalInput struct { - Amount float64 `json:"amt,string"` - TransactionFee float64 `json:"fee,string"` - WithdrawalDestination string `json:"dest"` - Currency string `json:"ccy"` - ChainName string `json:"chain"` - ToAddress string `json:"toAddr"` - ClientID string `json:"clientId"` + Currency currency.Code `json:"ccy"` + Amount float64 `json:"amt,string"` + TransactionFee float64 `json:"fee,string"` + WithdrawalDestination string `json:"dest"` + ChainName string `json:"chain"` + ToAddress string `json:"toAddr"` + ClientID string `json:"clientId"` + AreaCode string `json:"areaCode,omitempty"` + RecipientInformation *WithdrawalRecipientInformation `json:"rcvrInfo,omitempty"` +} + +// WithdrawalRecipientInformation represents a recipient information for withdrawal +type WithdrawalRecipientInformation struct { + WalletType string `json:"walletType,omitempty"` + ExchangeID string `json:"exchId,omitempty"` + ReceiverFirstName string `json:"rcvrFirstName,omitempty"` + ReceiverLastName string `json:"rcvrLastName,omitempty"` + ReceiverCountry string `json:"rcvrCountry,omitempty"` + ReceiverCountrySubDivision string `json:"rcvrCountrySubDivision,omitempty"` + ReceiverTownName string `json:"rcvrTownName,omitempty"` + ReceiverStreetName string `json:"rcvrStreetName,omitempty"` } // WithdrawalResponse cryptocurrency withdrawal response @@ -1032,90 +1433,111 @@ type WithdrawalResponse struct { Chain string `json:"chain"` } -// LightningWithdrawalRequestInput to request Lightning Withdrawal requests. +// LightningWithdrawalRequestInput to request Lightning Withdrawal requests type LightningWithdrawalRequestInput struct { - Currency string `json:"ccy"` // REQUIRED Token symbol. Currently only BTC is supported. - Invoice string `json:"invoice"` // REQUIRED Invoice text - Memo string `json:"memo"` // Lightning withdrawal memo + Currency currency.Code `json:"ccy"` // REQUIRED Token symbol. Currently only BTC is supported. + Invoice string `json:"invoice"` // REQUIRED Invoice text + Memo string `json:"memo"` // Lightning withdrawal memo } -// LightningWithdrawalResponse response item for holding lightning withdrawal requests. +// LightningWithdrawalResponse response item for holding lightning withdrawal requests type LightningWithdrawalResponse struct { - WithdrawalID string `json:"wdId"` - CreationTime okxUnixMilliTime `json:"cTime"` + WithdrawalID string `json:"wdId"` + CreationTime types.Time `json:"cTime"` } -// WithdrawalHistoryResponse represents the withdrawal response history. +// WithdrawalHistoryResponse represents the withdrawal response history type WithdrawalHistoryResponse struct { - ChainName string `json:"chain"` - WithdrawalFee types.Number `json:"fee"` - Currency string `json:"ccy"` - ClientID string `json:"clientId"` - Amount types.Number `json:"amt"` - TransactionID string `json:"txId"` // Hash record of the withdrawal. This parameter will not be returned for internal transfers. - FromRemittingAddress string `json:"from"` - ToReceivingAddress string `json:"to"` - StateOfWithdrawal string `json:"state"` - Timestamp okxUnixMilliTime `json:"ts"` - WithdrawalID string `json:"wdId"` - PaymentID string `json:"pmtId,omitempty"` - Memo string `json:"memo"` -} - -// SmallAssetConvertResponse represents a response of converting a small asset to OKB. + Currency string `json:"ccy"` + ChainName string `json:"chain"` + NonTradableAsset bool `json:"nonTradableAsset"` + Amount types.Number `json:"amt"` + Timestamp types.Time `json:"ts"` + FromRemittingAddress string `json:"from"` + ToReceivingAddress string `json:"to"` + AreaCodeFrom string `json:"areaCodeFrom"` + AreaCodeTo string `json:"areaCodeTo"` + Tag string `json:"tag"` + WithdrawalFee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` + Memo string `json:"memo"` + AddrEx string `json:"addrEx"` + ClientID string `json:"clientId"` + TransactionID string `json:"txId"` // Hash record of the withdrawal. This parameter will not be returned for internal transfers. + StateOfWithdrawal string `json:"state"` + WithdrawalID string `json:"wdId"` + PaymentID string `json:"pmtId"` +} + +// DepositWithdrawStatus holds deposit withdraw status info +type DepositWithdrawStatus struct { + WithdrawID string `json:"wdId"` + TransactionID string `json:"txId"` + State string `json:"state"` + EstCompleteTime types.Time `json:"estCompleteTime"` +} + +// ExchangeInfo represents exchange information +type ExchangeInfo struct { + ExchID string `json:"exchId"` + ExchangeName string `json:"exchName"` +} + +// SmallAssetConvertResponse represents a response of converting a small asset to OKB type SmallAssetConvertResponse struct { Details []struct { - Amount string `json:"amt"` // Quantity of currency assets before conversion - Currency string `json:"ccy"` // - ConvertAmount string `json:"cnvAmt"` // Quantity of OKB after conversion - ConversionFee string `json:"fee"` // Fee for conversion, unit in OKB + Amount types.Number `json:"amt"` // Quantity of currency assets before conversion + Currency string `json:"ccy"` // + ConvertAmount types.Number `json:"cnvAmt"` // Quantity of OKB after conversion + ConversionFee types.Number `json:"fee"` // Fee for conversion, unit in OKB } `json:"details"` - TotalConvertAmount string `json:"totalCnvAmt"` // Total quantity of OKB after conversion + TotalConvertAmount types.Number `json:"totalCnvAmt"` // Total quantity of OKB after conversion } -// SavingBalanceResponse returns a saving response. +// SavingBalanceResponse holds the response data for a savings balance. type SavingBalanceResponse struct { + Currency string `json:"ccy"` Earnings types.Number `json:"earnings"` RedemptAmount types.Number `json:"redemptAmt"` Rate types.Number `json:"rate"` - Currency string `json:"ccy"` Amount types.Number `json:"amt"` LoanAmount types.Number `json:"loanAmt"` PendingAmount types.Number `json:"pendingAmt"` } -// SavingsPurchaseRedemptionInput input json to SavingPurchase Post merthod. +// SavingsPurchaseRedemptionInput input json to SavingPurchase Post method type SavingsPurchaseRedemptionInput struct { - Currency string `json:"ccy"` // REQUIRED: - Amount float64 `json:"amt,string"` // REQUIRED: purchase or redemption amount - ActionType string `json:"side"` // REQUIRED: action type \"purchase\" or \"redemption\" - Rate float64 `json:"rate,string"` // REQUIRED: + Currency currency.Code `json:"ccy"` // REQUIRED: + Amount float64 `json:"amt,string"` // REQUIRED: purchase or redemption amount + ActionType string `json:"side"` // REQUIRED: action type 'purchase' or 'redemption' + Rate float64 `json:"rate,string"` // REQUIRED: } -// SavingsPurchaseRedemptionResponse response json to SavingPurchase or SavingRedemption Post method. +// SavingsPurchaseRedemptionResponse formats the JSON response for the SavingPurchase or SavingRedemption POST methods type SavingsPurchaseRedemptionResponse struct { Currency string `json:"ccy"` - Amount types.Number `json:"amt"` ActionType string `json:"side"` + Account string `json:"acct"` // '6': Funding account '18': Trading account + Amount types.Number `json:"amt"` Rate types.Number `json:"rate"` } -// LendingRate represents lending rate response +// LendingRate represents the response containing the lending rate. type LendingRate struct { - Currency string `json:"ccy"` - Rate types.Number `json:"rate"` + Currency currency.Code `json:"ccy"` + Rate types.Number `json:"rate"` } // LendingHistory holds lending history responses type LendingHistory struct { - Currency string `json:"ccy"` - Amount types.Number `json:"amt"` - Earnings types.Number `json:"earnings,omitempty"` - Rate types.Number `json:"rate"` - Timestamp okxUnixMilliTime `json:"ts"` + Currency string `json:"ccy"` + Amount types.Number `json:"amt"` + Earnings types.Number `json:"earnings"` + Rate types.Number `json:"rate"` + Timestamp types.Time `json:"ts"` } -// PublicBorrowInfo holds a currency's borrow info. +// PublicBorrowInfo holds a currency's borrow info type PublicBorrowInfo struct { Currency string `json:"ccy"` AverageAmount types.Number `json:"avgAmt"` @@ -1125,251 +1547,323 @@ type PublicBorrowInfo struct { EstimatedRate types.Number `json:"estRate"` } -// PublicBorrowHistory holds a currencies borrow history. +// PublicBorrowHistory holds a currencies borrow history type PublicBorrowHistory struct { - Amount types.Number `json:"amt"` - Currency string `json:"ccy"` - Rate types.Number `json:"rate"` - Timestamp okxUnixMilliTime `json:"ts"` + Amount types.Number `json:"amt"` + Currency string `json:"ccy"` + Rate types.Number `json:"rate"` + Timestamp types.Time `json:"ts"` } -// ConvertCurrency represents currency conversion detailed data. +// ConvertCurrency represents currency conversion detailed data type ConvertCurrency struct { Currency string `json:"currency"` Min types.Number `json:"min"` Max types.Number `json:"max"` } -// ConvertCurrencyPair holds information related to conversion between two pairs. +// ConvertCurrencyPair holds information related to conversion between two pairs type ConvertCurrencyPair struct { InstrumentID string `json:"instId"` BaseCurrency string `json:"baseCcy"` - BaseCurrencyMax types.Number `json:"baseCcyMax,omitempty"` - BaseCurrencyMin types.Number `json:"baseCcyMin,omitempty"` + BaseCurrencyMax types.Number `json:"baseCcyMax"` + BaseCurrencyMin types.Number `json:"baseCcyMin"` QuoteCurrency string `json:"quoteCcy,omitempty"` - QuoteCurrencyMax types.Number `json:"quoteCcyMax,omitempty"` - QuoteCurrencyMin types.Number `json:"quoteCcyMin,omitempty"` + QuoteCurrencyMax types.Number `json:"quoteCcyMax"` + QuoteCurrencyMin types.Number `json:"quoteCcyMin"` } // EstimateQuoteRequestInput represents estimate quote request parameters type EstimateQuoteRequestInput struct { - BaseCurrency string `json:"baseCcy,omitempty"` - QuoteCurrency string `json:"quoteCcy,omitempty"` - Side string `json:"side,omitempty"` - RfqAmount float64 `json:"rfqSz,omitempty"` - RfqSzCurrency string `json:"rfqSzCcy,omitempty"` - ClientRequestOrderID string `json:"clQReqId,string,omitempty"` - Tag string `json:"tag,omitempty"` + BaseCurrency currency.Code `json:"baseCcy,omitempty"` + QuoteCurrency currency.Code `json:"quoteCcy,omitempty"` + Side string `json:"side,omitempty"` + RFQAmount float64 `json:"rfqSz,omitempty"` + RFQSzCurrency string `json:"rfqSzCcy,omitempty"` + ClientRequestOrderID string `json:"clQReqId,string,omitempty"` + Tag string `json:"tag,omitempty"` } -// EstimateQuoteResponse represents estimate quote response data. +// EstimateQuoteResponse represents estimate quote response data type EstimateQuoteResponse struct { - BaseCurrency string `json:"baseCcy"` - BaseSize string `json:"baseSz"` - ClientRequestID string `json:"clQReqId"` - ConvertPrice string `json:"cnvtPx"` - OrigRfqSize string `json:"origRfqSz"` - QuoteCurrency string `json:"quoteCcy"` - QuoteID string `json:"quoteId"` - QuoteSize string `json:"quoteSz"` - QuoteTime okxUnixMilliTime `json:"quoteTime"` - RfqSize string `json:"rfqSz"` - RfqSizeCurrency string `json:"rfqSzCcy"` - Side order.Side `json:"side"` - TTLMs string `json:"ttlMs"` // Validity period of quotation in milliseconds + BaseCurrency string `json:"baseCcy"` + BaseSize types.Number `json:"baseSz"` + ClientRequestID string `json:"clQReqId"` + ConvertPrice types.Number `json:"cnvtPx"` + OrigRFQSize types.Number `json:"origRfqSz"` + QuoteCurrency string `json:"quoteCcy"` + QuoteID string `json:"quoteId"` + QuoteSize types.Number `json:"quoteSz"` + QuoteTime types.Time `json:"quoteTime"` + RFQSize types.Number `json:"rfqSz"` + RFQSizeCurrency string `json:"rfqSzCcy"` + Side order.Side `json:"side"` + TTLMs string `json:"ttlMs"` // Validity period of quotation in milliseconds } // ConvertTradeInput represents convert trade request input type ConvertTradeInput struct { - BaseCurrency string `json:"baseCcy"` - QuoteCurrency string `json:"quoteCcy"` - Side string `json:"side"` - Size float64 `json:"sz,string"` - SizeCurrency string `json:"szCcy"` - QuoteID string `json:"quoteId"` - ClientOrderID string `json:"clTReqId"` - Tag string `json:"tag"` + BaseCurrency string `json:"baseCcy"` + QuoteCurrency string `json:"quoteCcy"` + Side string `json:"side"` + Size float64 `json:"sz,string"` + SizeCurrency currency.Code `json:"szCcy"` + QuoteID string `json:"quoteId"` + ClientOrderID string `json:"clTReqId,omitempty"` + Tag string `json:"tag,omitempty"` } // ConvertTradeResponse represents convert trade response type ConvertTradeResponse struct { - BaseCurrency string `json:"baseCcy"` - ClientOrderID string `json:"clTReqId"` - FillBaseSize types.Number `json:"fillBaseSz"` - FillPrice string `json:"fillPx"` - FillQuoteSize types.Number `json:"fillQuoteSz"` - InstrumentID string `json:"instId"` - QuoteCurrency string `json:"quoteCcy"` - QuoteID string `json:"quoteId"` - Side order.Side `json:"side"` - State string `json:"state"` - TradeID string `json:"tradeId"` - Timestamp okxUnixMilliTime `json:"ts"` + BaseCurrency string `json:"baseCcy"` + ClientOrderID string `json:"clTReqId"` + FillBaseSize types.Number `json:"fillBaseSz"` + FillPrice types.Number `json:"fillPx"` + FillQuoteSize types.Number `json:"fillQuoteSz"` + InstrumentID string `json:"instId"` + QuoteCurrency string `json:"quoteCcy"` + QuoteID string `json:"quoteId"` + Side order.Side `json:"side"` + State string `json:"state"` + TradeID string `json:"tradeId"` + Size types.Number `json:"sz"` + SizeCurrency string `json:"szCcy"` + Timestamp types.Time `json:"ts"` } // ConvertHistory holds convert trade history response type ConvertHistory struct { - InstrumentID string `json:"instId"` - Side order.Side `json:"side"` - FillPrice types.Number `json:"fillPx"` - BaseCurrency string `json:"baseCcy"` - QuoteCurrency string `json:"quoteCcy"` - FillBaseSize types.Number `json:"fillBaseSz"` - State string `json:"state"` - TradeID string `json:"tradeId"` - FillQuoteSize types.Number `json:"fillQuoteSz"` - Timestamp okxUnixMilliTime `json:"ts"` + InstrumentID string `json:"instId"` + Side order.Side `json:"side"` + FillPrice types.Number `json:"fillPx"` + BaseCurrency string `json:"baseCcy"` + QuoteCurrency string `json:"quoteCcy"` + FillBaseSize types.Number `json:"fillBaseSz"` + State string `json:"state"` + TradeID string `json:"tradeId"` + FillQuoteSize types.Number `json:"fillQuoteSz"` + ClientRequestID string `json:"clTReqId"` + Timestamp types.Time `json:"ts"` } // Account holds currency account balance and related information type Account struct { - AdjEq types.Number `json:"adjEq"` - Details []AccountDetail `json:"details"` - Imr types.Number `json:"imr"` // Frozen equity for open positions and pending orders in USD level Applicable to Multi-currency margin and Portfolio margin - IsoEq types.Number `json:"isoEq"` - MgnRatio types.Number `json:"mgnRatio"` - Mmr types.Number `json:"mmr"` // Maintenance margin requirement in USD level Applicable to Multi-currency margin and Portfolio margin - NotionalUsd types.Number `json:"notionalUsd"` - OrdFroz types.Number `json:"ordFroz"` // Margin frozen for pending orders in USD level Applicable to Multi-currency margin and Portfolio margin - TotalEquity types.Number `json:"totalEq"` // Total Equity in USD level - UpdateTime okxUnixMilliTime `json:"uTime"` // UpdateTime -} - -// AccountDetail account detail information. + AdjustedEquity types.Number `json:"adjEq"` + Details []AccountDetail `json:"details"` + InitialMarginRequirement types.Number `json:"imr"` // Frozen equity for open positions and pending orders in USD level Applicable to Multi-currency margin and Portfolio margin + IsolatedMarginEquity types.Number `json:"isoEq"` + MgnRatio types.Number `json:"mgnRatio"` + MaintenanceMarginRequirement types.Number `json:"mmr"` // Maintenance margin requirement in USD level Applicable to Multi-currency margin and Portfolio margin + BorrowFrozen string `json:"borrowFroz"` + NotionalUsd types.Number `json:"notionalUsd"` + OrdFroz types.Number `json:"ordFroz"` // Margin frozen for pending orders in USD level Applicable to Multi-currency margin and Portfolio margin + TotalEquity types.Number `json:"totalEq"` // Total Equity in USD level + UpdateTime types.Time `json:"uTime"` // UpdateTime +} + +// AccountDetail account detail information type AccountDetail struct { - AvailableBalance types.Number `json:"availBal"` - AvailableEquity types.Number `json:"availEq"` - CashBalance types.Number `json:"cashBal"` // Cash Balance - Currency currency.Code `json:"ccy"` - CrossLiab types.Number `json:"crossLiab"` - DiscountEquity types.Number `json:"disEq"` - EquityOfCurrency types.Number `json:"eq"` - EquityUsd types.Number `json:"eqUsd"` - FrozenBalance types.Number `json:"frozenBal"` - Interest types.Number `json:"interest"` - IsoEquity types.Number `json:"isoEq"` - IsolatedLiabilities types.Number `json:"isoLiab"` - IsoUpl types.Number `json:"isoUpl"` // Isolated unrealized profit and loss of the currency applicable to Single-currency margin and Multi-currency margin and Portfolio margin - LiabilitiesOfCurrency types.Number `json:"liab"` - MaxLoan types.Number `json:"maxLoan"` - MarginRatio types.Number `json:"mgnRatio"` // Equity of the currency - NotionalLever types.Number `json:"notionalLever"` // Leverage of the currency applicable to Single-currency margin - OpenOrdersMarginFrozen types.Number `json:"ordFrozen"` - Twap types.Number `json:"twap"` - UpdateTime okxUnixMilliTime `json:"uTime"` - UnrealizedProfit types.Number `json:"upl"` - UnrealizedCurrencyLiabilities types.Number `json:"uplLiab"` - StrategyEquity types.Number `json:"stgyEq"` // strategy equity - TotalEquity types.Number `json:"totalEq"` // Total equity in USD level. Appears unused -} - -// AccountPosition account position. + Currency currency.Code `json:"ccy"` + EquityOfCurrency types.Number `json:"eq"` + CashBalance types.Number `json:"cashBal"` // Cash Balance + UpdateTime types.Time `json:"uTime"` + IsoEquity types.Number `json:"isoEq"` + AvailableEquity types.Number `json:"availEq"` + DiscountEquity types.Number `json:"disEq"` + FixedBalance types.Number `json:"fixedBal"` + AvailableBalance types.Number `json:"availBal"` + MarginFrozenForOpenOrders types.Number `json:"ordFrozen"` + + CrossLiab types.Number `json:"crossLiab"` + EquityUsd types.Number `json:"eqUsd"` + FrozenBalance types.Number `json:"frozenBal"` + Interest types.Number `json:"interest"` + IsolatedLiabilities types.Number `json:"isoLiab"` + IsoUpl types.Number `json:"isoUpl"` // Isolated unrealized profit and loss of the currency applicable to Single-currency margin and Multi-currency margin and Portfolio margin + LiabilitiesOfCurrency types.Number `json:"liab"` + MaxLoan types.Number `json:"maxLoan"` + MarginRatio types.Number `json:"mgnRatio"` // Equity of the currency + NotionalLever types.Number `json:"notionalLever"` // Leverage of the currency applicable to Single-currency margin + Twap types.Number `json:"twap"` + UPL types.Number `json:"upl"` // unrealized profit & loss of all margin and derivatives positions of currency. + UPLLiabilities types.Number `json:"uplLiab"` + StrategyEquity types.Number `json:"stgyEq"` // strategy equity + TotalEquity types.Number `json:"totalEq"` // Total equity in USD level. Appears unused + RewardBalance types.Number `json:"rewardBal"` + InitialMarginRate types.Number `json:"imr"` + MMR types.Number `json:"mmr"` // ross maintenance margin requirement at the currency level. Applicable to Spot and futures mode and when there is cross position + SpotInUseAmount types.Number `json:"spotInUseAmt"` + ClientSpotInUseAmount types.Number `json:"clSpotInUseAmt"` + MaxSpotInUseAmount types.Number `json:"maxSpotInUse"` + SpotIsolatedBalance types.Number `json:"spotIsoBal"` + SmarkSyncEquity types.Number `json:"smtSyncEq"` + SpotCopyTradingEquity types.Number `json:"spotCopyTradingEq"` + SpotBalance types.Number `json:"spotBal"` + OpenAvgPrice types.Number `json:"openAvgPx"` + AccAvgPrice types.Number `json:"accAvgPx"` + SpotUPL types.Number `json:"spotUpl"` + SpotUplRatio types.Number `json:"spotUplRatio"` + TotalPNL types.Number `json:"totalPnl"` + TotalPNLRatio types.Number `json:"totalPnlRatio"` +} + +// AccountPosition account position type AccountPosition struct { - AutoDeleveraging string `json:"adl"` // Auto-deleveraging (ADL) indicator Divided into 5 levels, from 1 to 5, the smaller the number, the weaker the adl intensity. - AvailablePosition string `json:"availPos"` // Position that can be closed Only applicable to MARGIN, FUTURES/SWAP in the long-short mode, OPTION in Simple and isolated OPTION in margin Account. - AveragePrice types.Number `json:"avgPx"` - CreationTime okxUnixMilliTime `json:"cTime"` - Currency currency.Code `json:"ccy"` - DeltaBS string `json:"deltaBS"` // delta:Black-Scholes Greeks in dollars,only applicable to OPTION - DeltaPA string `json:"deltaPA"` // delta:Greeks in coins,only applicable to OPTION - GammaBS string `json:"gammaBS"` // gamma:Black-Scholes Greeks in dollars,only applicable to OPTION - GammaPA string `json:"gammaPA"` // gamma:Greeks in coins,only applicable to OPTION - InitialMarginRequirement types.Number `json:"imr"` // Initial margin requirement, only applicable to cross. - InstrumentID string `json:"instId"` - InstrumentType asset.Item `json:"instType"` - Interest types.Number `json:"interest"` - USDPrice types.Number `json:"usdPx"` - LastTradePrice types.Number `json:"last"` - Leverage types.Number `json:"lever"` // Leverage, not applicable to OPTION seller - Liabilities string `json:"liab"` // Liabilities, only applicable to MARGIN. - LiabilitiesCurrency string `json:"liabCcy"` // Liabilities currency, only applicable to MARGIN. - LiquidationPrice types.Number `json:"liqPx"` // Estimated liquidation price Not applicable to OPTION - MarkPrice types.Number `json:"markPx"` - Margin types.Number `json:"margin"` - MarginMode string `json:"mgnMode"` - MarginRatio types.Number `json:"mgnRatio"` - MaintenanceMarginRequirement types.Number `json:"mmr"` // Maintenance margin requirement in USD level Applicable to Multi-currency margin and Portfolio margin - NotionalUsd types.Number `json:"notionalUsd"` // Quality of Positions -- usd - OptionValue types.Number `json:"optVal"` // Option Value, only application to position. - QuantityOfPosition types.Number `json:"pos"` // Quantity of positions,In the mode of autonomous transfer from position to position, after the deposit is transferred, a position with pos of 0 will be generated - PositionCurrency string `json:"posCcy"` - PositionID string `json:"posId"` - PositionSide string `json:"posSide"` - ThetaBS string `json:"thetaBS"` // theta:Black-Scholes Greeks in dollars,only applicable to OPTION - ThetaPA string `json:"thetaPA"` // theta:Greeks in coins,only applicable to OPTION - TradeID string `json:"tradeId"` - UpdatedTime okxUnixMilliTime `json:"uTime"` // Latest time position was adjusted, - UPNL types.Number `json:"upl"` // Unrealized profit and loss - UPLRatio types.Number `json:"uplRatio"` // Unrealized profit and loss ratio - VegaBS string `json:"vegaBS"` // vega:Black-Scholes Greeks in dollars,only applicable to OPTION - VegaPA string `json:"vegaPA"` // vega:Greeks in coins,only applicable to OPTION + AutoDeleveraging string `json:"adl"` // Auto-deleveraging (ADL) indicator Divided into 5 levels, from 1 to 5, the smaller the number, the weaker the adl intensity. + AvailablePosition string `json:"availPos"` // Position that can be closed Only applicable to MARGIN, FUTURES/SWAP in the long-short mode, OPTION in Simple and isolated OPTION in margin Account. + AveragePrice types.Number `json:"avgPx"` + CreationTime types.Time `json:"cTime"` + Currency currency.Code `json:"ccy"` + DeltaBS string `json:"deltaBS"` // delta:Black-Scholes Greeks in dollars,only applicable to OPTION + DeltaPA string `json:"deltaPA"` // delta:Greeks in coins,only applicable to OPTION + GammaBS string `json:"gammaBS"` // gamma:Black-Scholes Greeks in dollars,only applicable to OPTION + GammaPA string `json:"gammaPA"` // gamma:Greeks in coins,only applicable to OPTION + InitialMarginRequirement types.Number `json:"imr"` // Initial margin requirement, only applicable to cross. + InstrumentID string `json:"instId"` + InstrumentType asset.Item `json:"instType"` + Interest types.Number `json:"interest"` + USDPrice types.Number `json:"usdPx"` + LastTradePrice types.Number `json:"last"` + Leverage types.Number `json:"lever"` // Leverage, not applicable to OPTION seller + Liabilities types.Number `json:"liab"` // Liabilities, only applicable to MARGIN. + LiabilitiesCurrency string `json:"liabCcy"` // Liabilities currency, only applicable to MARGIN. + LiquidationPrice types.Number `json:"liqPx"` // Estimated liquidation price Not applicable to OPTION + MarkPrice types.Number `json:"markPx"` + Margin types.Number `json:"margin"` + MarginMode string `json:"mgnMode"` + MarginRatio types.Number `json:"mgnRatio"` + MaintenanceMarginRequirement types.Number `json:"mmr"` // Maintenance margin requirement in USD level Applicable to Multi-currency margin and Portfolio margin + NotionalUsd types.Number `json:"notionalUsd"` // Quality of Positions -- usd + OptionValue types.Number `json:"optVal"` // Option Value, only application to position. + QuantityOfPosition types.Number `json:"pos"` // Quantity of positions,In the mode of autonomous transfer from position to position, after the deposit is transferred, a position with pos of 0 will be generated + PositionCurrency string `json:"posCcy"` + PositionID string `json:"posId"` + PositionSide string `json:"posSide"` + ThetaBS string `json:"thetaBS"` // theta:Black-Scholes Greeks in dollars,only applicable to OPTION + ThetaPA string `json:"thetaPA"` // theta:Greeks in coins,only applicable to OPTION + TradeID string `json:"tradeId"` + UpdatedTime types.Time `json:"uTime"` // Latest time position was adjusted, + UPNL types.Number `json:"upl"` // Unrealized profit and loss + UPLRatio types.Number `json:"uplRatio"` // Unrealized profit and loss ratio + VegaBS string `json:"vegaBS"` // vega:Black-Scholes Greeks in dollars,only applicable to OPTION + VegaPA string `json:"vegaPA"` // vega:Greeks in coins,only applicable to OPTION // PushTime added feature in the websocket push data. - PushTime okxUnixMilliTime `json:"pTime"` // The time when the account position data is pushed. + PushTime types.Time `json:"pTime"` // The time when the account position data is pushed. } -// AccountPositionHistory hold account position history. +// AccountPositionHistory hold account position history type AccountPositionHistory struct { - CreationTime okxUnixMilliTime `json:"cTime"` - Currency string `json:"ccy"` - CloseAveragePrice types.Number `json:"closeAvgPx,omitempty"` - CloseTotalPosition types.Number `json:"closeTotalPos,omitempty"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Leverage string `json:"lever"` - ManagementMode string `json:"mgnMode"` - OpenAveragePrice string `json:"openAvgPx"` - OpenMaxPosition string `json:"openMaxPos"` - ProfitAndLoss types.Number `json:"pnl,omitempty"` - ProfitAndLossRatio types.Number `json:"pnlRatio,omitempty"` - PositionID string `json:"posId"` - PositionSide string `json:"posSide"` - TriggerPrice string `json:"triggerPx"` - Type string `json:"type"` - UpdateTime okxUnixMilliTime `json:"uTime"` - Underlying string `json:"uly"` -} - -// AccountBalanceData represents currency account balance. + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + ManagementMode string `json:"mgnMode"` + Type string `json:"type"` + CreationTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` + OpenAveragePrice string `json:"openAvgPx"` + CloseAveragePrice types.Number `json:"closeAvgPx"` + + Positions types.Number `json:"pos"` + BaseBalance types.Number `json:"baseBal"` + QuoteBalance types.Number `json:"quoteBal"` + BaseBorrowed types.Number `json:"baseBorrowed"` + BaseInterest types.Number `json:"baseInterest"` + QuoteBorrowed types.Number `json:"quoteBorrowed"` + QuoteInterest types.Number `json:"quoteInterest"` + PositionCurrency string `json:"posCcy"` + AvailablePositions string `json:"availPos"` + AveragePrice types.Number `json:"avgPx"` + MarkPrice types.Number `json:"markPx"` + UPL types.Number `json:"upl"` + UPLRatio types.Number `json:"uplRatio"` + UPLLastPrice types.Number `json:"uplLastPx"` + UPLRatioLastPrice types.Number `json:"uplRatioLastPx"` + Leverage string `json:"lever"` + LiquidiationPrice types.Number `json:"liqPx"` + InitialMarginRequirement types.Number `json:"imr"` + Margin string `json:"margin"` + MarginRatio types.Number `json:"mgnRatio"` + MMR types.Number `json:"mmr"` + Liabilities types.Number `json:"liab"` + LiabilitiesCurrency string `json:"liabCcy"` + Interest types.Number `json:"interest"` + TradeID string `json:"tradeId"` + OptionValue string `json:"optVal"` + PendingCloseOrdLiabVal types.Number `json:"pendingCloseOrdLiabVal"` + NotionalUSD string `json:"notionalUsd"` + ADL string `json:"adl"` + LastTradedPrice types.Number `json:"last"` + IndexPrice types.Number `json:"idxPx"` + USDPrice string `json:"usdPx"` + BreakevenPrice string `json:"bePx"` + DeltaBS string `json:"deltaBS"` + DeltaPA string `json:"deltaPA"` + GammaBS string `json:"gammaBS"` + ThetaBS string `json:"thetaBS"` + ThetaPA string `json:"thetaPA"` + VegaBS string `json:"vegaBS"` + VegaPA string `json:"vegaPA"` + SpotInUseAmount types.Number `json:"spotInUseAmt"` + SpotInUseCurrency string `json:"spotInUseCcy"` + ClientSpotInUseAmount types.Number `json:"clSpotInUseAmt"` + BizRefID string `json:"bizRefId"` + BizRefType string `json:"bizRefType"` + ProfitAndLoss types.Number `json:"pnl"` + Fee types.Number `json:"fee"` + LiqPenalty types.Number `json:"liqPenalty"` + CloseOrderAlgo types.Number `json:"closeOrderAlgo"` + + Currency string `json:"ccy"` + CloseTotalPosition types.Number `json:"closeTotalPos"` + OpenMaxPosition types.Number `json:"openMaxPos"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + PositionID string `json:"posId"` + PositionSide string `json:"posSide"` + TriggerPrice types.Number `json:"triggerPx"` + Underlying string `json:"uly"` +} + +// AccountBalanceData represents currency account balance type AccountBalanceData struct { - Currency string `json:"ccy"` - DiscountEquity string `json:"disEq"` // discount equity of the currency in USD level. - Equity string `json:"eq"` // Equity of the currency + Currency string `json:"ccy"` + DiscountEquity types.Number `json:"disEq"` // discount equity of the currency in USD level. + Equity types.Number `json:"eq"` // Equity of the currency } -// PositionData holds account position data. +// PositionData holds account position data type PositionData struct { - BaseBal string `json:"baseBal"` - Currency string `json:"ccy"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - ManagementMode string `json:"mgnMode"` - NotionalCurrency string `json:"notionalCcy"` - NotionalUsd string `json:"notionalUsd"` - Position string `json:"pos"` - PositionedCcy string `json:"posCcy"` - PositionedID string `json:"posId"` - PositionedSide string `json:"posSide"` - QuoteBalance string `json:"quoteBal"` -} - -// AccountAndPositionRisk holds information. + BaseBalance types.Number `json:"baseBal"` + Currency string `json:"ccy"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + ManagementMode string `json:"mgnMode"` + NotionalCurrency string `json:"notionalCcy"` + NotionalUSD types.Number `json:"notionalUsd"` + Position string `json:"pos"` + PositionedCurrency string `json:"posCcy"` + PositionedID string `json:"posId"` + PositionedSide string `json:"posSide"` + QuoteBalance types.Number `json:"quoteBal"` +} + +// AccountAndPositionRisk holds information type AccountAndPositionRisk struct { AdjEq string `json:"adjEq"` AccountBalanceData []AccountBalanceData `json:"balData"` PosData []PositionData `json:"posData"` - Timestamp okxUnixMilliTime `json:"ts"` + Timestamp types.Time `json:"ts"` } // BillsDetailQueryParameter represents bills detail query parameter type BillsDetailQueryParameter struct { InstrumentType string // Instrument type "SPOT" "MARGIN" "SWAP" "FUTURES" "OPTION" - Currency string + InstrumentID string + Currency currency.Code MarginMode string // Margin mode "isolated" "cross" ContractType string // Contract type "linear" & "inverse" Only applicable to FUTURES/SWAP - BillType uint // Bill type 1: Transfer 2: Trade 3: Delivery 4: Auto token conversion 5: Liquidation 6: Margin transfer 7: Interest deduction 8: Funding fee 9: ADL 10: Clawback 11: System token conversion 12: Strategy transfer 13: ddh - BillSubType int // allowed bill subtype values are [ 1,2,3,4,5,6,9,11,12,14,160,161,162,110,111,118,119,100,101,102,103,104,105,106,110,125,126,127,128,131,132,170,171,172,112,113,117,173,174,200,201,202,203 ], link: https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days + BillType uint64 // Bill type 1: Transfer 2: Trade 3: Delivery 4: Auto token conversion 5: Liquidation 6: Margin transfer 7: Interest deduction 8: Funding fee 9: ADL 10: Clawback 11: System token conversion 12: Strategy transfer 13: ddh + BillSubType int64 // allowed bill subtype values are [ 1,2,3,4,5,6,9,11,12,14,160,161,162,110,111,118,119,100,101,102,103,104,105,106,110,125,126,127,128,131,132,170,171,172,112,113,117,173,174,200,201,202,203 ], link: https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days After string Before string BeginTime time.Time @@ -1377,41 +1871,82 @@ type BillsDetailQueryParameter struct { Limit int64 } -// BillsDetailResponse represents account bills information. +// BillsDetailResp represents response for applying for bill-details +type BillsDetailResp struct { + Result string `json:"result"` + Timestamp types.Time `json:"ts"` +} + +// BillsArchiveInfo represents a bill archive information +type BillsArchiveInfo struct { + FileHref string `json:"fileHref"` + State string `json:"state"` + Timestamp types.Time `json:"ts"` +} + +// BillsDetailResponse represents account bills information type BillsDetailResponse struct { - Balance types.Number `json:"bal"` - BalanceChange string `json:"balChg"` - BillID string `json:"billId"` - Currency string `json:"ccy"` - ExecType string `json:"execType"` // Order flow type, T:taker M:maker - Fee types.Number `json:"fee"` // Fee Negative number represents the user transaction fee charged by the platform. Positive number represents rebate. - From string `json:"from"` // The remitting account 6: FUNDING 18: Trading account When bill type is not transfer, the field returns "". - InstrumentID string `json:"instId"` - InstrumentType asset.Item `json:"instType"` - MarginMode string `json:"mgnMode"` - Notes string `json:"notes"` // notes When bill type is not transfer, the field returns "". - OrderID string `json:"ordId"` - ProfitAndLoss types.Number `json:"pnl"` - PositionLevelBalance types.Number `json:"posBal"` - PositionLevelBalanceChange types.Number `json:"posBalChg"` - SubType string `json:"subType"` - Size types.Number `json:"sz"` - To string `json:"to"` - Timestamp okxUnixMilliTime `json:"ts"` - Type string `json:"type"` -} - -// AccountConfigurationResponse represents account configuration response. + Balance types.Number `json:"bal"` + BalanceChange types.Number `json:"balChg"` + BillID string `json:"billId"` + Currency string `json:"ccy"` + ExecType string `json:"execType"` // Order flow type, T:taker M:maker + Fee types.Number `json:"fee"` // Fee Negative number represents the user transaction fee charged by the platform. Positive number represents rebate. + From string `json:"from"` // The remitting account 6: FUNDING 18: Trading account When bill type is not transfer, the field returns "". + InstrumentID string `json:"instId"` + InstrumentType asset.Item `json:"instType"` + MarginMode string `json:"mgnMode"` + Notes string `json:"notes"` // notes When bill type is not transfer, the field returns "". + OrderID string `json:"ordId"` + ProfitAndLoss types.Number `json:"pnl"` + PositionLevelBalance types.Number `json:"posBal"` + PositionLevelBalanceChange types.Number `json:"posBalChg"` + SubType string `json:"subType"` + Price types.Number `json:"px"` + Interest types.Number `json:"interest"` + Tag string `json:"tag"` + FillTime types.Time `json:"fillTime"` + TradeID string `json:"tradeId"` + ClientOrdID string `json:"clOrdId"` + FillIdxPrice types.Number `json:"fillIdxPx"` + FillMarkPrice types.Number `json:"fillMarkPx"` + FillPxVolume types.Number `json:"fillPxVol"` + FillPxUSD types.Number `json:"fillPxUsd"` + FillMarkVolume types.Number `json:"fillMarkVol"` + FillFwdPrice types.Number `json:"fillFwdPx"` + Size types.Number `json:"sz"` + To string `json:"to"` + Timestamp types.Time `json:"ts"` + Type string `json:"type"` +} + +// AccountConfigurationResponse represents account configuration response type AccountConfigurationResponse struct { - AccountLevel uint `json:"acctLv,string"` // 1: Simple 2: Single-currency margin 3: Multi-currency margin 4:Portfolio margin - AutoLoan bool `json:"autoLoan"` // Whether to borrow coins automatically true: borrow coins automatically false: not borrow coins automatically - ContractIsolatedMode string `json:"ctIsoMode"` // Contract isolated margin trading settings automatic:Auto transfers autonomy:Manual transfers - GreeksType string `json:"greeksType"` // Current display type of Greeks PA: Greeks in coins BS: Black-Scholes Greeks in dollars - Level string `json:"level"` // The user level of the current real trading volume on the platform, e.g lv1 - LevelTemporary string `json:"levelTmp"` - MarginIsolatedMode string `json:"mgnIsoMode"` // Margin isolated margin trading settings automatic:Auto transfers autonomy:Manual transfers - PositionMode string `json:"posMode"` - AccountID string `json:"uid"` + UID string `json:"uid"` + MainUID string `json:"mainUid"` + AccountSelfTradePreventionMode string `json:"acctStpMode"` + AccountLevel string `json:"acctLv"` // 1: Simple 2: Single-currency margin 3: Multi-currency margin 4:Portfolio margin + AutoLoan bool `json:"autoLoan"` // Whether to borrow coins automatically true: borrow coins automatically false: not borrow coins automatically + ContractIsolatedMode string `json:"ctIsoMode"` // Contract isolated margin trading settings automatic:Auto transfers autonomy:Manual transfers + GreeksType string `json:"greeksType"` // Current display type of Greeks PA: Greeks in coins BS: Black-Scholes Greeks in dollars + Level types.Number `json:"level"` // The user level of the current real trading volume on the platform, e.g lv1 + LevelTemporary string `json:"levelTmp"` + MarginIsolatedMode string `json:"mgnIsoMode"` // Margin isolated margin trading settings automatic:Auto transfers autonomy:Manual transfers + PositionMode string `json:"posMode"` + SpotOffsetType string `json:"spotOffsetType"` + RoleType string `json:"roleType"` + TraderInsts string `json:"traderInsts"` + SpotRoleType string `json:"spotRoleType"` + SpotTraderInsts string `json:"spotTraderInsts"` + OptionalTradingAuth string `json:"opAuth"` // Whether the optional trading was activated 0: not activate 1: activated + KYCLevel string `json:"kycLv"` + Label string `json:"label"` + IP string `json:"ip"` + Permission string `json:"perm"` + DiscountType string `json:"discountType"` + LiquidationGear string `json:"liquidationGear"` + EnableSpotBorrow bool `json:"enableSpotBorrow"` + SpotBorrowAutoRepay bool `json:"spotBorrowAutoRepay"` } // PositionMode represents position mode response @@ -1421,11 +1956,13 @@ type PositionMode struct { // SetLeverageInput represents set leverage request input type SetLeverageInput struct { - Leverage float64 `json:"lever,string"` // set leverage for isolated - MarginMode string `json:"mgnMode"` // Margin Mode "cross" and "isolated" - InstrumentID string `json:"instId,omitempty"` // Optional: - Currency string `json:"ccy,omitempty"` // Optional: - PositionSide string `json:"posSide,omitempty"` + Leverage float64 `json:"lever,string"` // set leverage for isolated + MarginMode string `json:"mgnMode"` // Margin Mode "cross" and "isolated" + InstrumentID string `json:"instId,omitempty"` // Optional: + Currency currency.Code `json:"ccy,omitempty"` // Optional: + PositionSide string `json:"posSide,omitempty"` + + AssetType asset.Item `json:"-"` } // SetLeverageResponse represents set leverage response @@ -1438,10 +1975,10 @@ type SetLeverageResponse struct { // MaximumBuyAndSell get maximum buy , sell amount or open amount type MaximumBuyAndSell struct { - Currency string `json:"ccy"` - InstrumentID string `json:"instId"` - MaximumBuy string `json:"maxBuy"` - MaximumSell string `json:"maxSell"` + Currency string `json:"ccy"` + InstrumentID string `json:"instId"` + MaximumBuy types.Number `json:"maxBuy"` + MaximumSell types.Number `json:"maxSell"` } // MaximumTradableAmount represents get maximum tradable amount response @@ -1451,74 +1988,130 @@ type MaximumTradableAmount struct { AvailSell string `json:"availSell"` } -// IncreaseDecreaseMarginInput represents increase or decrease the margin of the isolated position. +// IncreaseDecreaseMarginInput represents increase or decrease the margin of the isolated position type IncreaseDecreaseMarginInput struct { - InstrumentID string `json:"instId"` - PositionSide string `json:"posSide"` - Type string `json:"type"` - Amount float64 `json:"amt,string"` - Currency string `json:"ccy"` - AutoLoadTransfer bool `json:"auto"` - LoadTransfer bool `json:"loanTrans"` + InstrumentID string `json:"instId"` + PositionSide string `json:"posSide"` + MarginBalanceType string `json:"type"` + Amount float64 `json:"amt,string"` + Currency string `json:"ccy"` } // IncreaseDecreaseMargin represents increase or decrease the margin of the isolated position response type IncreaseDecreaseMargin struct { Amount types.Number `json:"amt"` - Ccy string `json:"ccy"` + Currency string `json:"ccy"` InstrumentID string `json:"instId"` Leverage types.Number `json:"leverage"` - PosSide string `json:"posSide"` + PositionSide string `json:"posSide"` Type string `json:"type"` } -// LeverageResponse instrument id leverage response. +// LeverageResponse instrument ID leverage response type LeverageResponse struct { InstrumentID string `json:"instId"` MarginMode string `json:"mgnMode"` PositionSide string `json:"posSide"` Leverage types.Number `json:"lever"` + Currency string `json:"ccy"` +} + +// LeverageEstimatedInfo leverage estimated info response +type LeverageEstimatedInfo struct { + EstimatedAvailQuoteTrans string `json:"estAvailQuoteTrans"` + EstimatedAvailTrans string `json:"estAvailTrans"` + EstimatedLiqPrice types.Number `json:"estLiqPx"` + EstimatedMaxAmount types.Number `json:"estMaxAmt"` + EstimatedMargin types.Number `json:"estMgn"` + EstimatedQuoteMaxAmount types.Number `json:"estQuoteMaxAmt"` + EstimatedQuoteMargin types.Number `json:"estQuoteMgn"` + ExistOrd bool `json:"existOrd"` // Whether there is pending orders + MaxLeverage types.Number `json:"maxLever"` + MinLeverage types.Number `json:"minLever"` } -// MaximumLoanInstrument represents maximum loan of an instrument id. +// MaximumLoanInstrument represents maximum loan of an instrument ID type MaximumLoanInstrument struct { - InstrumentID string `json:"instId"` - MgnMode string `json:"mgnMode"` - MgnCcy string `json:"mgnCcy"` - MaxLoan string `json:"maxLoan"` - Ccy string `json:"ccy"` - Side order.Side `json:"side"` + InstrumentID string `json:"instId"` + MarginMode string `json:"mgnMode"` + MarginCurrency string `json:"mgnCcy"` + MaxLoan types.Number `json:"maxLoan"` + Currency string `json:"ccy"` + Side order.Side `json:"side"` } -// TradeFeeRate holds trade fee rate information for a given instrument type. +// TradeFeeRate holds trade fee rate information for a given instrument type type TradeFeeRate struct { - Category string `json:"category"` - DeliveryFeeRate string `json:"delivery"` - Exercise string `json:"exercise"` - InstrumentType asset.Item `json:"instType"` - FeeRateLevel string `json:"level"` - FeeRateMaker types.Number `json:"maker"` - FeeRateMakerUSDT types.Number `json:"makerU"` - FeeRateMakerUSDC types.Number `json:"makerUSDC"` - FeeRateTaker types.Number `json:"taker"` - FeeRateTakerUSDT types.Number `json:"takerU"` - FeeRateTakerUSDC types.Number `json:"takerUSDC"` - Timestamp okxUnixMilliTime `json:"ts"` + Category string `json:"category"` + DeliveryFeeRate types.Number `json:"delivery"` + Exercise string `json:"exercise"` + InstrumentType asset.Item `json:"instType"` + FeeRateLevel string `json:"level"` + FeeRateMaker types.Number `json:"maker"` + FeeRateTaker types.Number `json:"taker"` + Timestamp types.Time `json:"ts"` + FeeRateMakerUSDT types.Number `json:"makerU"` + FeeRateTakerUSDT types.Number `json:"takerU"` + FeeRateMakerUSDC types.Number `json:"makerUSDC"` + FeeRateTakerUSDC types.Number `json:"takerUSDC"` + RuleType string `json:"ruleType"` + Fiat []FiatItemInfo `json:"fiat"` +} + +// FiatItemInfo represents fiat currency with taker and maker fee details +type FiatItemInfo struct { + Currency string `json:"ccy"` + Taker types.Number `json:"taker"` + Maker types.Number `json:"maker"` } // InterestAccruedData represents interest rate accrued response type InterestAccruedData struct { - Currency string `json:"ccy"` - InstrumentID string `json:"instId"` - Interest string `json:"interest"` - InterestRate string `json:"interestRate"` // Interest rate in an hour. - Liability string `json:"liab"` - MarginMode string `json:"mgnMode"` // Margin mode "cross" "isolated" - Timestamp okxUnixMilliTime `json:"ts"` - LoanType string `json:"type"` + Currency string `json:"ccy"` + InstrumentID string `json:"instId"` + Interest types.Number `json:"interest"` + InterestRate types.Number `json:"interestRate"` // Interest rate in an hour. + Liability types.Number `json:"liab"` + MarginMode string `json:"mgnMode"` // Margin mode "cross" "isolated" + Timestamp types.Time `json:"ts"` + LoanType string `json:"type"` +} + +// VIPInterestData holds interest accrued/deducted data +type VIPInterestData struct { + OrderID string `json:"ordId"` + Currency string `json:"ccy"` + Interest types.Number `json:"interest"` + InterestRate types.Number `json:"interestRate"` + Liability types.Number `json:"liab"` + Timestamp types.Time `json:"ts"` +} + +// VIPLoanOrder holds VIP loan items +type VIPLoanOrder struct { + OrderID string `json:"ordId"` + Currency string `json:"ccy"` + State string `json:"state"` + BorrowAmount types.Number `json:"borrowAmt"` + CurrentRate types.Number `json:"curRate"` + DueAmount types.Number `json:"dueAmt"` + NextRefreshTime types.Time `json:"nextRefreshTime"` + OriginalRate types.Number `json:"origRate"` + RepayAmount types.Number `json:"repayAmt"` + Timestamp types.Time `json:"ts"` +} + +// VIPLoanOrderDetail holds vip loan order detail +type VIPLoanOrderDetail struct { + Amount types.Number `json:"amt"` + Currency string `json:"ccy"` + FailReason string `json:"failReason"` + Rate types.Number `json:"rate"` + Timestamp types.Time `json:"ts"` + Type string `json:"type"` // Operation Type: 1:Borrow 2:Repayment 3:System Repayment 4:Interest Rate Refresh } -// InterestRateResponse represents interest rate response. +// InterestRateResponse represents interest rate response type InterestRateResponse struct { InterestRate types.Number `json:"interestRate"` Currency string `json:"ccy"` @@ -1535,83 +2128,142 @@ type IsolatedMode struct { InstrumentType string `json:"type"` // Instrument type "MARGIN" "CONTRACTS" } -// MaximumWithdrawal represents maximum withdrawal amount query response. +// BorrowAndRepay manual holds manual borrow and repay in quick margin mode +type BorrowAndRepay struct { + Amount float64 `json:"amt,string"` + InstrumentID string `json:"instId"` + LoanCcy currency.Code `json:"ccy"` + Side string `json:"side"` // possible values: 'borrow' and 'repay' +} + +// BorrowRepayHistoryItem holds borrow or repay history item information +type BorrowRepayHistoryItem struct { + InstrumentID string `json:"instId"` + Currency string `json:"ccy"` + Side string `json:"side"` + AccBorrowAmount types.Number `json:"accBorrowed"` + Amount types.Number `json:"amt"` + RefID string `json:"refId"` + Timestamp types.Time `json:"ts"` +} + +// MaximumWithdrawal represents maximum withdrawal amount query response type MaximumWithdrawal struct { - Currency string `json:"ccy"` - MaximumWithdrawal string `json:"maxWd"` // Max withdrawal (not allowing borrowed crypto transfer out under Multi-currency margin) - MaximumWithdrawalEx string `json:"maxWdEx"` // Max withdrawal (allowing borrowed crypto transfer out under Multi-currency margin) + Currency string `json:"ccy"` + MaximumWithdrawal types.Number `json:"maxWd"` // Max withdrawal (not allowing borrowed crypto transfer out under Multi-currency margin) + MaximumWithdrawalEx types.Number `json:"maxWdEx"` // Max withdrawal (allowing borrowed crypto transfer out under Multi-currency margin) + SpotOffsetMaxWithdrawal types.Number `json:"spotOffsetMaxWd"` + SpotOffsetMaxWdEx types.Number `json:"spotOffsetMaxWdEx"` } -// AccountRiskState represents account risk state. +// AccountRiskState represents account risk state type AccountRiskState struct { - IsTheAccountAtRisk bool `json:"atRisk"` - AtRiskIdx []interface{} `json:"atRiskIdx"` // derivatives risk unit list - AtRiskMgn []interface{} `json:"atRiskMgn"` // margin risk unit list - Timestamp okxUnixMilliTime `json:"ts"` + IsTheAccountAtRisk string `json:"atRisk"` + AtRiskIdx []interface{} `json:"atRiskIdx"` // derivatives risk unit list + AtRiskMgn []interface{} `json:"atRiskMgn"` // margin risk unit list + Timestamp types.Time `json:"ts"` } -// LoanBorrowAndReplayInput represents currency VIP borrow or repay request params. +// LoanBorrowAndReplayInput represents currency VIP borrow or repay request params type LoanBorrowAndReplayInput struct { - Currency string `json:"ccy"` - Side string `json:"side,omitempty"` - Amount types.Number `json:"amt,omitempty"` + Currency currency.Code `json:"ccy"` + Side string `json:"side,omitempty"` + Amount float64 `json:"amt,string,omitempty"` } // LoanBorrowAndReplay loans borrow and repay type LoanBorrowAndReplay struct { - Amount string `json:"amt"` - AvailableLoan string `json:"availLoan"` - Currency string `json:"ccy"` - LoanQuota string `json:"loanQuota"` - PosLoan string `json:"posLoan"` - Side string `json:"side"` // borrow or repay - UsedLoan string `json:"usedLoan"` + Amount types.Number `json:"amt"` + AvailableLoan types.Number `json:"availLoan"` + Currency string `json:"ccy"` + LoanQuota types.Number `json:"loanQuota"` + PosLoan string `json:"posLoan"` + Side string `json:"side"` // borrow or repay + UsedLoan string `json:"usedLoan"` } // BorrowRepayHistory represents borrow and repay history item data type BorrowRepayHistory struct { - Currency string `json:"ccy"` - TradedLoan string `json:"tradedLoan"` - Timestamp okxUnixMilliTime `json:"ts"` - Type string `json:"type"` - UsedLoan string `json:"usedLoan"` + Currency string `json:"ccy"` + TradedLoan string `json:"tradedLoan"` + Timestamp types.Time `json:"ts"` + Type string `json:"type"` + UsedLoan string `json:"usedLoan"` } -// BorrowInterestAndLimitResponse represents borrow interest and limit rate for different loan type. +// BorrowInterestAndLimitResponse represents borrow interest and limit rate for different loan type type BorrowInterestAndLimitResponse struct { - Debt string `json:"debt"` - Interest string `json:"interest"` - NextDiscountTime okxUnixMilliTime `json:"nextDiscountTime"` - NextInterestTime okxUnixMilliTime `json:"nextInterestTime"` + Debt string `json:"debt"` + Interest string `json:"interest"` + NextDiscountTime types.Time `json:"nextDiscountTime"` + NextInterestTime types.Time `json:"nextInterestTime"` + LoanAllocation types.Number `json:"loanAlloc"` Records []struct { - AvailLoan string `json:"availLoan"` - Currency string `json:"ccy"` - Interest string `json:"interest"` - LoanQuota string `json:"loanQuota"` - PosLoan string `json:"posLoan"` // Frozen amount for current account Only applicable to VIP loans - Rate string `json:"rate"` - SurplusLmt string `json:"surplusLmt"` - UsedLmt string `json:"usedLmt"` - UsedLoan string `json:"usedLoan"` + AvailLoan types.Number `json:"availLoan"` + Currency string `json:"ccy"` + Interest types.Number `json:"interest"` + LoanQuota types.Number `json:"loanQuota"` + PosLoan types.Number `json:"posLoan"` // Frozen amount for current account Only applicable to VIP loans + Rate types.Number `json:"rate"` + SurplusLimit types.Number `json:"surplusLmt"` + SurplusLimitDetails SurplusLimitDetail `json:"surplusLmtDetails"` + UsedLmt types.Number `json:"usedLmt"` + UsedLoan types.Number `json:"usedLoan"` } `json:"records"` } -// PositionItem represents current position of the user. +// SurplusLimitDetail represents details of available amount across all sub-accounts. The value of surplusLmt is the minimum value within this array +type SurplusLimitDetail struct { + AllAcctRemainingQuota string `json:"allAcctRemainingQuota"` + CurAcctRemainingQuota string `json:"curAcctRemainingQuota"` + PlatRemainingQuota string `json:"platRemainingQuota"` +} + +// FixedLoanBorrowLimitInformation represents a fixed loan borrow information +type FixedLoanBorrowLimitInformation struct { + TotalBorrowLimit types.Number `json:"totalBorrowLmt"` + TotalAvailableBorrow types.Number `json:"totalAvailBorrow"` + Borrowed types.Number `json:"borrowed"` + UsedAmount types.Number `json:"used"` + AvailRepay string `json:"availRepay"` + Details []struct { + Borrowed types.Number `json:"borrowed"` + AvailBorrow types.Number `json:"availBorrow"` + Currency string `json:"ccy"` + MinBorrow types.Number `json:"minBorrow"` + Used types.Number `json:"used"` + Term string `json:"term"` + } `json:"details"` + Timestamp types.Time `json:"ts"` +} + +// FixedLoanBorrowQuote represents a fixed loan quote details +type FixedLoanBorrowQuote struct { + Currency string `json:"ccy"` + Term string `json:"term"` + EstAvailBorrow types.Number `json:"estAvailBorrow"` + EstRate types.Number `json:"estRate"` + EstInterest types.Number `json:"estInterest"` + PenaltyInterest types.Number `json:"penaltyInterest"` + Timestamp types.Time `json:"ts"` +} + +// PositionItem represents current position of the user type PositionItem struct { Position string `json:"pos"` InstrumentID string `json:"instId"` } -// PositionBuilderInput represents request parameter for position builder item. +// PositionBuilderInput represents request parameter for position builder item type PositionBuilderInput struct { InstrumentType string `json:"instType,omitempty"` InstrumentID string `json:"instId,omitempty"` ImportExistingPosition bool `json:"inclRealPos,omitempty"` // "true":Import existing positions and hedge with simulated ones "false":Only use simulated positions The default is true ListOfPositions []PositionItem `json:"simPos,omitempty"` - PositionsCount uint `json:"pos,omitempty"` + PositionsCount uint64 `json:"pos,omitempty"` } -// PositionBuilderResponse represents a position builder endpoint response. +// PositionBuilderResponse represents a position builder endpoint response type PositionBuilderResponse struct { InitialMarginRequirement string `json:"imr"` // Initial margin requirement of riskUnit dimension MaintenanceMarginRequirement string `json:"mmr"` // Maintenance margin requirement of riskUnit dimension @@ -1624,33 +2276,33 @@ type PositionBuilderResponse struct { TransactionCostAndSlippage string `json:"mr7"` PositionData []PositionBuilderData `json:"posData"` // List of positions RiskUnit string `json:"riskUnit"` - Timestamp okxUnixMilliTime `json:"ts"` + Timestamp types.Time `json:"ts"` } -// PositionBuilderData represent a position item. +// PositionBuilderData represent a position item type PositionBuilderData struct { - Delta string `json:"delta"` - Gamma string `json:"gamma"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - NotionalUsd string `json:"notionalUsd"` // Quantity of positions usd - QuantityOfPosition string `json:"pos"` // Quantity of positions - Theta string `json:"theta"` // Sensitivity of option price to remaining maturity - Vega string `json:"vega"` // Sensitivity of option price to implied volatility + Delta string `json:"delta"` + Gamma string `json:"gamma"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + NotionalUSD types.Number `json:"notionalUsd"` // Quantity of positions usd + QuantityOfPosition types.Number `json:"pos"` // Quantity of positions + Theta string `json:"theta"` // Sensitivity of option price to remaining maturity + Vega string `json:"vega"` // Sensitivity of option price to implied volatility } // GreeksItem represents greeks response type GreeksItem struct { - ThetaBS string `json:"thetaBS"` - ThetaPA string `json:"thetaPA"` - DeltaBS string `json:"deltaBS"` - DeltaPA string `json:"deltaPA"` - GammaBS string `json:"gammaBS"` - GammaPA string `json:"gammaPA"` - VegaBS string `json:"vegaBS"` - VegaPA string `json:"vegaPA"` - Currency string `json:"ccy"` - Timestamp okxUnixMilliTime `json:"ts"` + ThetaBS string `json:"thetaBS"` + ThetaPA string `json:"thetaPA"` + DeltaBS string `json:"deltaBS"` + DeltaPA string `json:"deltaPA"` + GammaBS string `json:"gammaBS"` + GammaPA string `json:"gammaPA"` + VegaBS string `json:"vegaBS"` + VegaPA string `json:"vegaPA"` + Currency string `json:"ccy"` + Timestamp types.Time `json:"ts"` } // CounterpartiesResponse represents @@ -1660,73 +2312,103 @@ type CounterpartiesResponse struct { Type string `json:"type"` } -// RfqOrderLeg represents Rfq Order responses leg. -type RfqOrderLeg struct { - Size string `json:"sz"` - Side string `json:"side"` - InstrumentID string `json:"instId"` - TgtCurrency string `json:"tgtCcy,omitempty"` +// RFQOrderLeg represents RFQ Order responses leg +type RFQOrderLeg struct { + Size types.Number `json:"sz"` + Side string `json:"side"` + InstrumentID string `json:"instId"` + TgtCurrency string `json:"tgtCcy,omitempty"` } -// CreateRfqInput Rfq create method input. -type CreateRfqInput struct { +// CreateRFQInput RFQ create method input +type CreateRFQInput struct { Anonymous bool `json:"anonymous"` CounterParties []string `json:"counterparties"` - ClientRfqID string `json:"clRfqId"` - Legs []RfqOrderLeg `json:"legs"` + ClientRFQID string `json:"clRfqId"` + Legs []RFQOrderLeg `json:"legs"` } -// CancelRfqRequestParam represents cancel Rfq order request params -type CancelRfqRequestParam struct { - RfqID string `json:"rfqId"` - ClientRfqID string `json:"clRfqId"` +// CancelRFQRequestParam represents cancel RFQ order request params +type CancelRFQRequestParam struct { + RFQID string `json:"rfqId,omitempty"` + ClientRFQID string `json:"clRfqId,omitempty"` } -// CancelRfqRequestsParam represents cancel multiple Rfq orders request params -type CancelRfqRequestsParam struct { - RfqIDs []string `json:"rfqIds"` - ClientRfqIDs []string `json:"clRfqIds"` +// CancelRFQRequestsParam represents cancel multiple RFQ orders request params +type CancelRFQRequestsParam struct { + RFQIDs []string `json:"rfqIds"` + ClientRFQIDs []string `json:"clRfqIds"` } -// CancelRfqResponse represents cancel Rfq orders response -type CancelRfqResponse struct { - RfqID string `json:"rfqId"` - ClientRfqID string `json:"clRfqId"` - StatusCode string `json:"sCode"` - StatusMsg string `json:"sMsg"` +// CancelRFQResponse represents cancel RFQ orders response +type CancelRFQResponse struct { + RFQID string `json:"rfqId"` + ClientRFQID string `json:"clRfqId"` + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` } -// TimestampResponse holds timestamp response only. -type TimestampResponse struct { - Timestamp okxUnixMilliTime `json:"ts"` +// MMPStatusResponse holds MMP reset status response +type MMPStatusResponse struct { + Result bool `json:"result"` +} + +// MMPConfig holds request/response structure to set Market Maker Protection (MMP) +type MMPConfig struct { + InstrumentFamily string `json:"instFamily"` + TimeInterval int64 `json:"timeInterval,string"` + FrozenInterval int64 `json:"frozenInterval,string"` // Frozen period (ms). "0" means the trade will remain frozen until you request "Reset MMP Status" to unfrozen + QuantityLimit float64 `json:"qtyLimit,string"` +} + +// MMPConfigDetail holds MMP config details +type MMPConfigDetail struct { + FrozenInterval types.Number `json:"frozenInterval"` + InstrumentFamily string `json:"instFamily"` + MMPFrozen bool `json:"mmpFrozen"` + MMPFrozenUntil string `json:"mmpFrozenUntil"` + QuantityLimit types.Number `json:"qtyLimit"` + TimeInterval int64 `json:"timeInterval"` } // ExecuteQuoteParams represents Execute quote request params type ExecuteQuoteParams struct { - RfqID string `json:"rfqId"` - QuoteID string `json:"quoteId"` + RFQID string `json:"rfqId,omitempty"` + QuoteID string `json:"quoteId,omitempty"` } -// ExecuteQuoteResponse represents execute quote response. +// ExecuteQuoteResponse represents execute quote response type ExecuteQuoteResponse struct { - BlockTradedID string `json:"blockTdId"` - RfqID string `json:"rfqId"` - ClientRfqID string `json:"clRfqId"` - QuoteID string `json:"quoteId"` - ClientQuoteID string `json:"clQuoteId"` - TraderCode string `json:"tTraderCode"` - MakerTraderCode string `json:"mTraderCode"` - CreationTime okxUnixMilliTime `json:"cTime"` - Legs []OrderLeg `json:"legs"` -} - -// OrderLeg represents legs information for both websocket and REST available Quote information. + BlockTradedID string `json:"blockTdId"` + RFQID string `json:"rfqId"` + ClientRFQID string `json:"clRfqId"` + QuoteID string `json:"quoteId"` + ClientQuoteID string `json:"clQuoteId"` + TraderCode string `json:"tTraderCode"` + MakerTraderCode string `json:"mTraderCode"` + CreationTime types.Time `json:"cTime"` + Legs []OrderLeg `json:"legs"` +} + +// QuoteProduct represents products which makers want to quote and receive RFQs for +type QuoteProduct struct { + InstrumentType string `json:"instType,omitempty"` + IncludeALL bool `json:"includeALL"` + Data []struct { + Underlying string `json:"uly"` + MaxBlockSize types.Number `json:"maxBlockSz"` + MakerPriceBand types.Number `json:"makerPxBand"` + } `json:"data"` + InstrumentType0 string `json:"instType:,omitempty"` +} + +// OrderLeg represents legs information for both websocket and REST available Quote information type OrderLeg struct { - Price string `json:"px"` - Size string `json:"sz"` - InstrumentID string `json:"instId"` - Side string `json:"side"` - TargetCurrency string `json:"tgtCcy"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + InstrumentID string `json:"instId"` + Side string `json:"side"` + TargetCurrency string `json:"tgtCcy"` // available in REST only Fee types.Number `json:"fee"` @@ -1734,44 +2416,44 @@ type OrderLeg struct { TradeID string `json:"tradeId"` } -// CreateQuoteParams holds information related to create quote. +// CreateQuoteParams holds information related to create quote type CreateQuoteParams struct { - RfqID string `json:"rfqId"` + RFQID string `json:"rfqId"` ClientQuoteID string `json:"clQuoteId"` - QuoteSide order.Side `json:"quoteSide"` + QuoteSide string `json:"quoteSide"` Legs []QuoteLeg `json:"legs"` } -// QuoteLeg the legs of the Quote. +// QuoteLeg the legs of the Quote type QuoteLeg struct { - Price types.Number `json:"px"` - SizeOfQuoteLeg types.Number `json:"sz"` - InstrumentID string `json:"instId"` - Side order.Side `json:"side"` + Price float64 `json:"px,string"` + SizeOfQuoteLeg float64 `json:"sz,string"` + InstrumentID string `json:"instId"` + Side order.Side `json:"side"` // TargetCurrency represents target currency TargetCurrency string `json:"tgtCcy,omitempty"` } -// QuoteResponse holds create quote response variables. +// QuoteResponse holds create quote response variables type QuoteResponse struct { - CreationTime okxUnixMilliTime `json:"cTime"` - UpdateTime okxUnixMilliTime `json:"uTime"` - ValidUntil okxUnixMilliTime `json:"validUntil"` - QuoteID string `json:"quoteId"` - ClientQuoteID string `json:"clQuoteId"` - RfqID string `json:"rfqId"` - QuoteSide string `json:"quoteSide"` - ClientRfqID string `json:"clRfqId"` - TraderCode string `json:"traderCode"` - State string `json:"state"` - Legs []QuoteLeg `json:"legs"` + CreationTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` + ValidUntil types.Time `json:"validUntil"` + QuoteID string `json:"quoteId"` + ClientQuoteID string `json:"clQuoteId"` + RFQID string `json:"rfqId"` + QuoteSide string `json:"quoteSide"` + ClientRFQID string `json:"clRfqId"` + TraderCode string `json:"traderCode"` + State string `json:"state"` + Legs []QuoteLeg `json:"legs"` } // CancelQuoteRequestParams represents cancel quote request params type CancelQuoteRequestParams struct { - QuoteID string `json:"quoteId"` - ClientQuoteID string `json:"clQuoteId"` + QuoteID string `json:"quoteId,omitempty"` + ClientQuoteID string `json:"clQuoteId,omitempty"` } // CancelQuotesRequestParams represents cancel multiple quotes request params @@ -1784,42 +2466,42 @@ type CancelQuotesRequestParams struct { type CancelQuoteResponse struct { QuoteID string `json:"quoteId"` ClientQuoteID string `json:"clQuoteId"` - SCode string `json:"sCode"` - SMsg string `json:"sMsg"` + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` } -// RfqRequestParams represents get Rfq orders param -type RfqRequestParams struct { - RfqID string - ClientRfqID string +// RFQRequestParams represents get RFQ orders param +type RFQRequestParams struct { + RFQID string + ClientRFQID string State string BeginningID string EndID string Limit int64 } -// RfqResponse Rfq response detail. -type RfqResponse struct { - CreateTime okxUnixMilliTime `json:"cTime"` - UpdateTime okxUnixMilliTime `json:"uTime"` - ValidUntil okxUnixMilliTime `json:"validUntil"` - TraderCode string `json:"traderCode"` - RfqID string `json:"rfqId"` - ClientRfqID string `json:"clRfqId"` - State string `json:"state"` - Counterparties []string `json:"counterparties"` +// RFQResponse RFQ response detail +type RFQResponse struct { + CreateTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` + ValidUntil types.Time `json:"validUntil"` + TraderCode string `json:"traderCode"` + RFQID string `json:"rfqId"` + ClientRFQID string `json:"clRfqId"` + State string `json:"state"` + Counterparties []string `json:"counterparties"` Legs []struct { - InstrumentID string `json:"instId"` - Size string `json:"sz"` - Side string `json:"side"` - TgtCcy string `json:"tgtCcy"` + InstrumentID string `json:"instId"` + Size types.Number `json:"sz"` + Side string `json:"side"` + TgtCurrency string `json:"tgtCcy"` } `json:"legs"` } -// QuoteRequestParams request params. +// QuoteRequestParams request params type QuoteRequestParams struct { - RfqID string - ClientRfqID string + RFQID string + ClientRFQID string QuoteID string ClientQuoteID string State string @@ -1828,10 +2510,10 @@ type QuoteRequestParams struct { Limit int64 } -// RfqTradesRequestParams represents Rfq trades request param -type RfqTradesRequestParams struct { - RfqID string - ClientRfqID string +// RFQTradesRequestParams represents RFQ trades request param +type RFQTradesRequestParams struct { + RFQID string + ClientRFQID string QuoteID string BlockTradeID string ClientQuoteID string @@ -1841,72 +2523,77 @@ type RfqTradesRequestParams struct { Limit int64 } -// RfqTradeResponse Rfq trade response -type RfqTradeResponse struct { - RfqID string `json:"rfqId"` - ClientRfqID string `json:"clRfqId"` - QuoteID string `json:"quoteId"` - ClientQuoteID string `json:"clQuoteId"` - BlockTradeID string `json:"blockTdId"` - Legs []BlockTradeLeg `json:"legs"` - CreationTime time.Time `json:"cTime"` - TakerTraderCode string `json:"tTraderCode"` - MakerTraderCode string `json:"mTraderCode"` +// RFQTradeResponse RFQ trade response +type RFQTradeResponse struct { + RFQID string `json:"rfqId"` + ClientRFQID string `json:"clRfqId"` + QuoteID string `json:"quoteId"` + ClientQuoteID string `json:"clQuoteId"` + BlockTradeID string `json:"blockTdId"` + Legs []RFQTradeLeg `json:"legs"` + CreationTime types.Time `json:"cTime"` + TakerTraderCode string `json:"tTraderCode"` + MakerTraderCode string `json:"mTraderCode"` } -// BlockTradeLeg Rfq trade response leg. -type BlockTradeLeg struct { - TradeID string `json:"tradeId"` +// RFQTradeLeg RFQ trade response leg +type RFQTradeLeg struct { InstrumentID string `json:"instId"` - Side order.Side `json:"side"` - Size types.Number `json:"sz"` + Side string `json:"side"` + Size string `json:"sz"` Price types.Number `json:"px"` - Fee types.Number `json:"fee,omitempty"` - FeeCurrency string `json:"feeCcy,omitempty"` + TradeID string `json:"tradeId"` + + Fee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` } -// PublicBlockTradesResponse represents data will be pushed whenever there is a block trade. -type PublicBlockTradesResponse struct { - BlockTradeID string `json:"blockTdId"` - CreationTime okxUnixMilliTime `json:"cTime"` - Legs []BlockTradeLeg `json:"legs"` +// PublicTradesResponse represents data will be pushed whenever there is a block trade +type PublicTradesResponse struct { + BlockTradeID string `json:"blockTdId"` + CreationTime types.Time `json:"cTime"` + Legs []RFQTradeLeg `json:"legs"` } -// SubaccountInfo represents subaccount information detail. +// SubaccountInfo represents subaccount information detail type SubaccountInfo struct { - Enable bool `json:"enable"` - SubAccountName string `json:"subAcct"` - SubaccountType string `json:"type"` // sub-account note - SubaccountLabel string `json:"label"` - MobileNumber string `json:"mobile"` // Mobile number that linked with the sub-account. - GoogleAuth bool `json:"gAuth"` // If the sub-account switches on the Google Authenticator for login authentication. - CanTransferOut bool `json:"canTransOut"` // If can transfer out, false: can not transfer out, true: can transfer. - Timestamp okxUnixMilliTime `json:"ts"` + Enable bool `json:"enable"` + SubAccountName string `json:"subAcct"` + SubaccountType string `json:"type"` // sub-account note + SubaccountLabel string `json:"label"` + MobileNumber string `json:"mobile"` // Mobile number that linked with the sub-account. + GoogleAuth bool `json:"gAuth"` // If the sub-account switches on the Google Authenticator for login authentication. + CanTransferOut bool `json:"canTransOut"` // If can transfer out, false: can not transfer out, true: can transfer. + Timestamp types.Time `json:"ts"` } // SubaccountBalanceDetail represents subaccount balance detail type SubaccountBalanceDetail struct { - AvailableBalance string `json:"availBal"` - AvailableEquity string `json:"availEq"` - CashBalance string `json:"cashBal"` - Currency string `json:"ccy"` - CrossLiability string `json:"crossLiab"` - DiscountEquity string `json:"disEq"` - Equity string `json:"eq"` - EquityUsd string `json:"eqUsd"` - FrozenBalance string `json:"frozenBal"` - Interest string `json:"interest"` - IsoEquity string `json:"isoEq"` - IsolatedLiabilities string `json:"isoLiab"` - LiabilitiesOfCurrency string `json:"liab"` - MaxLoan string `json:"maxLoan"` - MarginRatio string `json:"mgnRatio"` - NotionalLeverage string `json:"notionalLever"` - OrdFrozen string `json:"ordFrozen"` - Twap string `json:"twap"` - UpdateTime okxUnixMilliTime `json:"uTime"` - UnrealizedProfitAndLoss string `json:"upl"` - UnrealizedProfitAndLiabilities string `json:"uplLiab"` + AvailableBalance types.Number `json:"availBal"` + AvailableEquity types.Number `json:"availEq"` + CashBalance types.Number `json:"cashBal"` + Currency string `json:"ccy"` + CrossLiability types.Number `json:"crossLiab"` + DiscountEquity types.Number `json:"disEq"` + Equity types.Number `json:"eq"` + EquityUSD types.Number `json:"eqUsd"` + FrozenBalance types.Number `json:"frozenBal"` + Interest types.Number `json:"interest"` + IsoEquity string `json:"isoEq"` + IsolatedLiabilities types.Number `json:"isoLiab"` + LiabilitiesOfCurrency string `json:"liab"` + MaxLoan types.Number `json:"maxLoan"` + MarginRatio types.Number `json:"mgnRatio"` + NotionalLeverage string `json:"notionalLever"` + OrdFrozen string `json:"ordFrozen"` + Twap string `json:"twap"` + UpdateTime types.Time `json:"uTime"` + UnrealizedProfitAndLoss types.Number `json:"upl"` + UnrealizedProfitAndLiabilities string `json:"uplLiab"` + FixedBalance types.Number `json:"fixedBal"` + BorrowFroz types.Number `json:"borrowFroz"` + SpotISOBalance types.Number `json:"spotIsoBal"` + SMTSyncEquity types.Number `json:"smtSyncEq"` } // SubaccountBalanceResponse represents subaccount balance response @@ -1915,50 +2602,72 @@ type SubaccountBalanceResponse struct { Details []SubaccountBalanceDetail `json:"details"` Imr string `json:"imr"` IsolatedMarginEquity string `json:"isoEq"` - MarginRatio string `json:"mgnRatio"` - MaintenanceMarginRequirement string `json:"mmr"` - NotionalUsd string `json:"notionalUsd"` - OrdFroz string `json:"ordFroz"` - TotalEq string `json:"totalEq"` - UpdateTime okxUnixMilliTime `json:"uTime"` + MarginRatio types.Number `json:"mgnRatio"` + MaintenanceMarginRequirement types.Number `json:"mmr"` + NotionalUSD types.Number `json:"notionalUsd"` + OrdFroz types.Number `json:"ordFroz"` + TotalEq types.Number `json:"totalEq"` + UpdateTime types.Time `json:"uTime"` + BorrowFroz types.Number `json:"borrowFroz"` + UPL types.Number `json:"upl"` } -// FundingBalance holds function balance. +// FundingBalance holds function balance type FundingBalance struct { - AvailableBalance string `json:"availBal"` - Balance string `json:"bal"` - Currency string `json:"ccy"` - FrozenBalance string `json:"frozenBal"` + AvailableBalance types.Number `json:"availBal"` + Balance types.Number `json:"bal"` + Currency string `json:"ccy"` + FrozenBalance types.Number `json:"frozenBal"` +} + +// SubAccountMaximumWithdrawal holds sub-account maximum withdrawal information +type SubAccountMaximumWithdrawal struct { + Currency string `json:"ccy"` + MaxWd types.Number `json:"maxWd"` + MaxWdEx types.Number `json:"maxWdEx"` + SpotOffsetMaxWd types.Number `json:"spotOffsetMaxWd"` + SpotOffsetMaxWdEx types.Number `json:"spotOffsetMaxWdEx"` } // SubaccountBillItem represents subaccount balance bill item type SubaccountBillItem struct { - BillID string `json:"billId"` - Type string `json:"type"` - AccountCurrencyBalance string `json:"ccy"` - Amount string `json:"amt"` - SubAccount string `json:"subAcct"` - Timestamp okxUnixMilliTime `json:"ts"` + BillID string `json:"billId"` + Type string `json:"type"` + AccountCurrencyBalance string `json:"ccy"` + Amount types.Number `json:"amt"` + SubAccount string `json:"subAcct"` + Timestamp types.Time `json:"ts"` +} + +// SubAccountTransfer holds sub-account transfer instance +type SubAccountTransfer struct { + BillID string `json:"billId"` + Type string `json:"type"` + Currency string `json:"ccy"` + SubAccount string `json:"subAcct"` + SubUID string `json:"subUid"` + Amount types.Number `json:"amt"` + Timestamp types.Time `json:"ts"` } -// SubAccountAssetTransferParams represents subaccount asset transfer request parameters. +// SubAccountAssetTransferParams represents subaccount asset transfer request parameters type SubAccountAssetTransferParams struct { - Currency string `json:"ccy"` // {REQUIRED} - Amount float64 `json:"amt,string"` // {REQUIRED} - From int64 `json:"from,string"` // {REQUIRED} 6:Funding Account 18:Trading account - To int64 `json:"to,string"` // {REQUIRED} 6:Funding Account 18:Trading account - FromSubAccount string `json:"fromSubAccount"` // {REQUIRED} subaccount name. - ToSubAccount string `json:"toSubAccount"` // {REQUIRED} destination sub-account - LoanTransfer bool `json:"loanTrans,omitempty"` - OmitPositionRisk bool `json:"omitPosRisk,omitempty"` + Currency currency.Code `json:"ccy"` // {REQUIRED} + Amount float64 `json:"amt,string"` // {REQUIRED} + From int64 `json:"from,string"` // {REQUIRED} 6:Funding Account 18:Trading account + To int64 `json:"to,string"` // {REQUIRED} 6:Funding Account 18:Trading account + FromSubAccount string `json:"fromSubAccount"` // {REQUIRED} subaccount name. + ToSubAccount string `json:"toSubAccount"` // {REQUIRED} destination sub-account + LoanTransfer bool `json:"loanTrans,omitempty"` + OmitPositionRisk bool `json:"omitPosRisk,omitempty"` } -// TransferIDInfo represents master account transfer between subaccount. +// TransferIDInfo represents master account transfer between subaccount type TransferIDInfo struct { TransferID string `json:"transId"` } -// PermissionOfTransfer represents subaccount transfer information and it's permission. +// PermissionOfTransfer represents subaccount transfer information and it's permission type PermissionOfTransfer struct { SubAcct string `json:"subAcct"` CanTransOut bool `json:"canTransOut"` @@ -1969,204 +2678,234 @@ type SubaccountName struct { SubaccountName string `json:"subAcct"` } -// GridAlgoOrder represents grid algo order. +// SubAccountLoanAllocationParam holds parameter for VIP sub-account loan allocation +type SubAccountLoanAllocationParam struct { + Enable bool `json:"enable"` + Alloc []subAccountVIPLoanAllocationInfo `json:"alloc"` +} + +type subAccountVIPLoanAllocationInfo struct { + SubAcct string `json:"subAcct"` + LoanAlloc float64 `json:"loanAlloc,string"` +} + +// SubAccounBorrowInterestAndLimit represents sub-account borrow interest and limit +type SubAccounBorrowInterestAndLimit struct { + SubAcct string `json:"subAcct"` + Debt types.Number `json:"debt"` + Interest types.Number `json:"interest"` + NextDiscountTime types.Time `json:"nextDiscountTime"` + NextInterestTime types.Time `json:"nextInterestTime"` + LoanAlloc types.Number `json:"loanAlloc"` + Records []struct { + AvailLoan types.Number `json:"availLoan"` + Currency string `json:"ccy"` + Interest types.Number `json:"interest"` + LoanQuota types.Number `json:"loanQuota"` + PosLoan string `json:"posLoan"` + Rate types.Number `json:"rate"` + SurplusLmt string `json:"surplusLmt"` + SurplusLmtDetails struct { + AllAcctRemainingQuota types.Number `json:"allAcctRemainingQuota"` + CurAcctRemainingQuota types.Number `json:"curAcctRemainingQuota"` + PlatRemainingQuota types.Number `json:"platRemainingQuota"` + } `json:"surplusLmtDetails"` + UsedLmt types.Number `json:"usedLmt"` + UsedLoan types.Number `json:"usedLoan"` + } `json:"records"` +} + +// GridAlgoOrder represents grid algo order type GridAlgoOrder struct { - InstrumentID string `json:"instId"` - AlgoOrdType string `json:"algoOrdType"` - MaxPrice types.Number `json:"maxPx"` - MinPrice types.Number `json:"minPx"` - GridQuantity types.Number `json:"gridNum"` - GridType string `json:"runType"` // "1": Arithmetic, "2": Geometric Default is Arithmetic + InstrumentID string `json:"instId"` + AlgoOrdType string `json:"algoOrdType"` + MaxPrice float64 `json:"maxPx,string"` + MinPrice float64 `json:"minPx,string"` + GridQuantity float64 `json:"gridNum,string"` + GridType string `json:"runType"` // "1": Arithmetic, "2": Geometric Default is Arithmetic // Spot Grid Order - QuoteSize types.Number `json:"quoteSz"` // Invest amount for quote currency Either "instId" or "ccy" is required - BaseSize types.Number `json:"baseSz"` // Invest amount for base currency Either "instId" or "ccy" is required + QuoteSize float64 `json:"quoteSz,string"` // Invest amount for quote currency Either "instId" or "ccy" is required + BaseSize float64 `json:"baseSz,string"` // Invest amount for base currency Either "instId" or "ccy" is required // Contract Grid Order - BasePosition bool `json:"basePos"` // Whether or not open a position when strategy actives Default is false Neutral contract grid should omit the parameter - Size types.Number `json:"sz"` - Direction string `json:"direction"` - Lever string `json:"lever"` + BasePosition bool `json:"basePos"` // Whether or not open a position when strategy actives Default is false Neutral contract grid should omit the parameter + Size float64 `json:"sz,string"` + Direction string `json:"direction"` + Leverage string `json:"lever"` } // GridAlgoOrderIDResponse represents grid algo order type GridAlgoOrderIDResponse struct { - AlgoOrderID string `json:"algoId"` - SCode string `json:"sCode"` - SMsg string `json:"sMsg"` -} - -// GridAlgoOrderAmend represents amend algo order response -type GridAlgoOrderAmend struct { - AlgoID string `json:"algoId"` - InstrumentID string `json:"instId"` - StopLossTriggerPrice string `json:"slTriggerPx"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` + AlgoOrderID string `json:"algoId"` + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` } -// StopGridAlgoOrderRequest represents stop grid algo order request parameter -type StopGridAlgoOrderRequest struct { +// StopGridAlgoOrderParam holds stop grid algo order parameter +type StopGridAlgoOrderParam struct { AlgoID string `json:"algoId"` InstrumentID string `json:"instId"` - StopType uint `json:"stopType,string"` // Spot grid "1": Sell base currency "2": Keep base currency | Contract grid "1": Market Close All positions "2": Keep positions + StopType string `json:"stopType"` AlgoOrderType string `json:"algoOrdType"` } -// GridAlgoOrderResponse a complete information of grid algo order item response. -type GridAlgoOrderResponse struct { - ActualLever string `json:"actualLever"` - AlgoID string `json:"algoId"` - AlgoOrderType string `json:"algoOrdType"` - ArbitrageNumber string `json:"arbitrageNum"` - BasePosition bool `json:"basePos"` - BaseSize string `json:"baseSz"` - CancelType string `json:"cancelType"` - Direction string `json:"direction"` - FloatProfit string `json:"floatProfit"` - GridQuantity string `json:"gridNum"` - GridProfit string `json:"gridProfit"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Investment string `json:"investment"` - Leverage string `json:"lever"` - EstimatedLiquidationPrice string `json:"liqPx"` - MaximumPrice string `json:"maxPx"` - MinimumPrice string `json:"minPx"` - ProfitAndLossRatio string `json:"pnlRatio"` - QuoteSize string `json:"quoteSz"` - RunType string `json:"runType"` - StopLossTriggerPx string `json:"slTriggerPx"` - State string `json:"state"` - StopResult string `json:"stopResult,omitempty"` - StopType string `json:"stopType"` - Size string `json:"sz"` - Tag string `json:"tag"` - TotalProfitAndLoss string `json:"totalPnl"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - CreationTime okxUnixMilliTime `json:"cTime"` - UpdateTime okxUnixMilliTime `json:"uTime"` - Underlying string `json:"uly"` +// ClosePositionParams holds close position parameters +type ClosePositionParams struct { + AlgoID string `json:"algoId"` + MarketCloseAllPositions bool `json:"mktClose"` // true: Market close all position, false:Close part of position + Size float64 `json:"sz,omitempty,string"` + Price float64 `json:"px,omitempty,string"` +} - // Added in Detail +// ClosePositionContractGridResponse holds contract grid close position response data +type ClosePositionContractGridResponse struct { + AlgoClientOrderID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + OrderID string `json:"ordId"` + Tag string `json:"tag"` +} - EquityOfStrength string `json:"eq,omitempty"` - PerMaxProfitRate string `json:"perMaxProfitRate,omitempty"` - PerMinProfitRate string `json:"perMinProfitRate,omitempty"` - Profit string `json:"profit,omitempty"` - Runpx string `json:"runpx,omitempty"` - SingleAmt string `json:"singleAmt,omitempty"` - TotalAnnualizedRate string `json:"totalAnnualizedRate,omitempty"` - TradeNumber string `json:"tradeNum,omitempty"` +// CancelClosePositionOrder holds close position order parameter cancellation parameter +type CancelClosePositionOrder struct { + AlgoID string `json:"algoId"` + OrderID string `json:"ordId"` // Close position order ID +} - // Suborders Detail +// TriggeredGridAlgoOrderInfo holds grid algo order info +type TriggeredGridAlgoOrderInfo struct { + AlgoClientOrderID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` +} + +// GridAlgoOrderAmend represents amend algo order response +type GridAlgoOrderAmend struct { + AlgoID string `json:"algoId"` + InstrumentID string `json:"instId"` + StopLossTriggerPrice types.Number `json:"slTriggerPx"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` +} + +// StopGridAlgoOrderRequest represents stop grid algo order request parameter +type StopGridAlgoOrderRequest struct { + AlgoID string `json:"algoId"` + InstrumentID string `json:"instId"` + StopType uint64 `json:"stopType,string"` // Spot grid "1": Sell base currency "2": Keep base currency | Contract grid "1": Market Close All positions "2": Keep positions + AlgoOrderType string `json:"algoOrdType"` +} + +// GridAlgoOrderResponse is a complete information of grid algo order item response +type GridAlgoOrderResponse struct { + ActualLever string `json:"actualLever"` + AlgoID string `json:"algoId"` + AlgoOrderType string `json:"algoOrdType"` + ArbitrageNumber string `json:"arbitrageNum"` + BasePosition bool `json:"basePos"` + BaseSize types.Number `json:"baseSz"` + CancelType string `json:"cancelType"` + Direction string `json:"direction"` + FloatProfit types.Number `json:"floatProfit"` + GridQuantity types.Number `json:"gridNum"` + GridProfit string `json:"gridProfit"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Investment string `json:"investment"` + Leverage string `json:"lever"` + EstimatedLiquidationPrice types.Number `json:"liqPx"` + MaximumPrice types.Number `json:"maxPx"` + MinimumPrice types.Number `json:"minPx"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + QuoteSize types.Number `json:"quoteSz"` + RunType string `json:"runType"` + StopLossTriggerPrice types.Number `json:"slTriggerPx"` + State string `json:"state"` + StopResult string `json:"stopResult,omitempty"` + StopType string `json:"stopType"` + Size types.Number `json:"sz"` + Tag string `json:"tag"` + TotalProfitAndLoss types.Number `json:"totalPnl"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` + CreationTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` + Underlying string `json:"uly"` - AnnualizedRate string `json:"annualizedRate,omitempty"` - CurBaseSz string `json:"curBaseSz,omitempty"` - CurQuoteSz string `json:"curQuoteSz,omitempty"` -} - -// GridAlgoSuborder represents a grid algo suborder item. -type GridAlgoSuborder struct { - ActualLeverage string `json:"actualLever"` - AlgoID string `json:"algoId"` - AlgoOrderType string `json:"algoOrdType"` - AnnualizedRate string `json:"annualizedRate"` - ArbitrageNum string `json:"arbitrageNum"` - BasePosition bool `json:"basePos"` - BaseSize string `json:"baseSz"` - CancelType string `json:"cancelType"` - CurBaseSz string `json:"curBaseSz"` - CurQuoteSz string `json:"curQuoteSz"` - Direction string `json:"direction"` - EquityOfStrength string `json:"eq"` - FloatProfit string `json:"floatProfit"` - GridQuantity string `json:"gridNum"` - GridProfit string `json:"gridProfit"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Investment string `json:"investment"` - Leverage string `json:"lever"` - LiquidationPx string `json:"liqPx"` - MaximumPrice string `json:"maxPx"` - MinimumPrice string `json:"minPx"` - PerMaxProfitRate string `json:"perMaxProfitRate"` - PerMinProfitRate string `json:"perMinProfitRate"` - ProfitAndLossRatio string `json:"pnlRatio"` - Profit string `json:"profit"` - QuoteSize string `json:"quoteSz"` - RunType string `json:"runType"` - Runpx string `json:"runpx"` - SingleAmount string `json:"singleAmt"` - StopLossTriggerPx string `json:"slTriggerPx"` - State string `json:"state"` - StopResult string `json:"stopResult"` - StopType string `json:"stopType"` - Size string `json:"sz"` - Tag string `json:"tag"` - TotalAnnualizedRate string `json:"totalAnnualizedRate"` - TotalProfitAndLoss string `json:"totalPnl"` - TakeProfitTriggerPx string `json:"tpTriggerPx"` - TradeNum string `json:"tradeNum"` - UpdateTime okxUnixMilliTime `json:"uTime"` - CreationTime okxUnixMilliTime `json:"cTime"` -} - -// AlgoOrderPosition represents algo order position detailed data. + // Added in Detail + + EquityOfStrength string `json:"eq,omitempty"` + PerMaxProfitRate types.Number `json:"perMaxProfitRate,omitempty"` + PerMinProfitRate types.Number `json:"perMinProfitRate,omitempty"` + Profit types.Number `json:"profit,omitempty"` + Runpx string `json:"runpx,omitempty"` + SingleAmt types.Number `json:"singleAmt,omitempty"` + TotalAnnualizedRate types.Number `json:"totalAnnualizedRate,omitempty"` + TradeNumber string `json:"tradeNum,omitempty"` + + // Suborders Detail + + AnnualizedRate types.Number `json:"annualizedRate,omitempty"` + CurBaseSize types.Number `json:"curBaseSz,omitempty"` + CurQuoteSize types.Number `json:"curQuoteSz,omitempty"` +} + +// AlgoOrderPosition represents algo order position detailed data type AlgoOrderPosition struct { - AutoDecreasingLine string `json:"adl"` - AlgoID string `json:"algoId"` - AveragePrice string `json:"avgPx"` - Currency string `json:"ccy"` - InitialMarginRequirement string `json:"imr"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - LastTradedPrice string `json:"last"` - Leverage string `json:"lever"` - LiquidationPrice string `json:"liqPx"` - MarkPrice string `json:"markPx"` - MarginMode string `json:"mgnMode"` - MarginRatio string `json:"mgnRatio"` - MaintenanceMarginRequirement string `json:"mmr"` - NotionalUSD string `json:"notionalUsd"` - QuantityPosition string `json:"pos"` - PositionSide string `json:"posSide"` - UnrealizedProfitAndLoss string `json:"upl"` - UnrealizedProfitAndLossRatio string `json:"uplRatio"` - UpdateTime okxUnixMilliTime `json:"uTime"` - CreationTime okxUnixMilliTime `json:"cTime"` -} - -// AlgoOrderWithdrawalProfit algo withdrawal order profit info. + AutoDecreasingLine string `json:"adl"` + AlgoID string `json:"algoId"` + AveragePrice types.Number `json:"avgPx"` + Currency string `json:"ccy"` + InitialMarginRequirement string `json:"imr"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + LastTradedPrice types.Number `json:"last"` + Leverage types.Number `json:"lever"` + LiquidationPrice types.Number `json:"liqPx"` + MarkPrice types.Number `json:"markPx"` + MarginMode string `json:"mgnMode"` + MarginRatio types.Number `json:"mgnRatio"` + MaintenanceMarginRequirement string `json:"mmr"` + NotionalUSD types.Number `json:"notionalUsd"` + QuantityPosition types.Number `json:"pos"` + PositionSide string `json:"posSide"` + UnrealizedProfitAndLoss types.Number `json:"upl"` + UnrealizedProfitAndLossRatio types.Number `json:"uplRatio"` + UpdateTime types.Time `json:"uTime"` + CreationTime types.Time `json:"cTime"` +} + +// AlgoOrderWithdrawalProfit algo withdrawal order profit info type AlgoOrderWithdrawalProfit struct { AlgoID string `json:"algoId"` WithdrawProfit string `json:"profit"` } -// SystemStatusResponse represents the system status and other details. +// SystemStatusResponse represents the system status and other details type SystemStatusResponse struct { - Title string `json:"title"` - State string `json:"state"` - Begin okxUnixMilliTime `json:"begin"` // Begin time of system maintenance, - End okxUnixMilliTime `json:"end"` // Time of resuming trading totally. - Href string `json:"href"` // Hyperlink for system maintenance details - ServiceType string `json:"serviceType"` - System string `json:"system"` - ScheduleDescription string `json:"scheDesc"` + Title string `json:"title"` + State string `json:"state"` + Begin types.Time `json:"begin"` // Begin time of system maintenance, + End types.Time `json:"end"` // Time of resuming trading totally. + Href string `json:"href"` // Hyperlink for system maintenance details + ServiceType string `json:"serviceType"` + System string `json:"system"` + ScheduleDescription string `json:"scheDesc"` + PreOpenBegin string `json:"preOpenBegin"` + MaintenanceType string `json:"maintType"` + Environment string `json:"env"` // Environment '1': Production Trading '2': Demo Trading // PushTime timestamp information when the data is pushed - PushTime okxUnixMilliTime `json:"ts"` + PushTime types.Time `json:"ts"` } -// BlockTicker holds block trading information. +// BlockTicker holds block trading information type BlockTicker struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - TradingVolumeInCCY24Hour types.Number `json:"volCcy24h"` - TradingVolumeInUSD24Hour types.Number `json:"vol24h"` - Timestamp okxUnixMilliTime `json:"ts"` + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + TradingVolumeInCCY24Hour types.Number `json:"volCcy24h"` + TradingVolumeInUSD24Hour types.Number `json:"vol24h"` + Timestamp types.Time `json:"ts"` } -// BlockTrade represents a block trade. +// BlockTrade represents a block trade type BlockTrade struct { InstrumentID string `json:"instId"` TradeID string `json:"tradeId"` @@ -2180,15 +2919,160 @@ type BlockTrade struct { Timestamp types.Time `json:"ts"` } -// UnitConvertResponse unit convert response. +// SpreadOrderParam holds parameters for spread orders +type SpreadOrderParam struct { + InstrumentID string `json:"instId"` + SpreadID string `json:"sprdId,omitempty"` + ClientOrderID string `json:"clOrdId,omitempty"` + Side string `json:"side"` // Order side, buy sell + OrderType string `json:"ordType"` // Order type 'limit': Limit order 'post_only': Post-only order 'ioc': Immediate-or-cancel order + Size float64 `json:"sz,string"` + Price float64 `json:"px,string"` + Tag string `json:"tag,omitempty"` +} + +// SpreadOrderResponse represents a spread create order response +type SpreadOrderResponse struct { + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` + ClientOrderID string `json:"clOrdId"` + OrderID string `json:"ordId"` + Tag string `json:"tag"` + + // Added when amending spread order through websocket + RequestID string `json:"reqId"` +} + +// AmendSpreadOrderParam holds amend parameters for spread order +type AmendSpreadOrderParam struct { + OrderID string `json:"ordId"` + ClientOrderID string `json:"clOrdId"` + RequestID string `json:"reqId"` + NewSize float64 `json:"newSz,omitempty,string"` + NewPrice float64 `json:"newPx,omitempty,string"` +} + +// SpreadOrder holds spread order details +type SpreadOrder struct { + TradeID string `json:"tradeId"` + InstrumentID string `json:"instId"` + OrderID string `json:"ordId"` + SpreadID string `json:"sprdId"` + ClientOrderID string `json:"clOrdId"` + Tag string `json:"tag"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + OrderType string `json:"ordType"` + Side string `json:"side"` + FillSize types.Number `json:"fillSz"` + FillPrice types.Number `json:"fillPx"` + AccFillSize types.Number `json:"accFillSz"` + PendingFillSize types.Number `json:"pendingFillSz"` + PendingSettleSize types.Number `json:"pendingSettleSz"` + CanceledSize types.Number `json:"canceledSz"` + State string `json:"state"` + AveragePrice types.Number `json:"avgPx"` + CancelSource string `json:"cancelSource"` + UpdateTime types.Time `json:"uTime"` + CreationTime types.Time `json:"cTime"` +} + +// SpreadTrade holds spread trade transaction instance +type SpreadTrade struct { + SpreadID string `json:"sprdId"` + TradeID string `json:"tradeId"` + OrderID string `json:"ordId"` + ClientOrderID string `json:"clOrdId"` + Tag string `json:"tag"` + FillPrice types.Number `json:"fillPx"` + FillSize types.Number `json:"fillSz"` + State string `json:"state"` + Side string `json:"side"` + ExecType string `json:"execType"` + Timestamp string `json:"ts"` + Legs []struct { + InstrumentID string `json:"instId"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + Side string `json:"side"` + Fee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` + TradeID string `json:"tradeId"` + } `json:"legs"` + Code string `json:"code"` + Msg string `json:"msg"` +} + +// SpreadInstrument retrieve all available spreads based on the request parameters +type SpreadInstrument struct { + SpreadID string `json:"sprdId"` + SpreadType string `json:"sprdType"` + State string `json:"state"` + BaseCurrency string `json:"baseCcy"` + SizeCurrency string `json:"szCcy"` + QuoteCurrency string `json:"quoteCcy"` + TickSize types.Number `json:"tickSz"` + MinSize types.Number `json:"minSz"` + LotSize types.Number `json:"lotSz"` + ListTime types.Time `json:"listTime"` + Legs []struct { + InstrumentID string `json:"instId"` + Side string `json:"side"` + } `json:"legs"` + ExpTime types.Time `json:"expTime"` + UpdateTime types.Time `json:"uTime"` +} + +// SpreadOrderbook holds spread orderbook information +type SpreadOrderbook struct { + // Asks and Bids are [3]string; price, quantity, and # number of orders at the price + Asks [][]types.Number `json:"asks"` + Bids [][]types.Number `json:"bids"` + Timestamp types.Time `json:"ts"` +} + +// SpreadTicker represents a ticker instance +type SpreadTicker struct { + SpreadID string `json:"sprdId"` + Last types.Number `json:"last"` + LastSize types.Number `json:"lastSz"` + AskPrice types.Number `json:"askPx"` + AskSize types.Number `json:"askSz"` + BidPrice types.Number `json:"bidPx"` + BidSize types.Number `json:"bidSz"` + Timestamp types.Time `json:"ts"` +} + +// SpreadPublicTradeItem represents publicly available trade order instance +type SpreadPublicTradeItem struct { + SprdID string `json:"sprdId"` + Side string `json:"side"` + Size types.Number `json:"sz"` + Price types.Number `json:"px"` + TradeID string `json:"tradeId"` + Timestamp types.Time `json:"ts"` +} + +// UnitConvertResponse unit convert response type UnitConvertResponse struct { InstrumentID string `json:"instId"` Price types.Number `json:"px"` Size types.Number `json:"sz"` - ConvertType uint64 `json:"type"` + ConvertType types.Number `json:"type"` Unit string `json:"unit"` } +// OptionTickBand holds option band information +type OptionTickBand struct { + InstrumentType string `json:"instType"` + InstrumentFamily string `json:"instFamily"` + TickBand []struct { + MinPrice types.Number `json:"minPx"` + MaxPrice types.Number `json:"maxPx"` + TickSize types.Number `json:"tickSz"` + } `json:"tickBand"` +} + // Websocket Models // WebsocketEventRequest contains event data for a websocket channel @@ -2197,105 +3081,56 @@ type WebsocketEventRequest struct { Arguments []WebsocketLoginData `json:"args"` // args: the value is the channel name, which can be one or more channels } -// WebsocketLoginData represents the websocket login data input json data. +// WebsocketLoginData represents the websocket login data input json data type WebsocketLoginData struct { - APIKey string `json:"apiKey"` - Passphrase string `json:"passphrase"` - Timestamp time.Time `json:"timestamp"` - Sign string `json:"sign"` -} - -// WSLoginResponse represents a websocket login response. -type WSLoginResponse struct { - Event string `json:"event"` - Code string `json:"code"` - Msg string `json:"msg"` + APIKey string `json:"apiKey"` + Passphrase string `json:"passphrase"` + Timestamp int64 `json:"timestamp,string"` + Sign string `json:"sign"` } -// SubscriptionInfo holds the channel and instrument IDs. +// SubscriptionInfo holds the channel and instrument IDs type SubscriptionInfo struct { - Channel string `json:"channel"` - InstrumentID string `json:"instId,omitempty"` - InstrumentType string `json:"instType,omitempty"` - Underlying string `json:"uly,omitempty"` - UID string `json:"uid,omitempty"` // user identifier + Channel string `json:"channel,omitempty"` + InstrumentID string `json:"instId,omitempty"` + InstrumentFamily string `json:"instFamily,omitempty"` + InstrumentType string `json:"instType,omitempty"` + Underlying string `json:"uly,omitempty"` + UID string `json:"uid,omitempty"` // user identifier // For Algo Orders AlgoID string `json:"algoId,omitempty"` - Currency string `json:"ccy,omitempty"` // Currency: -} - -// WSSubscriptionInformation websocket subscription and unsubscription operation inputs. -type WSSubscriptionInformation struct { - Operation string `json:"op"` - Arguments SubscriptionInfo `json:"arg"` + Currency string `json:"ccy,omitempty"` + SpreadID string `json:"sprdId,omitempty"` } -// WSSubscriptionInformationList websocket subscription and unsubscription operation inputs. +// WSSubscriptionInformationList websocket subscription and unsubscription operation inputs type WSSubscriptionInformationList struct { Operation string `json:"op"` Arguments []SubscriptionInfo `json:"args"` } -// WSSubscriptionResponse represents websocket subscription information. -type WSSubscriptionResponse struct { - Event string `json:"event"` - Argument SubscriptionInfo `json:"arg,,omitempty"` - Code int `json:"code,string,omitempty"` - Msg string `json:"msg,omitempty"` +// OperationResponse holds common operation identification +type OperationResponse struct { + ID string `json:"id"` + Operation string `json:"op"` + Code string `json:"code"` + Msg string `json:"msg"` } -// WSInstrumentsResponse represents instrument subscription response. -type WSInstrumentsResponse struct { - Arguments []SubscriptionInfo `json:"args"` - Data []Instrument `json:"data"` +// WsPlaceOrderResponse place order response thought the websocket connection +type WsPlaceOrderResponse struct { + OperationResponse + Data []OrderData `json:"data"` } -// WSMarketDataResponse represents market data response and it's arguments. -type WSMarketDataResponse struct { - Arguments []SubscriptionInfo `json:"args"` - Data []TickerResponse `json:"data"` -} - -// WSPlaceOrderData holds websocket order information. -type WSPlaceOrderData struct { - ClientOrderID string `json:"clOrdId,omitempty"` - Currency string `json:"ccy,omitempty"` - Tag string `json:"tag,omitempty"` - PositionSide string `json:"posSide,omitempty"` - ExpiryTime int64 `json:"expTime,string,omitempty"` - BanAmend bool `json:"banAmend,omitempty"` - Side string `json:"side"` - InstrumentID string `json:"instId"` - TradeMode string `json:"tdMode"` - OrderType string `json:"ordType"` - Size float64 `json:"sz"` - Price types.Number `json:"px,omitempty"` - ReduceOnly bool `json:"reduceOnly,string,omitempty"` - TargetCurrency string `json:"tgtCurrency,omitempty"` -} - -// WSPlaceOrder holds the websocket place order input data. -type WSPlaceOrder struct { - ID string `json:"id"` - Operation string `json:"op"` - Arguments []WSPlaceOrderData `json:"args"` -} - -// WSOrderResponse place order response thought the websocket connection. -type WSOrderResponse struct { - ID string `json:"id"` - Operation string `json:"op"` - Data []OrderData `json:"data"` - Code string `json:"code,omitempty"` - Msg string `json:"msg,omitempty"` -} - -// WebsocketDataResponse represents all pushed websocket data coming thought the websocket connection -type WebsocketDataResponse struct { - Argument SubscriptionInfo `json:"arg"` - Action string `json:"action"` - Data []interface{} `json:"data"` +// SpreadOrderInfo holds spread order response information +type SpreadOrderInfo struct { + ClientOrderID string `json:"clOrdId"` + OrderID string `json:"ordId"` + Tag string `json:"tag"` + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` } type wsRequestInfo struct { @@ -2308,10 +3143,10 @@ type wsRequestInfo struct { } type wsIncomingData struct { - Event string `json:"event,omitempty"` - Argument SubscriptionInfo `json:"arg,omitempty"` - Code string `json:"code,omitempty"` - Msg string `json:"msg,omitempty"` + Event string `json:"event,omitempty"` + Argument SubscriptionInfo `json:"arg,omitempty"` + StatusCode string `json:"code,omitempty"` + Message string `json:"msg,omitempty"` // For Websocket Trading Endpoints websocket responses ID string `json:"id,omitempty"` @@ -2320,102 +3155,59 @@ type wsIncomingData struct { } // copyToPlaceOrderResponse returns WSPlaceOrderResponse struct instance -func (w *wsIncomingData) copyToPlaceOrderResponse() (*WSOrderResponse, error) { +func (w *wsIncomingData) copyToPlaceOrderResponse() (*WsPlaceOrderResponse, error) { if len(w.Data) == 0 { - return nil, errEmptyPlaceOrderResponse + return nil, common.ErrNoResponse } var placeOrds []OrderData err := json.Unmarshal(w.Data, &placeOrds) if err != nil { return nil, err } - return &WSOrderResponse{ - Operation: w.Operation, - ID: w.ID, - Data: placeOrds, + return &WsPlaceOrderResponse{ + OperationResponse: OperationResponse{ + Operation: w.Operation, + ID: w.ID, + }, + Data: placeOrds, }, nil } -// WSInstrumentResponse represents websocket instruments push message. +// copyResponseToInterface unmarshals the response data into the dataHolder interface. +func (w *wsIncomingData) copyResponseToInterface(dataHolder interface{}) error { + rv := reflect.ValueOf(dataHolder) + if rv.Kind() != reflect.Pointer { + return errInvalidResponseParam + } + if len(w.Data) == 0 { + return common.ErrNoResponse + } + return json.Unmarshal(w.Data, &[]any{dataHolder}) +} + +// WSInstrumentResponse represents websocket instruments push message type WSInstrumentResponse struct { Argument SubscriptionInfo `json:"arg"` Data []Instrument `json:"data"` } -// WSTickerResponse represents websocket ticker response. +// WSTickerResponse represents websocket ticker response type WSTickerResponse struct { Argument SubscriptionInfo `json:"arg"` Data []TickerResponse `json:"data"` } -// WSCandlestickData represents candlestick data coming through the web socket channels -type WSCandlestickData struct { - Timestamp time.Time `json:"ts"` - OpenPrice float64 `json:"o"` - HighestPrice float64 `json:"p"` - LowestPrice float64 `json:"l"` - ClosePrice float64 `json:"c"` - TradingVolume float64 `json:"vol"` - TradingVolumeWithCurrencyUnit float64 `json:"volCcy"` -} - -// WSCandlestickResponse represents candlestick response of with list of candlestick and -type WSCandlestickResponse struct { - Argument SubscriptionInfo `json:"arg"` - Data []WSCandlestickData `json:"data"` -} - -// WSOpenInterestResponse represents an open interest instance. +// WSOpenInterestResponse represents an open interest instance type WSOpenInterestResponse struct { Argument SubscriptionInfo `json:"arg"` Data []OpenInterest `json:"data"` } -// WSTradeData websocket trade data response. -type WSTradeData struct { - InstrumentID string `json:"instId"` - TradeID string `json:"tradeId"` - Price types.Number `json:"px"` - Size types.Number `json:"sz"` - Side order.Side `json:"side"` - Timestamp okxUnixMilliTime `json:"ts"` -} - -// WSPlaceOrderInput place order input variables as a json. -type WSPlaceOrderInput struct { - Side order.Side `json:"side"` - InstrumentID string `json:"instId"` - TradeMode string `json:"tdMode"` - OrderType string `json:"ordType"` - Size types.Number `json:"sz"` - Currency string `json:"ccy"` - ClientOrderID string `json:"clOrdId,omitempty"` - Tag string `json:"tag,omitempty"` - PositionSide string `json:"posSide,omitempty"` - Price types.Number `json:"px,omitempty"` - ReduceOnly bool `json:"reduceOnly,omitempty"` - TargetCurrency string `json:"tgtCcy"` -} - -// WsPlaceOrderInput for all websocket request inputs. -type WsPlaceOrderInput struct { - ID string `json:"id"` - Operation string `json:"op"` - Arguments []PlaceOrderRequestParam `json:"args"` -} - -// WsCancelOrderInput websocket cancel order request -type WsCancelOrderInput struct { - ID string `json:"id"` - Operation string `json:"op"` - Arguments []CancelOrderRequestParam `json:"args"` -} - -// WsAmendOrderInput websocket handler amend Order response -type WsAmendOrderInput struct { - ID string `json:"id"` - Operation string `json:"op"` - Arguments []AmendOrderRequestParams `json:"args"` +// WsOperationInput for all websocket request inputs +type WsOperationInput struct { + ID string `json:"id"` + Operation string `json:"op"` + Arguments any `json:"args"` } // WsOrderActionResponse holds websocket response Amendment request @@ -2429,12 +3221,12 @@ type WsOrderActionResponse struct { func (a *WsOrderActionResponse) populateFromIncomingData(incoming *wsIncomingData) error { if incoming == nil { - return errNilArgument + return common.ErrNilPointer } a.ID = incoming.ID - a.Code = incoming.Code + a.Code = incoming.StatusCode a.Operation = incoming.Operation - a.Msg = incoming.Msg + a.Msg = incoming.Message return nil } @@ -2444,21 +3236,13 @@ type SubscriptionOperationInput struct { Arguments []SubscriptionInfo `json:"args"` } -// SubscriptionOperationResponse holds account subscription response thought the websocket channel. -type SubscriptionOperationResponse struct { - Event string `json:"event"` - Argument *SubscriptionInfo `json:"arg,omitempty"` - Code string `json:"code,omitempty"` - Msg string `json:"msg,omitempty"` -} - -// WsAccountChannelPushData holds the websocket push data following the subscription. +// WsAccountChannelPushData holds the websocket push data following the subscription type WsAccountChannelPushData struct { Argument SubscriptionInfo `json:"arg"` Data []Account `json:"data,omitempty"` } -// WsPositionResponse represents pushed position data through the websocket channel. +// WsPositionResponse represents pushed position data through the websocket channel type WsPositionResponse struct { Argument SubscriptionInfo `json:"arg"` Arguments []AccountPosition `json:"data"` @@ -2466,41 +3250,41 @@ type WsPositionResponse struct { // PositionDataDetail position data information for the websocket push data type PositionDataDetail struct { - PositionID string `json:"posId"` - TradeID string `json:"tradeId"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - MarginMode string `json:"mgnMode"` - PositionSide string `json:"posSide"` - Position string `json:"pos"` - Currency string `json:"ccy"` - PositionCurrency string `json:"posCcy"` - AveragePrice string `json:"avgPx"` - UpdateTime okxUnixMilliTime `json:"uTIme"` -} - -// BalanceData represents currency and it's Cash balance with the update time. + PositionID string `json:"posId"` + TradeID string `json:"tradeId"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + MarginMode string `json:"mgnMode"` + PositionSide string `json:"posSide"` + Position string `json:"pos"` + Currency string `json:"ccy"` + PositionCurrency string `json:"posCcy"` + AveragePrice types.Number `json:"avgPx"` + UpdateTime types.Time `json:"uTIme"` +} + +// BalanceData represents currency and it's Cash balance with the update time type BalanceData struct { - Currency string `json:"ccy"` - CashBalance string `json:"cashBal"` - UpdateTime okxUnixMilliTime `json:"uTime"` + Currency string `json:"ccy"` + CashBalance types.Number `json:"cashBal"` + UpdateTime types.Time `json:"uTime"` } -// BalanceAndPositionData represents balance and position data with the push time. +// BalanceAndPositionData represents balance and position data with the push time type BalanceAndPositionData struct { - PushTime okxUnixMilliTime `json:"pTime"` + PushTime types.Time `json:"pTime"` EventType string `json:"eventType"` BalanceData []BalanceData `json:"balData"` PositionData []PositionDataDetail `json:"posData"` } -// WsBalanceAndPosition websocket push data for lis of BalanceAndPosition information. +// WsBalanceAndPosition websocket push data for lis of BalanceAndPosition information type WsBalanceAndPosition struct { Argument SubscriptionInfo `json:"arg"` Data []BalanceAndPositionData `json:"data"` } -// WsOrder represents a websocket order. +// WsOrder represents a websocket order type WsOrder struct { PendingOrderItem AmendResult string `json:"amendResult"` @@ -2521,7 +3305,7 @@ type WsOrderResponse struct { Data []WsOrder `json:"data"` } -// WsAlgoOrder algo order detailed data. +// WsAlgoOrder algo order detailed data type WsAlgoOrder struct { Argument SubscriptionInfo `json:"arg"` Data []WsAlgoOrderDetail `json:"data"` @@ -2529,38 +3313,38 @@ type WsAlgoOrder struct { // WsAlgoOrderDetail algo order response pushed through the websocket conn type WsAlgoOrderDetail struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - OrderID string `json:"ordId"` - Currency string `json:"ccy"` - AlgoID string `json:"algoId"` - Price string `json:"px"` - Size string `json:"sz"` - TradeMode string `json:"tdMode"` - TargetCurrency string `json:"tgtCcy"` - NotionalUsd string `json:"notionalUsd"` - OrderType string `json:"ordType"` - Side order.Side `json:"side"` - PositionSide string `json:"posSide"` - State string `json:"state"` - Leverage string `json:"lever"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` - TakeProfitOrdPrice string `json:"tpOrdPx"` - StopLossTriggerPrice string `json:"slTriggerPx"` - StopLossTriggerPriceType string `json:"slTriggerPxType"` - TriggerPrice string `json:"triggerPx"` - TriggerPriceType string `json:"triggerPxType"` - OrderPrice types.Number `json:"ordPx"` - ActualSize string `json:"actualSz"` - ActualPrice string `json:"actualPx"` - Tag string `json:"tag"` - ActualSide string `json:"actualSide"` - TriggerTime okxUnixMilliTime `json:"triggerTime"` - CreationTime okxUnixMilliTime `json:"cTime"` -} - -// WsAdvancedAlgoOrder advanced algo order response. + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + OrderID string `json:"ordId"` + Currency string `json:"ccy"` + AlgoID string `json:"algoId"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + TradeMode string `json:"tdMode"` + TargetCurrency string `json:"tgtCcy"` + NotionalUsd string `json:"notionalUsd"` + OrderType string `json:"ordType"` + Side order.Side `json:"side"` + PositionSide string `json:"posSide"` + State string `json:"state"` + Leverage string `json:"lever"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` + TakeProfitTriggerPriceType string `json:"tpTriggerPxType"` + TakeProfitOrdPrice types.Number `json:"tpOrdPx"` + StopLossTriggerPrice types.Number `json:"slTriggerPx"` + StopLossTriggerPriceType string `json:"slTriggerPxType"` + TriggerPrice types.Number `json:"triggerPx"` + TriggerPriceType string `json:"triggerPxType"` + OrderPrice types.Number `json:"ordPx"` + ActualSize types.Number `json:"actualSz"` + ActualPrice types.Number `json:"actualPx"` + Tag string `json:"tag"` + ActualSide string `json:"actualSide"` + TriggerTime types.Time `json:"triggerTime"` + CreationTime types.Time `json:"cTime"` +} + +// WsAdvancedAlgoOrder advanced algo order response type WsAdvancedAlgoOrder struct { Argument SubscriptionInfo `json:"arg"` Data []WsAdvancedAlgoOrderDetail `json:"data"` @@ -2568,41 +3352,41 @@ type WsAdvancedAlgoOrder struct { // WsAdvancedAlgoOrderDetail advanced algo order response pushed through the websocket conn type WsAdvancedAlgoOrderDetail struct { - ActualPrice string `json:"actualPx"` - ActualSide string `json:"actualSide"` - ActualSize string `json:"actualSz"` - AlgoID string `json:"algoId"` - Currency string `json:"ccy"` - Count string `json:"count"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Leverage string `json:"lever"` - NotionalUsd string `json:"notionalUsd"` - OrderPrice string `json:"ordPx"` - OrdType string `json:"ordType"` - PositionSide string `json:"posSide"` - PriceLimit string `json:"pxLimit"` - PriceSpread string `json:"pxSpread"` - PriceVariation string `json:"pxVar"` - Side order.Side `json:"side"` - StopLossOrderPrice string `json:"slOrdPx"` - StopLossTriggerPrice string `json:"slTriggerPx"` - State string `json:"state"` - Size string `json:"sz"` - SizeLimit string `json:"szLimit"` - TradeMode string `json:"tdMode"` - TimeInterval string `json:"timeInterval"` - TakeProfitOrderPrice string `json:"tpOrdPx"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - Tag string `json:"tag"` - TriggerPrice string `json:"triggerPx"` - CallbackRatio string `json:"callbackRatio"` - CallbackSpread string `json:"callbackSpread"` - ActivePrice string `json:"activePx"` - MoveTriggerPrice string `json:"moveTriggerPx"` - CreationTime okxUnixMilliTime `json:"cTime"` - PushTime okxUnixMilliTime `json:"pTime"` - TriggerTime okxUnixMilliTime `json:"triggerTime"` + ActualPrice types.Number `json:"actualPx"` + ActualSide string `json:"actualSide"` + ActualSize types.Number `json:"actualSz"` + AlgoID string `json:"algoId"` + Currency string `json:"ccy"` + Count string `json:"count"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage types.Number `json:"lever"` + NotionalUSD types.Number `json:"notionalUsd"` + OrderPrice types.Number `json:"ordPx"` + OrdType string `json:"ordType"` + PositionSide string `json:"posSide"` + PriceLimit types.Number `json:"pxLimit"` + PriceSpread types.Number `json:"pxSpread"` + PriceVariation string `json:"pxVar"` + Side order.Side `json:"side"` + StopLossOrderPrice types.Number `json:"slOrdPx"` + StopLossTriggerPrice types.Number `json:"slTriggerPx"` + State string `json:"state"` + Size types.Number `json:"sz"` + SizeLimit types.Number `json:"szLimit"` + TradeMode string `json:"tdMode"` + TimeInterval string `json:"timeInterval"` + TakeProfitOrderPrice types.Number `json:"tpOrdPx"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` + Tag string `json:"tag"` + TriggerPrice types.Number `json:"triggerPx"` + CallbackRatio types.Number `json:"callbackRatio"` + CallbackSpread string `json:"callbackSpread"` + ActivePrice types.Number `json:"activePx"` + MoveTriggerPrice types.Number `json:"moveTriggerPx"` + CreationTime types.Time `json:"cTime"` + PushTime types.Time `json:"pTime"` + TriggerTime types.Time `json:"triggerTime"` } // WsGreeks greeks push data with the subscription info through websocket channel @@ -2613,35 +3397,35 @@ type WsGreeks struct { // WsGreekData greeks push data through websocket channel type WsGreekData struct { - ThetaBS string `json:"thetaBS"` - ThetaPA string `json:"thetaPA"` - DeltaBS string `json:"deltaBS"` - DeltaPA string `json:"deltaPA"` - GammaBS string `json:"gammaBS"` - GammaPA string `json:"gammaPA"` - VegaBS string `json:"vegaBS"` - VegaPA string `json:"vegaPA"` - Currency string `json:"ccy"` - Timestamp okxUnixMilliTime `json:"ts"` -} - -// WsRfq represents websocket push data for "rfqs" subscription -type WsRfq struct { + ThetaBS string `json:"thetaBS"` + ThetaPA string `json:"thetaPA"` + DeltaBS string `json:"deltaBS"` + DeltaPA string `json:"deltaPA"` + GammaBS string `json:"gammaBS"` + GammaPA string `json:"gammaPA"` + VegaBS string `json:"vegaBS"` + VegaPA string `json:"vegaPA"` + Currency string `json:"ccy"` + Timestamp types.Time `json:"ts"` +} + +// WsRFQ represents websocket push data for "rfqs" subscription +type WsRFQ struct { Argument SubscriptionInfo `json:"arg"` - Data []WsRfqData `json:"data"` + Data []WsRFQData `json:"data"` } -// WsRfqData represents rfq order response data streamed through the websocket channel -type WsRfqData struct { +// WsRFQData represents rfq order response data streamed through the websocket channel +type WsRFQData struct { CreationTime time.Time `json:"cTime"` UpdateTime time.Time `json:"uTime"` TraderCode string `json:"traderCode"` - RfqID string `json:"rfqId"` - ClientRfqID string `json:"clRfqId"` + RFQID string `json:"rfqId"` + ClientRFQID string `json:"clRfqId"` State string `json:"state"` ValidUntil string `json:"validUntil"` Counterparties []string `json:"counterparties"` - Legs []RfqOrderLeg `json:"legs"` + Legs []RFQOrderLeg `json:"legs"` } // WsQuote represents websocket push data for "quotes" subscription @@ -2652,16 +3436,16 @@ type WsQuote struct { // WsQuoteData represents a single quote order information type WsQuoteData struct { - ValidUntil okxUnixMilliTime `json:"validUntil"` - UpdatedTime okxUnixMilliTime `json:"uTime"` - CreationTime okxUnixMilliTime `json:"cTime"` - Legs []OrderLeg `json:"legs"` - QuoteID string `json:"quoteId"` - RfqID string `json:"rfqId"` - TraderCode string `json:"traderCode"` - QuoteSide string `json:"quoteSide"` - State string `json:"state"` - ClientQuoteID string `json:"clQuoteId"` + ValidUntil types.Time `json:"validUntil"` + UpdatedTime types.Time `json:"uTime"` + CreationTime types.Time `json:"cTime"` + Legs []OrderLeg `json:"legs"` + QuoteID string `json:"quoteId"` + RFQID string `json:"rfqId"` + TraderCode string `json:"traderCode"` + QuoteSide string `json:"quoteSide"` + State string `json:"state"` + ClientQuoteID string `json:"clQuoteId"` } // WsStructureBlocTrade represents websocket push data for "struc-block-trades" subscription @@ -2672,15 +3456,15 @@ type WsStructureBlocTrade struct { // WsBlockTradeResponse represents a structure block order information type WsBlockTradeResponse struct { - CreationTime okxUnixMilliTime `json:"cTime"` - RfqID string `json:"rfqId"` - ClientRfqID string `json:"clRfqId"` - QuoteID string `json:"quoteId"` - ClientQuoteID string `json:"clQuoteId"` - BlockTradeID string `json:"blockTdId"` - TakerTraderCode string `json:"tTraderCode"` - MakerTraderCode string `json:"mTraderCode"` - Legs []OrderLeg `json:"legs"` + CreationTime types.Time `json:"cTime"` + RFQID string `json:"rfqId"` + ClientRFQID string `json:"clRfqId"` + QuoteID string `json:"quoteId"` + ClientQuoteID string `json:"clQuoteId"` + BlockTradeID string `json:"blockTdId"` + TakerTraderCode string `json:"tTraderCode"` + MakerTraderCode string `json:"mTraderCode"` + Legs []OrderLeg `json:"legs"` } // WsSpotGridAlgoOrder represents websocket push data for "struc-block-trades" subscription @@ -2689,50 +3473,50 @@ type WsSpotGridAlgoOrder struct { Data []SpotGridAlgoData `json:"data"` } -// SpotGridAlgoData represents spot grid algo orders. +// SpotGridAlgoData represents spot grid algo orders type SpotGridAlgoData struct { - AlgoID string `json:"algoId"` - AlgoOrderType string `json:"algoOrdType"` - AnnualizedRate string `json:"annualizedRate"` - ArbitrageNumber string `json:"arbitrageNum"` - BaseSize string `json:"baseSz"` + AlgoID string `json:"algoId"` + AlgoOrderType string `json:"algoOrdType"` + AnnualizedRate types.Number `json:"annualizedRate"` + ArbitrageNumber types.Number `json:"arbitrageNum"` + BaseSize types.Number `json:"baseSz"` // Algo order stop reason 0: None 1: Manual stop 2: Take profit // 3: Stop loss 4: Risk control 5: delivery - CancelType string `json:"cancelType"` - CurBaseSize string `json:"curBaseSz"` - CurQuoteSize string `json:"curQuoteSz"` - FloatProfit string `json:"floatProfit"` - GridNumber string `json:"gridNum"` - GridProfit string `json:"gridProfit"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Investment string `json:"investment"` - MaximumPrice string `json:"maxPx"` - MinimumPrice string `json:"minPx"` - PerMaximumProfitRate string `json:"perMaxProfitRate"` - PerMinimumProfitRate string `json:"perMinProfitRate"` - ProfitAndLossRatio string `json:"pnlRatio"` - QuoteSize string `json:"quoteSz"` - RunPrice string `json:"runPx"` - RunType string `json:"runType"` - SingleAmount string `json:"singleAmt"` - StopLossTriggerPrice string `json:"slTriggerPx"` - State string `json:"state"` + CancelType string `json:"cancelType"` + CurBaseSize types.Number `json:"curBaseSz"` + CurQuoteSize types.Number `json:"curQuoteSz"` + FloatProfit types.Number `json:"floatProfit"` + GridNumber string `json:"gridNum"` + GridProfit types.Number `json:"gridProfit"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Investment types.Number `json:"investment"` + MaximumPrice types.Number `json:"maxPx"` + MinimumPrice types.Number `json:"minPx"` + PerMaximumProfitRate types.Number `json:"perMaxProfitRate"` + PerMinimumProfitRate types.Number `json:"perMinProfitRate"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + QuoteSize types.Number `json:"quoteSz"` + RunPrice types.Number `json:"runPx"` + RunType string `json:"runType"` + SingleAmount types.Number `json:"singleAmt"` + StopLossTriggerPrice types.Number `json:"slTriggerPx"` + State string `json:"state"` // Stop result of spot grid // 0: default, 1: Successful selling of currency at market price, // -1: Failed to sell currency at market price StopResult string `json:"stopResult"` // Stop type Spot grid 1: Sell base currency 2: Keep base currency // Contract grid 1: Market Close All positions 2: Keep positions - StopType string `json:"stopType"` - TotalAnnualizedRate string `json:"totalAnnualizedRate"` - TotalProfitAndLoss string `json:"totalPnl"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - TradeNum string `json:"tradeNum"` - TriggerTime okxUnixMilliTime `json:"triggerTime"` - CreationTime okxUnixMilliTime `json:"cTime"` - PushTime okxUnixMilliTime `json:"pTime"` - UpdateTime okxUnixMilliTime `json:"uTime"` + StopType string `json:"stopType"` + TotalAnnualizedRate types.Number `json:"totalAnnualizedRate"` + TotalProfitAndLoss types.Number `json:"totalPnl"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` + TradeNum types.Number `json:"tradeNum"` + TriggerTime types.Time `json:"triggerTime"` + CreationTime types.Time `json:"cTime"` + PushTime types.Time `json:"pTime"` + UpdateTime types.Time `json:"uTime"` } // WsContractGridAlgoOrder represents websocket push data for "grid-orders-contract" subscription @@ -2743,80 +3527,48 @@ type WsContractGridAlgoOrder struct { // ContractGridAlgoOrder represents contract grid algo order type ContractGridAlgoOrder struct { - ActualLever string `json:"actualLever"` - AlgoID string `json:"algoId"` - AlgoOrderType string `json:"algoOrdType"` - AnnualizedRate string `json:"annualizedRate"` - ArbitrageNumber string `json:"arbitrageNum"` - BasePosition bool `json:"basePos"` - CancelType string `json:"cancelType"` - Direction string `json:"direction"` - Eq string `json:"eq"` - FloatProfit string `json:"floatProfit"` - GridQuantity string `json:"gridNum"` - GridProfit string `json:"gridProfit"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Investment string `json:"investment"` - Leverage string `json:"lever"` - LiqPrice string `json:"liqPx"` - MaxPrice string `json:"maxPx"` - MinPrice string `json:"minPx"` - CreationTime okxUnixMilliTime `json:"cTime"` - PushTime okxUnixMilliTime `json:"pTime"` - PerMaxProfitRate string `json:"perMaxProfitRate"` - PerMinProfitRate string `json:"perMinProfitRate"` - ProfitAndLossRatio string `json:"pnlRatio"` - RunPrice string `json:"runPx"` - RunType string `json:"runType"` - SingleAmount string `json:"singleAmt"` - SlTriggerPx string `json:"slTriggerPx"` - State string `json:"state"` - StopType string `json:"stopType"` - Size string `json:"sz"` - Tag string `json:"tag"` - TotalAnnualizedRate string `json:"totalAnnualizedRate"` - TotalProfitAndLoss string `json:"totalPnl"` - TakeProfitTriggerPrice string `json:"tpTriggerPx"` - TradeNumber string `json:"tradeNum"` - TriggerTime string `json:"triggerTime"` - UpdateTime string `json:"uTime"` - Underlying string `json:"uly"` -} - -// WsGridPosition represents websocket push data for "grid-positions" subscription -type WsGridPosition struct { - Argument SubscriptionInfo `json:"arg"` - Data []GridPositionData `json:"data"` -} - -// GridPositionData represents a position data -type GridPositionData struct { - AutoDeleveraging string `json:"adl"` - AlgoID string `json:"algoId"` - AveragePrice string `json:"avgPx"` - Currency string `json:"ccy"` - InitialMarginRequirement string `json:"imr"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Last string `json:"last"` - Leverage string `json:"lever"` - LiquidationPrice string `json:"liqPx"` - MarkPrice string `json:"markPx"` - MarginMode string `json:"mgnMode"` - MarginRatio string `json:"mgnRatio"` - MaintenanceMarginRequirement string `json:"mmr"` - NotionalUsd string `json:"notionalUsd"` - QuantityOfPositions string `json:"pos"` - PositionSide string `json:"posSide"` - UnrealizedProfitAndLoss string `json:"upl"` - UnrealizedProfitAndLossRatio string `json:"uplRatio"` - PushTime okxUnixMilliTime `json:"pTime"` - UpdateTime okxUnixMilliTime `json:"uTime"` - CreationTime okxUnixMilliTime `json:"cTime"` -} - -// WsGridSubOrderData to retrieve grid sub orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing order. + ActualLever string `json:"actualLever"` + AlgoID string `json:"algoId"` + AlgoOrderType string `json:"algoOrdType"` + AnnualizedRate types.Number `json:"annualizedRate"` + ArbitrageNumber types.Number `json:"arbitrageNum"` + BasePosition bool `json:"basePos"` + CancelType string `json:"cancelType"` + Direction string `json:"direction"` + Equity types.Number `json:"eq"` + FloatProfit types.Number `json:"floatProfit"` + GridQuantity types.Number `json:"gridNum"` + GridProfit types.Number `json:"gridProfit"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Investment string `json:"investment"` + Leverage string `json:"lever"` + LiqPrice types.Number `json:"liqPx"` + MaxPrice types.Number `json:"maxPx"` + MinPrice types.Number `json:"minPx"` + CreationTime types.Time `json:"cTime"` + PushTime types.Time `json:"pTime"` + PerMaxProfitRate types.Number `json:"perMaxProfitRate"` + PerMinProfitRate types.Number `json:"perMinProfitRate"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + RunPrice types.Number `json:"runPx"` + RunType string `json:"runType"` + SingleAmount types.Number `json:"singleAmt"` + SlTriggerPrice types.Number `json:"slTriggerPx"` + State string `json:"state"` + StopType string `json:"stopType"` + Size types.Number `json:"sz"` + Tag string `json:"tag"` + TotalAnnualizedRate string `json:"totalAnnualizedRate"` + TotalProfitAndLoss types.Number `json:"totalPnl"` + TakeProfitTriggerPrice types.Number `json:"tpTriggerPx"` + TradeNumber string `json:"tradeNum"` + TriggerTime types.Time `json:"triggerTime"` + UpdateTime types.Time `json:"uTime"` + Underlying string `json:"uly"` +} + +// WsGridSubOrderData to retrieve grid sub orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing order type WsGridSubOrderData struct { Argument SubscriptionInfo `json:"arg"` Data []GridSubOrderData `json:"data"` @@ -2824,30 +3576,30 @@ type WsGridSubOrderData struct { // GridSubOrderData represents a single sub order detailed info type GridSubOrderData struct { - AccumulatedFillSize string `json:"accFillSz"` - AlgoID string `json:"algoId"` - AlgoOrderType string `json:"algoOrdType"` - AveragePrice string `json:"avgPx"` - CreationTime string `json:"cTime"` - ContractValue string `json:"ctVal"` - Fee string `json:"fee"` - FeeCurrency string `json:"feeCcy"` - GroupID string `json:"groupId"` - InstrumentID string `json:"instId"` - InstrumentType string `json:"instType"` - Leverage string `json:"lever"` - OrderID string `json:"ordId"` - OrderType string `json:"ordType"` - PushTime okxUnixMilliTime `json:"pTime"` - ProfitAdLoss string `json:"pnl"` - PositionSide string `json:"posSide"` - Price string `json:"px"` - Side order.Side `json:"side"` - State string `json:"state"` - Size string `json:"sz"` - Tag string `json:"tag"` - TradeMode string `json:"tdMode"` - UpdateTime okxUnixMilliTime `json:"uTime"` + AccumulatedFillSize types.Number `json:"accFillSz"` + AlgoID string `json:"algoId"` + AlgoOrderType string `json:"algoOrdType"` + AveragePrice types.Number `json:"avgPx"` + CreationTime types.Time `json:"cTime"` + ContractValue string `json:"ctVal"` + Fee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` + GroupID string `json:"groupId"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage types.Number `json:"lever"` + OrderID string `json:"ordId"` + OrderType string `json:"ordType"` + PushTime types.Time `json:"pTime"` + ProfitAndLoss types.Number `json:"pnl"` + PositionSide string `json:"posSide"` + Price types.Number `json:"px"` + Side order.Side `json:"side"` + State string `json:"state"` + Size types.Number `json:"sz"` + Tag string `json:"tag"` + TradeMode string `json:"tdMode"` + UpdateTime types.Time `json:"uTime"` } // WsTradeOrder represents a trade push data response as a result subscription to "trades" channel @@ -2868,7 +3620,7 @@ type WsDeliveryEstimatedPrice struct { Data []DeliveryEstimatedPrice `json:"data"` } -// CandlestickMarkPrice represents candlestick mark price push data as a result of subscription to "mark-price-candle*" channel. +// CandlestickMarkPrice represents candlestick mark price push data as a result of subscription to "mark-price-candle*" channel type CandlestickMarkPrice struct { Timestamp time.Time `json:"ts"` OpenPrice float64 `json:"o"` @@ -2884,12 +3636,34 @@ type WsOrderBook struct { Data []WsOrderBookData `json:"data"` } -// WsOrderBookData represents a book order push data. +// WsOptionTrades represents option trade data +type WsOptionTrades struct { + Arg SubscriptionInfo `json:"arg"` + Data []PublicTrade `json:"data"` +} + +// PublicTrade represents public trade item for option, block, and others +type PublicTrade struct { + FillVolume types.Number `json:"fillVol"` + ForwardPrice types.Number `json:"fwdPx"` + IndexPrice types.Number `json:"idxPx"` + InstrumentFamily string `json:"instFamily"` + InstrumentID string `json:"instId"` + MarkPrice types.Number `json:"markPx"` + OptionType string `json:"optType"` + Price types.Number `json:"px"` + Side string `json:"side"` + Size types.Number `json:"sz"` + TradeID string `json:"tradeId"` + Timestamp types.Time `json:"ts"` +} + +// WsOrderBookData represents a book order push data type WsOrderBookData struct { - Asks [][4]string `json:"asks"` - Bids [][4]string `json:"bids"` - Timestamp okxUnixMilliTime `json:"ts"` - Checksum int32 `json:"checksum,omitempty"` + Asks [][4]types.Number `json:"asks"` + Bids [][4]types.Number `json:"bids"` + Timestamp types.Time `json:"ts"` + Checksum int32 `json:"checksum,omitempty"` } // WsOptionSummary represents option summary @@ -2898,7 +3672,7 @@ type WsOptionSummary struct { Data []OptionMarketDataResponse `json:"data"` } -// WsFundingRate represents websocket push data funding rate response. +// WsFundingRate represents websocket push data funding rate response type WsFundingRate struct { Argument SubscriptionInfo `json:"arg"` Data []FundingRateResponse `json:"data"` @@ -2918,24 +3692,46 @@ type WsSystemStatusResponse struct { // WsPublicTradesResponse represents websocket push data of structured block trades as a result of subscription to "public-struc-block-trades" type WsPublicTradesResponse struct { - Argument SubscriptionInfo `json:"arg"` - Data []PublicBlockTradesResponse `json:"data"` + Argument SubscriptionInfo `json:"arg"` + Data []PublicTradesResponse `json:"data"` } -// WsBlockTicker represents websocket push data as a result of subscription to channel "block-tickers". +// WsBlockTicker represents websocket push data as a result of subscription to channel "block-tickers" type WsBlockTicker struct { Argument SubscriptionInfo `json:"arg"` Data []BlockTicker `json:"data"` } +// PublicBlockTrades holds public block trades +type PublicBlockTrades struct { + Arg SubscriptionInfo `json:"arg"` + Data []PublicTrade `json:"data"` +} + // PMLimitationResponse represents portfolio margin mode limitation for specific underlying type PMLimitationResponse struct { - MaximumSize types.Number `json:"maxSz"` - PositionType string `json:"postType"` - Underlying string `json:"uly"` + MaximumSize types.Number `json:"maxSz"` + PositionType string `json:"postType"` + Underlying string `json:"uly"` + InstrumentFamily string `json:"instFamily"` +} + +// RiskOffsetType represents risk offset type value +type RiskOffsetType struct { + Type string `json:"type"` } -// EasyConvertDetail represents easy convert currencies list and their detail. +// AutoLoan holds auto loan information +type AutoLoan struct { + AutoLoan bool `json:"autoLoan"` +} + +// AccountMode holds account mode +type AccountMode struct { + IsoMode string `json:"isoMode"` +} + +// EasyConvertDetail represents easy convert currencies list and their detail type EasyConvertDetail struct { FromData []EasyConvertFromData `json:"fromData"` ToCurrency []string `json:"toCcy"` @@ -2951,35 +3747,18 @@ type EasyConvertFromData struct { type PlaceEasyConvertParam struct { FromCurrency []string `json:"fromCcy"` ToCurrency string `json:"toCcy"` + Source string `json:"source,omitempty"` } -// EasyConvertItem represents easy convert place order response. +// EasyConvertItem represents easy convert place order response type EasyConvertItem struct { - FilFromSize types.Number `json:"fillFromSz"` - FillToSize types.Number `json:"fillToSz"` - FromCurrency string `json:"fromCcy"` - Status string `json:"status"` - ToCurrency string `json:"toCcy"` - UpdateTime okxUnixMilliTime `json:"uTime"` -} - -// OneClickRepayCurrencyItem represents debt currency data and repay currencies. -type OneClickRepayCurrencyItem struct { - DebtData []CurrencyDebtAmount `json:"debtData"` - DebtType string `json:"debtType"` - RepayData []CurrencyRepayAmount `json:"repayData"` -} - -// CurrencyDebtAmount represents debt currency data -type CurrencyDebtAmount struct { - DebtAmount types.Number `json:"debtAmt"` - DebtCurrency string `json:"debtCcy"` -} - -// CurrencyRepayAmount represents rebat currency amount. -type CurrencyRepayAmount struct { - RepayAmount types.Number `json:"repayAmt"` - RepayCurrency string `json:"repayCcy"` + FilFromSize types.Number `json:"fillFromSz"` + FillToSize types.Number `json:"fillToSz"` + FromCurrency string `json:"fromCcy"` + Status string `json:"status"` + ToCurrency string `json:"toCcy"` + UpdateTime types.Time `json:"uTime"` + Account string `json:"acct"` } // TradeOneClickRepayParam represents click one repay param @@ -2988,15 +3767,28 @@ type TradeOneClickRepayParam struct { RepayCurrency string `json:"repayCcy"` } -// CurrencyOneClickRepay represents one click repay currency +// CurrencyOneClickRepay represents the currency used for one-click repayment. type CurrencyOneClickRepay struct { DebtCurrency string `json:"debtCcy"` FillFromSize types.Number `json:"fillFromSz"` FillRepaySize types.Number `json:"fillRepaySz"` + FillDebtSize types.Number `json:"fillDebtSz"` FillToSize types.Number `json:"fillToSz"` RepayCurrency string `json:"repayCcy"` Status string `json:"status"` - UpdateTime time.Time `json:"uTime"` + UpdateTime types.Time `json:"uTime"` +} + +// CancelMMPResponse holds the result of a cancel MMP response. +type CancelMMPResponse struct { + Result bool `json:"result"` +} + +// CancelResponse represents a pending order cancellation response +type CancelResponse struct { + TriggerTime types.Time `json:"triggerTime"` // The time the cancellation is triggered. triggerTime=0 means Cancel All After is disabled. + Tag string `json:"tag"` + Timestamp types.Time `json:"ts"` // The time the request is sent. } // SetQuoteProductParam represents set quote product request param @@ -3030,20 +3822,20 @@ type SubAccountAPIKeyParam struct { // SubAccountAPIKeyResponse represents sub-account api key reset response type SubAccountAPIKeyResponse struct { - SubAccountName string `json:"subAcct"` - APIKey string `json:"apiKey"` - Label string `json:"label"` - APIKeyPermission string `json:"perm"` - IP string `json:"ip"` - Timestamp okxUnixMilliTime `json:"ts"` + IP string `json:"ip"` + SubAccountName string `json:"subAcct"` + APIKey string `json:"apiKey"` + Label string `json:"label"` + APIKeyPermission string `json:"perm"` + Timestamp types.Time `json:"ts"` } // MarginBalanceParam represents compute margin balance request param type MarginBalanceParam struct { - AlgoID string `json:"algoId"` - Type string `json:"type"` - Amount float64 `json:"amt,string"` // Adjust margin balance amount Either amt or percent is required. - Percentage float64 `json:"percent,string,omitempty"` // Adjust margin balance percentage, used In Adjusting margin balance + AlgoID string `json:"algoId"` + AdjustMarginBalanceType string `json:"type"` + Amount float64 `json:"amt,string"` // Adjust margin balance amount Either amt or percent is required. + Percentage float64 `json:"percent,string,omitempty"` // Adjust margin balance percentage, used In Adjusting margin balance } // ComputeMarginBalance represents compute margin amount request response @@ -3052,12 +3844,12 @@ type ComputeMarginBalance struct { MaximumAmount types.Number `json:"maxAmt"` } -// AdjustMarginBalanceResponse represents algo id for response for margin balance adjust request. +// AdjustMarginBalanceResponse represents algo ID for response for margin balance adjust request type AdjustMarginBalanceResponse struct { AlgoID string `json:"algoId"` } -// GridAIParameterResponse represents gri AI parameter response. +// GridAIParameterResponse represents gri AI parameter response type GridAIParameterResponse struct { AlgoOrderType string `json:"algoOrdType"` AnnualizedRate string `json:"annualizedRate"` @@ -3075,21 +3867,346 @@ type GridAIParameterResponse struct { RunType string `json:"runType"` } +// InvestmentData holds investment data parameter +type InvestmentData struct { + Amount float64 `json:"amt,string"` + Currency currency.Code `json:"ccy"` +} + +// ComputeInvestmentDataParam holds parameter values for computing investment data +type ComputeInvestmentDataParam struct { + InstrumentID string `json:"instId"` + AlgoOrderType string `json:"algoOrdType"` // Algo order type 'grid': Spot grid 'contract_grid': Contract grid + GridNumber float64 `json:"gridNum,string"` + Direction string `json:"direction,omitempty"` // Contract grid type 'long','short', 'neutral' Only applicable to contract grid + MaxPrice float64 `json:"maxPx,string"` + MinPrice float64 `json:"minPx,string"` + RunType string `json:"runType"` // Grid type 1: Arithmetic, 2: Geometric + Leverage float64 `json:"lever,omitempty,string"` + BasePosition bool `json:"basePos,omitempty"` + InvestmentType string `json:"investmentType,omitempty"` + TriggerStrategy string `json:"triggerStrategy,omitempty"` // TriggerStrategy possible values are 'instant', 'price', 'rsi' + InvestmentData []InvestmentData `json:"investmentData,omitempty"` +} + +// InvestmentResult holds investment response +type InvestmentResult struct { + MinInvestmentData []InvestmentData `json:"minInvestmentData"` + SingleAmount types.Number `json:"singleAmt"` +} + +// RSIBacktestingResponse holds response for relative strength index(RSI) backtesting +type RSIBacktestingResponse struct { + TriggerNumber string `json:"triggerNum"` +} + +// SignalBotOrderDetail holds detail of signal bot order +type SignalBotOrderDetail struct { + AlgoID string `json:"algoId"` + ClientSuppliedAlgoID string `json:"algoClOrdId"` + AlgoOrderType string `json:"algoOrdType"` + InstrumentType string `json:"instType"` + InstrumentIDs []string `json:"instIds"` + CreationTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` + State string `json:"state"` + CancelType string `json:"cancelType"` + TotalPNL types.Number `json:"totalPnl"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + TotalEq types.Number `json:"totalEq"` + FloatPNL types.Number `json:"floatPnl"` + FrozenBalance types.Number `json:"frozenBal"` + AvailableBalance types.Number `json:"availBal"` + Lever types.Number `json:"lever"` + InvestAmount types.Number `json:"investAmt"` + SubOrdType string `json:"subOrdType"` + Ratio types.Number `json:"ratio"` + EntrySettingParam struct { + AllowMultipleEntry bool `json:"allowMultipleEntry"` + Amount types.Number `json:"amt"` + EntryType string `json:"entryType"` + Ratio types.Number `json:"ratio"` + } `json:"entrySettingParam"` + ExitSettingParam struct { + StopLossPercentage types.Number `json:"slPct"` + TakeProfitPercentage types.Number `json:"tpPct"` + TakeProfitSlType string `json:"tpSlType"` + } `json:"exitSettingParam"` + SignalChanID string `json:"signalChanId"` + SignalChanName string `json:"signalChanName"` + SignalSourceType string `json:"signalSourceType"` + + TotalPnlRatio types.Number `json:"totalPnlRatio"` + RealizedPnl types.Number `json:"realizedPnl"` +} + +// SignalBotPosition holds signal bot position information +type SignalBotPosition struct { + AutoDecreaseLine string `json:"adl"` + AlgoClientOrderID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + AveragePrice types.Number `json:"avgPx"` + CreationTime types.Time `json:"cTime"` + Currency string `json:"ccy"` + InitialMarginRequirement string `json:"imr"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Last types.Number `json:"last"` + Lever types.Number `json:"lever"` + LiquidationPrice types.Number `json:"liqPx"` + MarkPrice types.Number `json:"markPx"` + MarginMode string `json:"mgnMode"` + MgnRatio types.Number `json:"mgnRatio"` // Margin mode 'cross' 'isolated' + MaintenanceMarginRequirement string `json:"mmr"` + NotionalUSD string `json:"notionalUsd"` + Position string `json:"pos"` + PositionSide string `json:"posSide"` // Position side 'net' + UpdateTime types.Time `json:"uTime"` + UnrealizedProfitAndLoss string `json:"upl"` + UplRatio types.Number `json:"uplRatio"` // Unrealized profit and loss ratio +} + +// SubOrder holds signal bot sub orders +type SubOrder struct { + AccountFillSize types.Number `json:"accFillSz"` + AlgoClientOrderID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + AlgoOrdType string `json:"algoOrdType"` + AveragePrice types.Number `json:"avgPx"` + CreationTime types.Time `json:"cTime"` + Currency string `json:"ccy"` + ClientOrderID string `json:"clOrdId"` + CtVal string `json:"ctVal"` + Fee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage types.Number `json:"lever"` + OrderID string `json:"ordId"` + OrderType string `json:"ordType"` + ProfitAndLoss types.Number `json:"pnl"` + PositionSide string `json:"posSide"` + Price types.Number `json:"px"` + Side string `json:"side"` + State string `json:"state"` + Size types.Number `json:"sz"` + Tag string `json:"tag"` + TdMode string `json:"tdMode"` + UpdateTime types.Time `json:"uTime"` +} + +// SignalBotEventHistory holds history information for signal bot +type SignalBotEventHistory struct { + AlertMsg time.Time `json:"alertMsg"` + AlgoID string `json:"algoId"` + EventCreationTime types.Time `json:"eventCtime"` + EventProcessMessage string `json:"eventProcessMsg"` + EventStatus string `json:"eventStatus"` + EventUtime types.Time `json:"eventUtime"` + EventType string `json:"eventType"` + TriggeredOrdData []struct { + ClientOrderID string `json:"clOrdId"` + } `json:"triggeredOrdData"` +} + +// PlaceRecurringBuyOrderParam holds parameters for placing recurring order +type PlaceRecurringBuyOrderParam struct { + Tag string `json:"tag"` + ClientSuppliedAlgoOrderID string `json:"algoClOrdId"` + StrategyName string `json:"stgyName"` // Custom name for trading bot + Amount float64 `json:"amt,string"` + RecurringList []RecurringListItem `json:"recurringList"` + Period string `json:"period"` // Period 'monthly' 'weekly' 'daily' + + // Recurring buy date + // When the period is monthly, the value range is an integer of [1,28] + // When the period is weekly, the value range is an integer of [1,7] + // When the period is daily, the value is 1 + RecurringDay string `json:"recurringDay"` + RecurringTime int64 `json:"recurringTime,string"` // Recurring buy time, the value range is an integer of [0,23] + TimeZone string `json:"timeZone"` + TradeMode string `json:"tdMode"` // Trading mode Margin mode: 'cross' Non-Margin mode: 'cash' + InvestmentCurrency string `json:"investmentCcy"` +} + +// RecurringListItem holds recurring list item +type RecurringListItem struct { + Currency currency.Code `json:"ccy"` + Ratio float64 `json:"ratio,string"` +} + +// RecurringListItemDetailed holds a detailed instance of recurring list item +type RecurringListItemDetailed struct { + AveragePrice types.Number `json:"avgPx"` + Currency string `json:"ccy"` + Profit types.Number `json:"profit"` + Price types.Number `json:"px"` + Ratio types.Number `json:"ratio"` + TotalAmount types.Number `json:"totalAmt"` +} + +// RecurringOrderResponse holds recurring order response +type RecurringOrderResponse struct { + AlgoID string `json:"algoId"` + AlgoClientOrderID string `json:"algoClOrdId"` + StatusCode string `json:"sCode"` + StatusMessage string `json:"sMsg"` +} + +// AmendRecurringOrderParam holds recurring order params +type AmendRecurringOrderParam struct { + AlgoID string `json:"algoId"` + StrategyName string `json:"stgyName"` +} + +// StopRecurringBuyOrder stop recurring order +type StopRecurringBuyOrder struct { + AlgoID string `json:"algoId"` +} + +// RecurringOrderItem holds recurring order info +type RecurringOrderItem struct { + AlgoClOrdID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + AlgoOrdType string `json:"algoOrdType"` + Amount types.Number `json:"amt"` + CreationTime types.Time `json:"cTime"` + Cycles string `json:"cycles"` + InstrumentType string `json:"instType"` + InvestmentAmount types.Number `json:"investmentAmt"` + InvestmentCurrency string `json:"investmentCcy"` + MarketCap string `json:"mktCap"` + Period string `json:"period"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + RecurringDay string `json:"recurringDay"` + RecurringList []RecurringListItem `json:"recurringList"` + RecurringTime string `json:"recurringTime"` + State string `json:"state"` + StgyName string `json:"stgyName"` + Tag string `json:"tag"` + TimeZone string `json:"timeZone"` + TotalAnnRate types.Number `json:"totalAnnRate"` + TotalPnl types.Number `json:"totalPnl"` + UpdateTime types.Time `json:"uTime"` +} + +// RecurringOrderDeail holds detailed information about recurring order +type RecurringOrderDeail struct { + RecurringListItem + RecurringList []RecurringListItemDetailed `json:"recurringList"` +} + +// RecurringBuySubOrder holds recurring buy sub order detail +type RecurringBuySubOrder struct { + AccFillSize types.Number `json:"accFillSz"` + AlgoClientOrdID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + AlgoOrderType string `json:"algoOrdType"` + AveragePrice types.Number `json:"avgPx"` + CreationTime types.Time `json:"cTime"` + Fee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + OrderID string `json:"ordId"` + OrderType string `json:"ordType"` + Price types.Number `json:"px"` + Side string `json:"side"` + State string `json:"state"` + Size types.Number `json:"sz"` + Tag string `json:"tag"` + TradeMode string `json:"tdMode"` + UpdateTime types.Time `json:"uTime"` +} + +// PositionInfo represents a positions detail +type PositionInfo struct { + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + AlgoID string `json:"algoId"` + Lever types.Number `json:"lever"` + MarginMode string `json:"mgnMode"` + OpenAvgPrice types.Number `json:"openAvgPx"` + OpenOrderID string `json:"openOrdId"` + OpenTime types.Time `json:"openTime"` + PositionSide string `json:"posSide"` + SlTriggerPrice types.Number `json:"slTriggerPx"` + SubPos string `json:"subPos"` + SubPosID string `json:"subPosId"` + TpTriggerPrice types.Number `json:"tpTriggerPx"` + CloseAveragePrice types.Number `json:"closeAvgPx"` + CloseTime types.Time `json:"closeTime"` +} + +// TPSLOrderParam holds Take profit and stop loss order parameters +type TPSLOrderParam struct { + InstrumentType string `json:"instType"` + SubPositionID string `json:"subPosId"` + TakeProfitTriggerPrice float64 `json:"tpTriggerPx,omitempty,string"` + StopLossTriggerPrice float64 `json:"slTriggerPx,omitempty,string"` + + TakeProfitOrderPrice float64 `json:"tpOrdPx,omitempty,string"` + StopLossOrderPrice float64 `json:"slOrdPx,omitempty,string"` + + TakePofitTriggerPriceType string `json:"tpTriggerPriceType,omitempty,string"` // last: last price, 'index': index price 'mark': mark price Default is 'last' + StopLossTriggerPriceType string `jsonL:"slTriggerPxType,omitempty,string"` // Stop-loss trigger price type 'last': last price 'index': index price 'mark': mark price Default is 'last' + SubPositionType string `json:"subPosType,omitempty,string"` // 'lead': lead trading, the default value 'copy': copy trading + Tag string `json:"tag,omitempty,string"` +} + +// PositionIDInfo holds place positions information +type PositionIDInfo struct { + SubPosID string `json:"subPosId"` + Tag string `json:"tag"` +} + +// CloseLeadingPositionParam request parameter for closing leading position +type CloseLeadingPositionParam struct { + InstrumentType string `json:"instType"` + SubPositionID string `json:"subPosId"` + Tag string `json:"tag"` +} + +// LeadingInstrumentItem represents leading instrument info and it's status +type LeadingInstrumentItem struct { + Enabled bool `json:"enabled"` + InstrumentID string `json:"instId"` +} + +// ProfitSharingItem holds profit sharing information +type ProfitSharingItem struct { + Currency string `json:"ccy"` + NickName string `json:"nickName"` + ProfitSharingAmount types.Number `json:"profitSharingAmt"` + ProfitSharingID string `json:"profitSharingId"` + InstrumentType string `json:"instType"` + Timestamp types.Time `json:"ts"` +} + +// TotalProfitSharing holds information about total amount of profit shared since joining the platform +type TotalProfitSharing struct { + Currency string `json:"ccy"` + InstrumentType string `json:"instType"` + TotalProfitSharingAmount types.Number `json:"totalProfitSharingAmt"` +} + // Offer represents an investment offer information for different 'staking' and 'defi' protocols type Offer struct { - Currency string `json:"ccy"` - ProductID string `json:"productId"` - Protocol string `json:"protocol"` - ProtocolType string `json:"protocolType"` - EarningCcy []string `json:"earningCcy"` - Term string `json:"term"` - Apy types.Number `json:"apy"` - EarlyRedeem bool `json:"earlyRedeem"` - InvestData []OfferInvestData `json:"investData"` - EarningData []struct { + Currency string `json:"ccy"` + ProductID string `json:"productId"` + Protocol string `json:"protocol"` + ProtocolType string `json:"protocolType"` + EarningCurrency []string `json:"earningCcy"` + Term string `json:"term"` + Apy types.Number `json:"apy"` + EarlyRedeem bool `json:"earlyRedeem"` + InvestData []OfferInvestData `json:"investData"` + EarningData []struct { Currency string `json:"ccy"` EarningType string `json:"earningType"` } `json:"earningData"` + State string `json:"state"` + FastRedemptionDailyLimit types.Number `json:"fastRedemptionDailyLimit"` } // OfferInvestData represents currencies invest data information for an offer @@ -3103,19 +4220,26 @@ type OfferInvestData struct { // PurchaseRequestParam represents purchase request param specific product type PurchaseRequestParam struct { ProductID string `json:"productId"` - Term int `json:"term,string,omitempty"` + Term int64 `json:"term,string,omitempty"` InvestData []PurchaseInvestDataItem `json:"investData"` } // PurchaseInvestDataItem represents purchase invest data information having the currency and amount information type PurchaseInvestDataItem struct { - Currency string `json:"ccy"` - Amount types.Number `json:"amt"` + Currency currency.Code `json:"ccy"` + Amount float64 `json:"amt,string"` } // OrderIDResponse represents purchase order ID type OrderIDResponse struct { OrderID string `json:"orderId"` + Tag string `json:"tag"` // Optional to most ID responses +} + +// CancelPurchaseOrRedemptionResponse represents a response for canceling a purchase or redemption +type CancelPurchaseOrRedemptionResponse struct { + OrderIDResponse + Tag string `json:"tag"` } // RedeemRequestParam represents redeem request input param @@ -3131,6 +4255,11 @@ type CancelFundingParam struct { ProtocolType string `json:"protocolType"` } +// ProductInfo represents ETH staking information +type ProductInfo struct { + FastRedemptionDailyLimit types.Number `json:"fastRedemptionDailyLimit"` +} + // ActiveFundingOrder represents active purchase orders type ActiveFundingOrder struct { OrderID string `json:"ordId"` @@ -3145,34 +4274,43 @@ type ActiveFundingOrder struct { Amount types.Number `json:"amt"` } `json:"investData"` EarningData []struct { - Ccy string `json:"ccy"` + Currency string `json:"ccy"` EarningType string `json:"earningType"` Earnings types.Number `json:"earnings"` } `json:"earningData"` - PurchasedTime okxUnixMilliTime `json:"purchasedTime"` + PurchasedTime types.Time `json:"purchasedTime"` + FastRedemptionData []struct { + Currency string `json:"ccy"` + RedeemingAmount types.Number `json:"redeemingAmt"` + } `json:"fastRedemptionData"` + EstimatedRedemptionSettlementTime types.Time `json:"estSettlementTime"` + CancelRedemptionDeadline types.Time `json:"cancelRedemptionDeadline"` + Tag string `json:"tag"` } -// FundingOrder represents orders of earning, purchase, and redeem -type FundingOrder struct { - OrderID string `json:"ordId"` - State string `json:"state"` - Currency string `json:"ccy"` - Protocol string `json:"protocol"` - ProtocolType string `json:"protocolType"` - Term string `json:"term"` - Apy types.Number `json:"apy"` - InvestData []struct { - Currency string `json:"ccy"` - Amount types.Number `json:"amt"` - } `json:"investData"` - EarningData []struct { - Currency string `json:"ccy"` - EarningType string `json:"earningType"` - RealizedEarnings types.Number `json:"realizedEarnings"` - } `json:"earningData"` - PurchasedTime okxUnixMilliTime `json:"purchasedTime"` - RedeemedTime okxUnixMilliTime `json:"redeemedTime"` - EarningCcy []string `json:"earningCcy,omitempty"` +// BETHAssetsBalance balance is a snapshot summarized all BETH assets +type BETHAssetsBalance struct { + Currency string `json:"ccy"` + Amount types.Number `json:"amt"` + LatestInterestAccrual types.Number `json:"latestInterestAccrual"` + TotalInterestAccrual types.Number `json:"totalInterestAccrual"` + Timestamp types.Time `json:"ts"` +} + +// PurchaseRedeemHistory holds purchase and redeem history +type PurchaseRedeemHistory struct { + Amt types.Number `json:"amt"` + CompletedTime types.Time `json:"completedTime"` + EstCompletedTime types.Time `json:"estCompletedTime"` + RequestTime types.Time `json:"requestTime"` + Status string `json:"status"` + Type string `json:"type"` +} + +// APYItem holds annual percentage yield record +type APYItem struct { + Rate types.Number `json:"rate"` + Timestamp types.Time `json:"ts"` } // wsRequestDataChannelsMultiplexer a single multiplexer instance to multiplex websocket messages multiplexer channels @@ -3185,7 +4323,7 @@ type wsRequestDataChannelsMultiplexer struct { shutdown chan bool } -// wsSubscriptionParameters represents toggling boolean values for subscription parameters. +// wsSubscriptionParameters represents toggling boolean values for subscription parameters type wsSubscriptionParameters struct { InstrumentType bool InstrumentID bool @@ -3204,9 +4342,956 @@ type WsOrderbook5 struct { // Book5Data stores the orderbook data for orderbook 5 websocket type Book5Data struct { - Asks [][4]string `json:"asks"` - Bids [][4]string `json:"bids"` - InstrumentID string `json:"instId"` - TimestampMilli int64 `json:"ts,string"` - SequenceID int64 `json:"seqId"` + Asks [][4]types.Number `json:"asks"` + Bids [][4]types.Number `json:"bids"` + InstrumentID string `json:"instId"` + Timestamp types.Time `json:"ts"` + SequenceID int64 `json:"seqId"` +} + +// WsSpreadOrder represents spread order detail +type WsSpreadOrder struct { + SpreadID string `json:"sprdId"` + OrderID string `json:"ordId"` + ClientOrderID string `json:"clOrdId"` + Tag string `json:"tag"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + OrderType string `json:"ordType"` + Side string `json:"side"` + FillSize types.Number `json:"fillSz"` + FillPrice types.Number `json:"fillPx"` + TradeID string `json:"tradeId"` + AccFillSize types.Number `json:"accFillSz"` + PendingFillSize types.Number `json:"pendingFillSz"` + PendingSettleSize types.Number `json:"pendingSettleSz"` + CanceledSize types.Number `json:"canceledSz"` + State string `json:"state"` + AveragePrice types.Number `json:"avgPx"` + CancelSource string `json:"cancelSource"` + UpdateTime types.Time `json:"uTime"` + CreationTime types.Time `json:"cTime"` + Code string `json:"code"` + Msg string `json:"msg"` +} + +// WsSpreadOrderTrade trade of an order +type WsSpreadOrderTrade struct { + Argument struct { + Channel string `json:"channel"` + SpreadID string `json:"sprdId"` + UID string `json:"uid"` + } `json:"arg"` + Data []struct { + SpreadID string `json:"sprdId"` + TradeID string `json:"tradeId"` + OrderID string `json:"ordId"` + ClientOrderID string `json:"clOrdId"` + Tag string `json:"tag"` + FillPrice types.Number `json:"fillPx"` + FillSize types.Number `json:"fillSz"` + State string `json:"state"` + Side string `json:"side"` + ExecType string `json:"execType"` + Timestamp types.Time `json:"ts"` + Legs []struct { + InstrumentID string `json:"instId"` + Price types.Number `json:"px"` + Size types.Number `json:"sz"` + Side string `json:"side"` + Fee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` + TradeID string `json:"tradeId"` + } `json:"legs"` + Code string `json:"code"` + Msg string `json:"msg"` + } `json:"data"` +} + +// WsSpreadOrderbook holds spread orderbook data +type WsSpreadOrderbook struct { + Arg struct { + Channel string `json:"channel"` + SpreadID string `json:"sprdId"` + } `json:"arg"` + Data []struct { + Asks [][3]types.Number `json:"asks"` + Bids [][3]types.Number `json:"bids"` + Timestamp types.Time `json:"ts"` + } `json:"data"` +} + +// WsSpreadPushData holds push data +type WsSpreadPushData struct { + Argument SubscriptionInfo `json:"arg"` + Data any `json:"data"` +} + +// WsSpreadPublicTicker holds spread public ticker data +type WsSpreadPublicTicker struct { + SpreadID string `json:"sprdId"` + Last types.Number `json:"last"` + LastSize types.Number `json:"lastSz"` + AskPrice types.Number `json:"askPx"` + AskSize types.Number `json:"askSz"` + BidPrice types.Number `json:"bidPx"` + BidSize types.Number `json:"bidSz"` + Timestamp types.Time `json:"ts"` +} + +// WsSpreadPublicTrade holds trades data from sprd-public-trades +type WsSpreadPublicTrade struct { + SpreadID string `json:"sprdId"` + Side string `json:"side"` + Size types.Number `json:"sz"` + Price types.Number `json:"px"` + TradeID string `json:"tradeId"` + Timestamp types.Time `json:"ts"` +} + +// ExtractSpreadOrder extracts WsSpreadOrderbookData from a WsSpreadOrderbook instance +func (a *WsSpreadOrderbook) ExtractSpreadOrder() (*WsSpreadOrderbookData, error) { + resp := &WsSpreadOrderbookData{ + Argument: SubscriptionInfo{ + SpreadID: a.Arg.SpreadID, + Channel: a.Arg.Channel, + }, + Data: make([]WsSpreadOrderbookItem, len(a.Data)), + } + for x := range a.Data { + resp.Data[x].Timestamp = a.Data[x].Timestamp.Time() + resp.Data[x].Asks = make([]orderbook.Tranche, len(a.Data[x].Asks)) + resp.Data[x].Bids = make([]orderbook.Tranche, len(a.Data[x].Bids)) + + for as := range a.Data[x].Asks { + resp.Data[x].Asks[as].Price = a.Data[x].Asks[as][0].Float64() + resp.Data[x].Asks[as].Amount = a.Data[x].Asks[as][1].Float64() + resp.Data[x].Asks[as].OrderCount = a.Data[x].Asks[as][2].Int64() + } + for as := range a.Data[x].Bids { + resp.Data[x].Bids[as].Price = a.Data[x].Bids[as][0].Float64() + resp.Data[x].Bids[as].Amount = a.Data[x].Bids[as][1].Float64() + resp.Data[x].Bids[as].OrderCount = a.Data[x].Bids[as][2].Int64() + } + } + return resp, nil +} + +// WsSpreadOrderbookItem represents an orderbook asks and bids details +type WsSpreadOrderbookItem struct { + Asks []orderbook.Tranche + Bids []orderbook.Tranche + Timestamp time.Time +} + +// WsSpreadOrderbookData represents orderbook response for spread instruments +type WsSpreadOrderbookData struct { + Argument SubscriptionInfo `json:"arg"` + Data []WsSpreadOrderbookItem +} + +// AffilateInviteesDetail represents affiliate invitee's detail +type AffilateInviteesDetail struct { + InviteeLevel types.Number `json:"inviteeLv"` + JoinTime types.Time `json:"joinTime"` + InviteeRebateRate types.Number `json:"inviteeRebateRate"` + TotalCommission types.Number `json:"totalCommission"` + FirstTradeTime types.Time `json:"firstTradeTime"` + Level string `json:"level"` + DepositAmount types.Number `json:"depAmt"` + AccumulatedTradingVolume types.Number `json:"volMonth"` + AccumulatedFee types.Number `json:"accFee"` + KYCTime types.Time `json:"kycTime"` + Region string `json:"region"` + AffiliateCode string `json:"affiliateCode"` +} + +// AffilateRebateInfo represents rebate information +type AffilateRebateInfo struct { + Result bool `json:"result"` + Type string `json:"type"` +} + +// WsDepositInfo represents a deposit information +type WsDepositInfo struct { + ActualDepBulkConfirm string `json:"actualDepBlkConfirm"` + Amount types.Number `json:"amt"` + AreaCodeFrom string `json:"areaCodeFrom"` + Currency string `json:"ccy"` + Chain string `json:"chain"` + DepositID string `json:"depId"` + From string `json:"from"` + FromWdID string `json:"fromWdId"` // Internal transfer initiator's withdrawal ID + PushTime types.Time `json:"pTime"` + State string `json:"state"` + SubAccount string `json:"subAcct"` + To string `json:"to"` + Timestamp types.Time `json:"ts"` + TransactionID string `json:"txId"` + UID string `json:"uid"` +} + +// WsWithdrawlInfo represents push notification is triggered when a withdrawal is initiated or the withdrawal status changes +type WsWithdrawlInfo struct { + AddrEx any `json:"addrEx"` + Amount types.Number `json:"amt"` + AreaCodeFrom string `json:"areaCodeFrom"` + AreaCodeTo string `json:"areaCodeTo"` + Currency string `json:"ccy"` + Chain string `json:"chain"` + ClientID string `json:"clientId"` + Fee types.Number `json:"fee"` + FeeCurrency string `json:"feeCcy"` + From string `json:"from"` + Memo string `json:"memo"` + NonTradableAsset bool `json:"nonTradableAsset"` + PushTime types.Time `json:"pTime"` + PmtID string `json:"pmtId"` + State string `json:"state"` + SubAcct string `json:"subAcct"` + Tag string `json:"tag"` + To string `json:"to"` + Timestamp types.Time `json:"ts"` + TransactionID string `json:"txId"` + UID string `json:"uid"` + WithdrawalID string `json:"wdId"` +} + +// RecurringBuyOrder represents a recurring buy order instance +type RecurringBuyOrder struct { + AlgoClOrdID string `json:"algoClOrdId"` + AlgoID string `json:"algoId"` + AlgoOrderType string `json:"algoOrdType"` + Amount types.Number `json:"amt"` + CreationTime types.Time `json:"cTime"` + Cycles string `json:"cycles"` + InstrumentType string `json:"instType"` + InvestmentAmount types.Number `json:"investmentAmt"` + InvestmentCurrency string `json:"investmentCcy"` + MarketCap string `json:"mktCap"` + NextInvestTime types.Time `json:"nextInvestTime"` + PushTime types.Time `json:"pTime"` + Period string `json:"period"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + RecurringDay string `json:"recurringDay"` + RecurringHour string `json:"recurringHour"` + RecurringList []struct { + AveragePrice types.Number `json:"avgPx"` + Currency string `json:"ccy"` + Profit string `json:"profit"` + Price types.Number `json:"px"` + Ratio types.Number `json:"ratio"` + TotalAmount types.Number `json:"totalAmt"` + } `json:"recurringList"` + RecurringTime string `json:"recurringTime"` + State string `json:"state"` + StrategyName string `json:"stgyName"` + Tag string `json:"tag"` + TimeZone string `json:"timeZone"` + TotalAnnRate string `json:"totalAnnRate"` + TotalPnl string `json:"totalPnl"` + UpdateTime types.Time `json:"uTime"` +} + +// ADLWarning represents auto-deleveraging warning +type ADLWarning struct { + Arg SubscriptionInfo `json:"arg"` + Data []struct { + DecRate string `json:"decRate"` + MaxBal string `json:"maxBal"` + AdlRecRate types.Number `json:"adlRecRate"` + AdlRecBal types.Number `json:"adlRecBal"` + Balance types.Number `json:"bal"` + InstrumentType string `json:"instType"` + AdlRate types.Number `json:"adlRate"` + InstrumentFamily string `json:"instFamily"` + MaxBalTimestamp types.Time `json:"maxBalTs"` + AdlType string `json:"adlType"` + State string `json:"state"` + AdlBalance types.Number `json:"adlBal"` + Timestamp types.Time `json:"ts"` + } `json:"data"` +} + +// EconomicCalendar represents macro-economic calendar data +type EconomicCalendar struct { + Actual string `json:"actual"` + CalendarID string `json:"calendarId"` + Date types.Time `json:"date"` + Region string `json:"region"` + Category string `json:"category"` + Event string `json:"event"` + RefDate types.Time `json:"refDate"` + Previous string `json:"previous"` + Forecast string `json:"forecast"` + Importance string `json:"importance"` + PrevInitial string `json:"prevInitial"` + Currency string `json:"ccy"` + Unit string `json:"unit"` + Timestamp types.Time `json:"ts"` + DateSpan string `json:"dateSpan"` + UpdateTime types.Time `json:"uTime"` +} + +// EconomicCalendarResponse represents response for economic calendar +type EconomicCalendarResponse struct { + Arg SubscriptionInfo `json:"arg"` + Data []EconomicCalendar `json:"data"` +} + +// CopyTradingNotification holds copy-trading notifications +type CopyTradingNotification struct { + Argument SubscriptionInfo `json:"arg"` + Data []struct { + AveragePrice types.Number `json:"avgPx"` + Currency string `json:"ccy"` + CopyTotalAmount types.Number `json:"copyTotalAmt"` + InfoType string `json:"infoType"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage types.Number `json:"lever"` + MaxLeadTraderNumber string `json:"maxLeadTraderNum"` + MinNotional types.Number `json:"minNotional"` + PositionSide string `json:"posSide"` + RmThreshold string `json:"rmThold"` // Lead trader can remove copy trader if balance of copy trader less than this value. + Side string `json:"side"` + StopLossTotalAmount types.Number `json:"slTotalAmt"` + SlippageRatio types.Number `json:"slippageRatio"` + SubPosID string `json:"subPosId"` + UniqueCode string `json:"uniqueCode"` + } `json:"data"` +} + +// FirstCopySettings holds parameters first copy settings for the certain lead trader +type FirstCopySettings struct { + InstrumentType string `json:"instType,omitempty"` + InstrumentID string `json:"instId"` // Instrument ID. If there are multiple instruments, separate them with commas. Maximum of 200 instruments can be selected + UniqueCode string `json:"uniqueCode"` + CopyMarginMode string `json:"copyMgnMode,omitempty"` // Copy margin mode 'cross': cross 'isolated': isolated 'copy' + CopyInstrumentIDType string `json:"copyInstIdType"` // Copy contract type set 'custom': custom by instId which is requiredï¼›'copy': Keep your contracts consistent with this trader + CopyMode string `json:"copyMode,omitempty"` // Possible values: 'fixed_amount', 'ratio_copy', 'copyRatio', and 'fixed_amount' + CopyRatio float64 `json:"copyRatio,string"` + CopyAmount float64 `json:"copyAmt,string,omitempty"` + CopyTotalAmount float64 `json:"copyTotalAmt,string"` + SubPosCloseType string `json:"subPosCloseType"` + TakeProfitRatio float64 `json:"tpRatio,string,omitempty"` + StopLossRatio float64 `json:"slRatio,string,omitempty"` + StopLossTotalAmount float64 `json:"slTotalAmt,string,omitempty"` +} + +// StopCopyingParameter holds stop copying request parameter +type StopCopyingParameter struct { + InstrumentType string `json:"instType,omitempty"` + UniqueCode string `json:"uniqueCode"` + SubPositionCloseType string `json:"subPosCloseType"` +} + +// CopySetting represents a copy setting response +type CopySetting struct { + Currency string `json:"ccy"` + CopyState string `json:"copyState"` + CopyMarginMode string `json:"copyMgnMode"` + SubPositionCloseType string `json:"subPosCloseType"` + StopLossTotalAmount types.Number `json:"slTotalAmt"` + CopyAmount types.Number `json:"copyAmt"` + CopyInstrumentIDType string `json:"copyInstIdType"` + CopyMode string `json:"copyMode"` + CopyRatio types.Number `json:"copyRatio"` + CopyTotalAmount types.Number `json:"copyTotalAmt"` + InstrumentIDs []struct { + Enabled string `json:"enabled"` + InstrumentID string `json:"instId"` + } `json:"instIds"` + StopLossRatio types.Number `json:"slRatio"` + TakeProfitRatio types.Number `json:"tpRatio"` +} + +// Leverages holds batch leverage info +type Leverages struct { + LeadTraderLevers []LeverageInfo `json:"leadTraderLevers"` + MyLevers []LeverageInfo `json:"myLevers"` + InstrumentID string `json:"instId"` + MarginMode string `json:"mgnMode"` +} + +// LeverageInfo holds leverage information +type LeverageInfo struct { + Leverage types.Number `json:"lever"` + PositionSide string `json:"posSide"` +} + +// SetMultipleLeverageResponse represents multiple leverage response +type SetMultipleLeverageResponse struct { + FailInstrumentID string `json:"failInstId"` + Result string `json:"result"` + SuccInstrumentID string `json:"succInstId"` +} + +// SetLeveragesParam sets leverage parameter +type SetLeveragesParam struct { + MarginMode string `json:"mgnMode"` + Leverage int64 `json:"lever,string"` + InstrumentID string `json:"instId,omitempty"` // Instrument ID. If there are multiple instruments, separate them with commas. Maximum of 200 instruments can be selected +} + +// CopyTradingLeadTrader represents a lead trader information +type CopyTradingLeadTrader struct { + BeginCopyTime types.Time `json:"beginCopyTime"` + Currency string `json:"ccy"` + CopyTotalAmount types.Number `json:"copyTotalAmt"` + CopyTotalProfitAndLoss types.Number `json:"copyTotalPnl"` + LeadMode string `json:"leadMode"` + Margin types.Number `json:"margin"` + NickName string `json:"nickName"` + PortLink string `json:"portLink"` + ProfitSharingRatio types.Number `json:"profitSharingRatio"` + TodayProfitAndLoss types.Number `json:"todayPnl"` + UniqueCode string `json:"uniqueCode"` + UnrealizedProfitAndLoss types.Number `json:"upl"` + CopyMode string `json:"copyMode"` + CopyNum string `json:"copyNum"` + CopyRatio types.Number `json:"copyRatio"` + CopyRelID string `json:"copyRelId"` + CopyState string `json:"copyState"` +} + +// LeadTradersRank represents lead traders rank info +type LeadTradersRank struct { + DataVer string `json:"dataVer"` + Ranks []struct { + AccCopyTraderNum string `json:"accCopyTraderNum"` + Aum string `json:"aum"` + Currency string `json:"ccy"` + CopyState string `json:"copyState"` + CopyTraderNum string `json:"copyTraderNum"` + LeadDays string `json:"leadDays"` + MaxCopyTraderNum string `json:"maxCopyTraderNum"` + NickName string `json:"nickName"` + Pnl types.Number `json:"pnl"` + PnlRatio types.Number `json:"pnlRatio"` + PnlRatios []struct { + BeginTimestamp types.Time `json:"beginTs"` + PnlRatio types.Number `json:"pnlRatio"` + } `json:"pnlRatios"` + PortLink string `json:"portLink"` + TraderInsts []string `json:"traderInsts"` + UniqueCode string `json:"uniqueCode"` + WinRatio types.Number `json:"winRatio"` + } `json:"ranks"` + TotalPage string `json:"totalPage"` +} + +// TraderWeeklyProfitAndLoss represents lead trader weekly pnl +type TraderWeeklyProfitAndLoss struct { + BeginTimestamp types.Time `json:"beginTs"` + ProfitAndLoss types.Number `json:"pnl"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` +} + +// LeadTraderStat represents lead trader performance info +type LeadTraderStat struct { + AvgSubPosNotional types.Number `json:"avgSubPosNotional"` + Currency string `json:"ccy"` + CurCopyTraderPnl types.Number `json:"curCopyTraderPnl"` + InvestAmount types.Number `json:"investAmt"` + LossDays string `json:"lossDays"` + ProfitDays string `json:"profitDays"` + WinRatio types.Number `json:"winRatio"` +} + +// LeadTraderCurrencyPreference holds public preference currency +type LeadTraderCurrencyPreference struct { + Currency string `json:"ccy"` + Ratio types.Number `json:"ratio"` +} + +// LeadTraderCurrentLeadPosition holds leading positions of lead trader +type LeadTraderCurrentLeadPosition struct { + Currency string `json:"ccy"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Lever types.Number `json:"lever"` + Margin types.Number `json:"margin"` + MarkPrice types.Number `json:"markPx"` + MarginMode string `json:"mgnMode"` + OpenAvgPrice types.Number `json:"openAvgPx"` + OpenTime types.Time `json:"openTime"` + PositionSide string `json:"posSide"` + SubPos string `json:"subPos"` + SubPosID string `json:"subPosId"` + UniqueCode string `json:"uniqueCode"` + UPL types.Number `json:"upl"` + UPLRatio types.Number `json:"uplRatio"` +} + +// LeadPosition holds lead trader completed leading position +type LeadPosition struct { + Currency string `json:"ccy"` + CloseAveragePrice types.Number `json:"closeAvgPx"` + CloseTime types.Time `json:"closeTime"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + Leverage types.Number `json:"lever"` + Margin types.Number `json:"margin"` + MarginMode string `json:"mgnMode"` + OpenAveragePrice types.Number `json:"openAvgPx"` + OpenTime types.Time `json:"openTime"` + ProfitAndLoss string `json:"pnl"` + ProfitAndLossRatio types.Number `json:"pnlRatio"` + PositionSide string `json:"posSide"` + SubPosition string `json:"subPos"` + SubPositionID string `json:"subPosId"` + UniqueCode string `json:"uniqueCode"` +} + +// LendingOrderParam represents a lending order request parameters +type LendingOrderParam struct { + Currency currency.Code `json:"ccy"` + Amount float64 `json:"amt,omitempty,string"` + Rate float64 `json:"rate,omitempty,string"` + Term string `json:"term"` + AutoRenewal bool `json:"autoRenewal,omitempty"` +} + +// LendingOrderResponse represents an order ID response after placing a lending order +type LendingOrderResponse []struct { + OrderID string `json:"ordId"` +} + +// LendingOrderDetail represents a lending order detail +type LendingOrderDetail struct { + OrderID string `json:"ordId"` + Amount types.Number `json:"amt"` + AutoRenewal bool `json:"autoRenewal"` + Currency string `json:"ccy"` + EarningAmount types.Number `json:"earningAmt"` + PendingAmount types.Number `json:"pendingAmt"` + Rate types.Number `json:"rate"` + State string `json:"state"` + Term string `json:"term"` + TotalInterest string `json:"totalInterest"` + CreationTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` + SettledTime types.Time `json:"settledTime"` + StartTime types.Time `json:"startTime"` +} + +// LendingSubOrder represents a lending sub-order detail +type LendingSubOrder struct { + AccruedInterest string `json:"accruedInterest"` + Amount types.Number `json:"amt"` + Currency string `json:"ccy"` + EarlyTerminatedPenalty string `json:"earlyTerminatedPenalty"` + ExpiryTime types.Time `json:"expiryTime"` + FinalSettlementTime types.Time `json:"finalSettlementTime"` + OrderID string `json:"ordId"` + OverdueInterest string `json:"overdueInterest"` + Rate string `json:"rate"` + SettledTime types.Time `json:"settledTime"` + State string `json:"state"` + SubOrdID string `json:"subOrdId"` + Term string `json:"term"` + TotalInterest string `json:"totalInterest"` + CreationTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` +} + +// PublicLendingOffer represents a lending offer detail +type PublicLendingOffer struct { + Currency string `json:"ccy"` + LendQuota string `json:"lendQuota"` + MinLendingAmount types.Number `json:"minLend"` + Rate types.Number `json:"rate"` + Term string `json:"term"` +} + +// LendingAPIHistoryItem represents a lending API history item +type LendingAPIHistoryItem struct { + Currency string `json:"ccy"` + Rate types.Number `json:"rate"` + Timestamp types.Time `json:"ts"` +} + +// LendingVolume represents a lending volume detail for a specific currency +type LendingVolume struct { + Currency string `json:"ccy"` + PendingVol types.Number `json:"pendingVol"` + RateRangeFrom string `json:"rateRangeFrom"` + RateRangeTo string `json:"rateRangeTo"` + Term string `json:"term"` +} + +// SpreadOrderCancellationResponse represents a spread order cancellation response +type SpreadOrderCancellationResponse struct { + TriggerTime types.Time `json:"triggerTime"` + Timestamp types.Time `json:"ts"` +} + +// ContractTakerVolume represents a contract taker sell and buy volume +type ContractTakerVolume struct { + Timestamp types.Time + TakerSellVolume types.Number + TakerBuyVolume types.Number +} + +// UnmarshalJSON deserializes a slice data into ContractTakerVolume +func (c *ContractTakerVolume) UnmarshalJSON(data []byte) error { + target := [3]any{&c.Timestamp, &c.TakerSellVolume, &c.TakerBuyVolume} + return json.Unmarshal(data, &target) +} + +// ContractOpenInterestHistoryItem represents an open interest information for contract +type ContractOpenInterestHistoryItem struct { + Timestamp types.Time + OpenInterestInContract types.Number + OpenInterestInCurrency types.Number + OpenInterestInUSD types.Number +} + +// UnmarshalJSON deserializes slice data into ContractOpenInterestHistoryItem instance +func (c *ContractOpenInterestHistoryItem) UnmarshalJSON(data []byte) error { + target := [4]any{&c.Timestamp, &c.OpenInterestInContract, &c.OpenInterestInCurrency, &c.OpenInterestInUSD} + return json.Unmarshal(data, &target) +} + +// TopTraderContractsLongShortRatio represents the timestamp and ratio information of top traders long and short accounts/positions +type TopTraderContractsLongShortRatio struct { + Timestamp types.Time + Ratio types.Number +} + +// UnmarshalJSON deserializes slice data into TopTraderContractsLongShortRatio instance +func (t *TopTraderContractsLongShortRatio) UnmarshalJSON(data []byte) error { + target := [2]any{&t.Timestamp, &t.Ratio} + return json.Unmarshal(data, &target) +} + +// AccountInstrument represents an account instrument +type AccountInstrument struct { + BaseCurrency string `json:"baseCcy"` + ContractMultiplier string `json:"ctMult"` + ContractType string `json:"ctType"` + ContractValue string `json:"ctVal"` + ContractValCurrency string `json:"ctValCcy"` + ExpiryTime types.Time `json:"expTime"` + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + InstFamily string `json:"instFamily"` + MaxLeverage string `json:"lever"` + ListTime types.Time `json:"listTime"` + LotSz types.Number `json:"lotSz"` // If it is a derivatives contract, the value is the number of contracts. If it is SPOT/MARGIN, the value is the quantity in base currency. + MaxIcebergSz types.Number `json:"maxIcebergSz"` + MaxLimitAmount types.Number `json:"maxLmtAmt"` + MaxLimitSize types.Number `json:"maxLmtSz"` + MaxMktAmount types.Number `json:"maxMktAmt"` + MaxMktSize types.Number `json:"maxMktSz"` + MaxStopSize types.Number `json:"maxStopSz"` + MaxTriggerSz types.Number `json:"maxTriggerSz"` + MaxTwapSize types.Number `json:"maxTwapSz"` + MinSize types.Number `json:"minSz"` + OptionType string `json:"optType"` + QuoteCurrency string `json:"quoteCcy"` + SettleCurrency string `json:"settleCcy"` + State string `json:"state"` + StrikePrice string `json:"stk"` + TickSize types.Number `json:"tickSz"` + Underlying string `json:"uly"` + RuleType string `json:"ruleType"` +} + +// ReduceLiabilities represents a response after reducing liabilities +type ReduceLiabilities struct { + OrderID string `json:"ordId"` + PendingRepay bool `json:"pendingRepay"` +} + +// FixedLoanBorrowOrderDetail represents a borrow order detail +type FixedLoanBorrowOrderDetail struct { + OrderID string `json:"ordId"` + AccruedInterest string `json:"accruedInterest"` + ActualBorrowAmount types.Number `json:"actualBorrowAmt"` + CreateTime types.Time `json:"cTime"` + Currency string `json:"ccy"` + CurRate types.Number `json:"curRate"` + DeadlinePenaltyInterest types.Number `json:"deadlinePenaltyInterest"` + EarlyRepayPenaltyInterest types.Number `json:"earlyRepayPenaltyInterest"` + ExpiryTime types.Time `json:"expiryTime"` + FailedReason string `json:"failedReason"` + ForceRepayTime types.Time `json:"forceRepayTime"` + OverduePenaltyInterest types.Number `json:"overduePenaltyInterest"` + PotentialPenaltyInterest types.Number `json:"potentialPenaltyInterest"` + Reborrow bool `json:"reborrow"` + ReborrowRate types.Number `json:"reborrowRate"` + ReqBorrowAmount types.Number `json:"reqBorrowAmt"` + SettleReason string `json:"settleReason"` + State string `json:"state"` + Term string `json:"term"` + UpdateTime types.Time `json:"uTime"` +} + +// BorrowOrRepay represents a borrow and repay operation response +type BorrowOrRepay struct { + Currency string `json:"ccy"` + Side string `json:"side"` + Amount types.Number `json:"amt"` +} + +// AutoRepay represents an auto-repay request and response +type AutoRepay struct { + AutoRepay bool `json:"autoRepay"` +} + +// BorrowRepayItem represents a borrow/repay history +type BorrowRepayItem struct { + AccBorrowed string `json:"accBorrowed"` + Amount types.Number `json:"amt"` + Currency string `json:"ccy"` + Timestamp types.Time `json:"ts"` + EventType string `json:"type"` +} + +// PositionBuilderParam represents a position builder parameters +type PositionBuilderParam struct { + InclRealPosAndEq bool `json:"inclRealPosAndEq"` + SimPos []SimulatedPosition `json:"simPos"` + SimAsset []SimulatedAsset `json:"simAsset"` + SpotOffsetType string `json:"spotOffsetType"` + GreeksType string `json:"greeksType"` +} + +// SimulatedPosition represents a simulated position detail of a new position builder +type SimulatedPosition struct { + Position string `json:"pos"` + InstrumentID string `json:"instId"` +} + +// SimulatedAsset represents a simulated asset detail +type SimulatedAsset struct { + Currency string `json:"ccy"` + Amount types.Number `json:"amt"` +} + +// PositionBuilderDetail represents details of portfolio margin information for virtual position/assets or current position of the user +type PositionBuilderDetail struct { + Assets []struct { + AvailEq types.Number `json:"availEq"` + BorrowIMR types.Number `json:"borrowImr"` + BorrowMMR types.Number `json:"borrowMmr"` + Currency string `json:"ccy"` + SpotInUse string `json:"spotInUse"` + } `json:"assets"` + BorrowMMR string `json:"borrowMmr"` + DerivMMR string `json:"derivMmr"` + Equity string `json:"eq"` + MarginRatio types.Number `json:"marginRatio"` + RiskUnitData []struct { + Delta string `json:"delta"` + Gamma string `json:"gamma"` + IMR string `json:"imr"` + IndexUsd string `json:"indexUsd"` + Mmr string `json:"mmr"` + Mr1 string `json:"mr1"` + Mr1FinalResult struct { + PNL types.Number `json:"pnl"` + SpotShock string `json:"spotShock"` + VolShock string `json:"volShock"` + } `json:"mr1FinalResult"` + Mr1Scenarios struct { + VolSame map[string]string `json:"volSame"` + VolShockDown map[string]string `json:"volShockDown"` + VolShockUp map[string]string `json:"volShockUp"` + } `json:"mr1Scenarios"` + Mr2 string `json:"mr2"` + Mr3 string `json:"mr3"` + Mr4 string `json:"mr4"` + Mr5 string `json:"mr5"` + Mr6 string `json:"mr6"` + Mr6FinalResult struct { + PNL types.Number `json:"pnl"` + SpotShock string `json:"spotShock"` + } `json:"mr6FinalResult"` + Mr7 string `json:"mr7"` + Portfolios []struct { + Amount types.Number `json:"amt"` + Delta types.Number `json:"delta"` + Gamma types.Number `json:"gamma"` + InstrumentID string `json:"instId"` + InstrumentType string `json:"instType"` + IsRealPos bool `json:"isRealPos"` + NotionalUsd string `json:"notionalUsd"` + Theta string `json:"theta"` + Vega string `json:"vega"` + } `json:"portfolios"` + RiskUnit string `json:"riskUnit"` + Theta string `json:"theta"` + Vega string `json:"vega"` + } `json:"riskUnitData"` + TotalImr types.Number `json:"totalImr"` + TotalMmr types.Number `json:"totalMmr"` + Timestamp types.Time `json:"ts"` +} + +// RiskOffsetAmount represents risk offset amount +type RiskOffsetAmount struct { + Currency string `json:"ccy"` + ClientSpotInUseAmount types.Number `json:"clSpotInUseAmt"` +} + +// AccountRateLimit represents an account rate limit details +type AccountRateLimit struct { + AccRateLimit types.Number `json:"accRateLimit"` + FillRatio types.Number `json:"fillRatio"` + MainFillRatio types.Number `json:"mainFillRatio"` + NextAccRateLimit types.Number `json:"nextAccRateLimit"` + Timestamp types.Time `json:"ts"` +} + +// OrderPreCheckParams represents an order pre-check parameters +type OrderPreCheckParams struct { + InstrumentID string `json:"instId"` + TradeMode string `json:"tdMode"` + ClientOrderID string `json:"clOrdId"` + Side string `json:"side"` + PositionSide string `json:"posSide"` + OrderType string `json:"ordType"` + Size float64 `json:"sz,omitempty"` + Price float64 `json:"px,omitempty"` + ReduceOnly bool `json:"reduceOnly,omitempty"` + TargetCurrency string `json:"tgtCcy,omitempty"` + AttachAlgoOrders []AlgoOrderInfo `json:"attachAlgoOrds,omitempty"` +} + +// AlgoOrderInfo represents an algo order info +type AlgoOrderInfo struct { + AttachAlgoClientOrderID string `json:"attachAlgoClOrdId,omitempty"` + TPTriggerPrice types.Number `json:"tpTriggerPx,omitempty"` + TPOrderPrice types.Number `json:"tpOrdPx,omitempty"` + TPOrderKind string `json:"tpOrdKind,omitempty"` + StopLossTriggerPrice types.Number `json:"slTriggerPx,omitempty"` + StopLossOrderPrice types.Number `json:"slOrdPx,omitempty"` + TPTriggerPriceType string `json:"tpTriggerPxType,omitempty"` + StopLossTriggerPriceType string `json:"slTriggerPxType,omitempty"` + Size types.Number `json:"sz,omitempty"` +} + +// OrderPreCheckResponse represents an order pre-checks response of account information for placing orders +type OrderPreCheckResponse struct { + AdjEq types.Number `json:"adjEq"` + AdjEqChg types.Number `json:"adjEqChg"` + AvailBal types.Number `json:"availBal"` + AvailBalChg types.Number `json:"availBalChg"` + IMR types.Number `json:"imr"` + IMRChg types.Number `json:"imrChg"` + Liab types.Number `json:"liab"` + LiabChg types.Number `json:"liabChg"` + LiabChgCurrency string `json:"liabChgCcy"` + LiquidiationPrice types.Number `json:"liqPx"` + LiquidiationPriceDiff string `json:"liqPxDiff"` + LiquidiationPriceDiffRatio types.Number `json:"liqPxDiffRatio"` + MgnRatio types.Number `json:"mgnRatio"` + MgnRatioChg types.Number `json:"mgnRatioChg"` + MMR types.Number `json:"mmr"` + MMRChange types.Number `json:"mmrChg"` + PosBalance types.Number `json:"posBal"` + PosBalChange types.Number `json:"posBalChg"` + Type string `json:"type"` +} + +// AnnouncementDetail represents an exchange's announcement detail +type AnnouncementDetail struct { + Details []struct { + AnnouncementType string `json:"annType"` + PushTime types.Time `json:"pTime"` + Title string `json:"title"` + URL string `json:"url"` + } `json:"details"` + TotalPage types.Number `json:"totalPage"` +} + +// AnnouncementTypeInfo represents an announcement type sample and it's description +type AnnouncementTypeInfo struct { + AnnouncementType string `json:"annType"` + AnnouncementTypeDesc string `json:"annTypeDesc"` +} + +// FiatOrderDetail represents a fiat deposit/withdrawal order detail +type FiatOrderDetail struct { + CreatTime types.Time `json:"cTime"` + UpdateTime types.Time `json:"uTime"` + OrdID string `json:"ordId"` + PaymentMethod string `json:"paymentMethod"` + PaymentAcctID string `json:"paymentAcctId"` + Amount types.Number `json:"amt"` + Fee types.Number `json:"fee"` + Currency string `json:"ccy"` + State string `json:"state"` + ClientID string `json:"clientId"` + PaymentMethodID string `json:"paymentMethodId,omitempty"` +} + +// OrderIDAndState represents an orderID and state information +type OrderIDAndState struct { + OrderID string `json:"ordId"` + State string `json:"state"` +} + +// FiatWithdrawalPaymentMethods represents a detailed information about fiat asset withdrawal payment methods and accounts +type FiatWithdrawalPaymentMethods struct { + Currency string `json:"ccy"` + PaymentMethod string `json:"paymentMethod"` + FeeRate types.Number `json:"feeRate"` + MinFee types.Number `json:"minFee"` + Limits struct { + DailyLimit types.Number `json:"dailyLimit"` + DailyLimitRemaining types.Number `json:"dailyLimitRemaining"` + WeeklyLimit types.Number `json:"weeklyLimit"` + WeeklyLimitRemaining types.Number `json:"weeklyLimitRemaining"` + MonthlyLimit types.Number `json:"monthlyLimit"` + MonthlyLimitRemaining types.Number `json:"monthlyLimitRemaining"` + MaxAmount types.Number `json:"maxAmt"` + MinAmount types.Number `json:"minAmt"` + LifetimeLimit types.Number `json:"lifetimeLimit"` + } `json:"limits"` + Accounts []struct { + PaymentAcctID string `json:"paymentAcctId"` + AccountNumber string `json:"acctNum"` + RecipientName string `json:"recipientName"` + BankName string `json:"bankName"` + BankCode string `json:"bankCode"` + State string `json:"state"` + } `json:"accounts"` +} + +// FiatDepositPaymentMethods represents a fiat deposit payment methods +type FiatDepositPaymentMethods struct { + Currency string `json:"ccy"` + PaymentMethod string `json:"paymentMethod"` + FeeRate types.Number `json:"feeRate"` + MinFee types.Number `json:"minFee"` + Limits struct { + DailyLimit types.Number `json:"dailyLimit"` + DailyLimitRemaining types.Number `json:"dailyLimitRemaining"` + WeeklyLimit types.Number `json:"weeklyLimit"` + WeeklyLimitRemaining types.Number `json:"weeklyLimitRemaining"` + MonthlyLimit types.Number `json:"monthlyLimit"` + MonthlyLimitRemaining types.Number `json:"monthlyLimitRemaining"` + MaxAmount types.Number `json:"maxAmt"` + MinAmount types.Number `json:"minAmt"` + LifetimeLimit types.Number `json:"lifetimeLimit"` + } `json:"limits"` + Accounts []struct { + PaymentAcctID string `json:"paymentAcctId"` + AccountNumber string `json:"acctNum"` + RecipientName string `json:"recipientName"` + BankName string `json:"bankName"` + BankCode string `json:"bankCode"` + State string `json:"state"` + } `json:"accounts"` +} + +// MonthlyStatement represents the information and download link for a monthly statement document. +type MonthlyStatement struct { + FileHref string `json:"fileHref"` + State string `json:"state"` + Timestamp types.Time `json:"ts"` } diff --git a/exchanges/okx/okx_websocket.go b/exchanges/okx/okx_websocket.go index 8303452521a..5546dda786f 100644 --- a/exchanges/okx/okx_websocket.go +++ b/exchanges/okx/okx_websocket.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/request" @@ -26,16 +27,13 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/log" + "github.com/thrasher-corp/gocryptotrader/types" ) var ( - errInvalidChecksum = errors.New("invalid checksum") -) - -var ( - candlestickChannelsMap = map[string]bool{okxChannelCandle1Y: true, okxChannelCandle6M: true, okxChannelCandle3M: true, okxChannelCandle1M: true, okxChannelCandle1W: true, okxChannelCandle1D: true, okxChannelCandle2D: true, okxChannelCandle3D: true, okxChannelCandle5D: true, okxChannelCandle12H: true, okxChannelCandle6H: true, okxChannelCandle4H: true, okxChannelCandle2H: true, okxChannelCandle1H: true, okxChannelCandle30m: true, okxChannelCandle15m: true, okxChannelCandle5m: true, okxChannelCandle3m: true, okxChannelCandle1m: true, okxChannelCandle1Yutc: true, okxChannelCandle3Mutc: true, okxChannelCandle1Mutc: true, okxChannelCandle1Wutc: true, okxChannelCandle1Dutc: true, okxChannelCandle2Dutc: true, okxChannelCandle3Dutc: true, okxChannelCandle5Dutc: true, okxChannelCandle12Hutc: true, okxChannelCandle6Hutc: true} - candlesticksMarkPriceMap = map[string]bool{okxChannelMarkPriceCandle1Y: true, okxChannelMarkPriceCandle6M: true, okxChannelMarkPriceCandle3M: true, okxChannelMarkPriceCandle1M: true, okxChannelMarkPriceCandle1W: true, okxChannelMarkPriceCandle1D: true, okxChannelMarkPriceCandle2D: true, okxChannelMarkPriceCandle3D: true, okxChannelMarkPriceCandle5D: true, okxChannelMarkPriceCandle12H: true, okxChannelMarkPriceCandle6H: true, okxChannelMarkPriceCandle4H: true, okxChannelMarkPriceCandle2H: true, okxChannelMarkPriceCandle1H: true, okxChannelMarkPriceCandle30m: true, okxChannelMarkPriceCandle15m: true, okxChannelMarkPriceCandle5m: true, okxChannelMarkPriceCandle3m: true, okxChannelMarkPriceCandle1m: true, okxChannelMarkPriceCandle1Yutc: true, okxChannelMarkPriceCandle3Mutc: true, okxChannelMarkPriceCandle1Mutc: true, okxChannelMarkPriceCandle1Wutc: true, okxChannelMarkPriceCandle1Dutc: true, okxChannelMarkPriceCandle2Dutc: true, okxChannelMarkPriceCandle3Dutc: true, okxChannelMarkPriceCandle5Dutc: true, okxChannelMarkPriceCandle12Hutc: true, okxChannelMarkPriceCandle6Hutc: true} - candlesticksIndexPriceMap = map[string]bool{okxChannelIndexCandle1Y: true, okxChannelIndexCandle6M: true, okxChannelIndexCandle3M: true, okxChannelIndexCandle1M: true, okxChannelIndexCandle1W: true, okxChannelIndexCandle1D: true, okxChannelIndexCandle2D: true, okxChannelIndexCandle3D: true, okxChannelIndexCandle5D: true, okxChannelIndexCandle12H: true, okxChannelIndexCandle6H: true, okxChannelIndexCandle4H: true, okxChannelIndexCandle2H: true, okxChannelIndexCandle1H: true, okxChannelIndexCandle30m: true, okxChannelIndexCandle15m: true, okxChannelIndexCandle5m: true, okxChannelIndexCandle3m: true, okxChannelIndexCandle1m: true, okxChannelIndexCandle1Yutc: true, okxChannelIndexCandle3Mutc: true, okxChannelIndexCandle1Mutc: true, okxChannelIndexCandle1Wutc: true, okxChannelIndexCandle1Dutc: true, okxChannelIndexCandle2Dutc: true, okxChannelIndexCandle3Dutc: true, okxChannelIndexCandle5Dutc: true, okxChannelIndexCandle12Hutc: true, okxChannelIndexCandle6Hutc: true} + candlestickChannelsMap = map[string]bool{channelCandle1Y: true, channelCandle6M: true, channelCandle3M: true, channelCandle1M: true, channelCandle1W: true, channelCandle1D: true, channelCandle2D: true, channelCandle3D: true, channelCandle5D: true, channelCandle12H: true, channelCandle6H: true, channelCandle4H: true, channelCandle2H: true, channelCandle1H: true, channelCandle30m: true, channelCandle15m: true, channelCandle5m: true, channelCandle3m: true, channelCandle1m: true, channelCandle1Yutc: true, channelCandle3Mutc: true, channelCandle1Mutc: true, channelCandle1Wutc: true, channelCandle1Dutc: true, channelCandle2Dutc: true, channelCandle3Dutc: true, channelCandle5Dutc: true, channelCandle12Hutc: true, channelCandle6Hutc: true} + candlesticksMarkPriceMap = map[string]bool{channelMarkPriceCandle1Y: true, channelMarkPriceCandle6M: true, channelMarkPriceCandle3M: true, channelMarkPriceCandle1M: true, channelMarkPriceCandle1W: true, channelMarkPriceCandle1D: true, channelMarkPriceCandle2D: true, channelMarkPriceCandle3D: true, channelMarkPriceCandle5D: true, channelMarkPriceCandle12H: true, channelMarkPriceCandle6H: true, channelMarkPriceCandle4H: true, channelMarkPriceCandle2H: true, channelMarkPriceCandle1H: true, channelMarkPriceCandle30m: true, channelMarkPriceCandle15m: true, channelMarkPriceCandle5m: true, channelMarkPriceCandle3m: true, channelMarkPriceCandle1m: true, channelMarkPriceCandle1Yutc: true, channelMarkPriceCandle3Mutc: true, channelMarkPriceCandle1Mutc: true, channelMarkPriceCandle1Wutc: true, channelMarkPriceCandle1Dutc: true, channelMarkPriceCandle2Dutc: true, channelMarkPriceCandle3Dutc: true, channelMarkPriceCandle5Dutc: true, channelMarkPriceCandle12Hutc: true, channelMarkPriceCandle6Hutc: true} + candlesticksIndexPriceMap = map[string]bool{channelIndexCandle1Y: true, channelIndexCandle6M: true, channelIndexCandle3M: true, channelIndexCandle1M: true, channelIndexCandle1W: true, channelIndexCandle1D: true, channelIndexCandle2D: true, channelIndexCandle3D: true, channelIndexCandle5D: true, channelIndexCandle12H: true, channelIndexCandle6H: true, channelIndexCandle4H: true, channelIndexCandle2H: true, channelIndexCandle1H: true, channelIndexCandle30m: true, channelIndexCandle15m: true, channelIndexCandle5m: true, channelIndexCandle3m: true, channelIndexCandle1m: true, channelIndexCandle1Yutc: true, channelIndexCandle3Mutc: true, channelIndexCandle1Mutc: true, channelIndexCandle1Wutc: true, channelIndexCandle1Dutc: true, channelIndexCandle2Dutc: true, channelIndexCandle3Dutc: true, channelIndexCandle5Dutc: true, channelIndexCandle12Hutc: true, channelIndexCandle6Hutc: true} ) var ( @@ -60,44 +58,73 @@ const ( indexCandlestick = "index-" candle = "candle" + // Spread Order + + // Operations + okxSpreadOrder = "sprd-order" + okxSpreadAmendOrder = "sprd-amend-order" + okxSpreadCancelOrder = "sprd-cancel-order" + okxSpreadCancelAllOrders = "sprd-mass-cancel" + + // Subscriptions + okxSpreadOrders = "sprd-orders" + okxSpreadTrades = "sprd-trades" + + // Public Spread Subscriptions + okxSpreadOrderbookLevel1 = "sprd-bbo-tbt" + okxSpreadOrderbook = "sprd-books5" + okxSpreadPublicTrades = "sprd-public-trades" + okxSpreadPublicTicker = "sprd-tickers" + + // Withdrawal Info Channel subscriptions + okxWithdrawalInfo = "withdrawal-info" + okxDepositInfo = "deposit-info" + // Ticker channel - okxChannelTickers = "tickers" - okxChannelIndexTickers = "index-tickers" - okxChannelStatus = "status" - okxChannelPublicStrucBlockTrades = "public-struc-block-trades" - okxChannelBlockTickers = "block-tickers" + channelTickers = "tickers" + channelIndexTickers = "index-tickers" + channelStatus = "status" + channelPublicStrucBlockTrades = "public-struc-block-trades" + channelPublicBlockTrades = "public-block-trades" + channelBlockTickers = "block-tickers" // Private Channels - okxChannelAccount = "account" - okxChannelPositions = "positions" - okxChannelBalanceAndPosition = "balance_and_position" - okxChannelOrders = "orders" - okxChannelAlgoOrders = "orders-algo" - okxChannelAlgoAdvance = "algo-advance" - okxChannelLiquidationWarning = "liquidation-warning" - okxChannelAccountGreeks = "account-greeks" - okxChannelRfqs = "rfqs" - okxChannelQuotes = "quotes" - okxChannelStructureBlockTrades = "struc-block-trades" - okxChannelSpotGridOrder = "grid-orders-spot" - okxChannelGridOrdersContract = "grid-orders-contract" - okxChannelGridPositions = "grid-positions" - okcChannelGridSubOrders = "grid-sub-orders" + channelAccount = "account" + channelPositions = "positions" + channelBalanceAndPosition = "balance_and_position" + channelOrders = "orders" + channelAlgoOrders = "orders-algo" + channelAlgoAdvance = "algo-advance" + channelLiquidationWarning = "liquidation-warning" + channelAccountGreeks = "account-greeks" + channelRFQs = "rfqs" + channelQuotes = "quotes" + channelStructureBlockTrades = "struc-block-trades" + channelSpotGridOrder = "grid-orders-spot" + channelGridOrdersContract = "grid-orders-contract" + channelGridPositions = "grid-positions" + channelGridSubOrders = "grid-sub-orders" + channelRecurringBuy = "algo-recurring-buy" + liquidationOrders = "liquidation-orders" + adlWarning = "adl-warning" + economicCalendar = "economic-calendar" // Public channels - okxChannelInstruments = "instruments" - okxChannelOpenInterest = "open-interest" - okxChannelTrades = "trades" - okxChannelEstimatedPrice = "estimated-price" - okxChannelMarkPrice = "mark-price" - okxChannelPriceLimit = "price-limit" - okxChannelOrderBooks = "books" - okxChannelOrderBooks5 = "books5" - okxChannelOrderBooks50TBT = "books50-l2-tbt" - okxChannelOrderBooksTBT = "books-l2-tbt" - okxChannelBBOTBT = "bbo-tbt" - okxChannelOptSummary = "opt-summary" - okxChannelFundingRate = "funding-rate" + channelInstruments = "instruments" + channelOpenInterest = "open-interest" + channelTrades = "trades" + channelAllTrades = "trades-all" + channelEstimatedPrice = "estimated-price" + channelMarkPrice = "mark-price" + channelPriceLimit = "price-limit" + channelOrderBooks = "books" + channelOptionTrades = "option-trades" + channelOrderBooks5 = "books5" + channelOrderBooks50TBT = "books50-l2-tbt" + channelOrderBooksTBT = "books-l2-tbt" + channelBBOTBT = "bbo-tbt" + channelOptSummary = "opt-summary" + channelFundingRate = "funding-rate" // Websocket trade endpoint operations okxOpOrder = "order" @@ -106,99 +133,103 @@ const ( okxOpBatchCancelOrders = "batch-cancel-orders" okxOpAmendOrder = "amend-order" okxOpBatchAmendOrders = "batch-amend-orders" + okxOpMassCancelOrder = "mass-cancel" // Candlestick lengths - okxChannelCandle1Y = candle + "1Y" - okxChannelCandle6M = candle + "6M" - okxChannelCandle3M = candle + "3M" - okxChannelCandle1M = candle + "1M" - okxChannelCandle1W = candle + "1W" - okxChannelCandle1D = candle + "1D" - okxChannelCandle2D = candle + "2D" - okxChannelCandle3D = candle + "3D" - okxChannelCandle5D = candle + "5D" - okxChannelCandle12H = candle + "12H" - okxChannelCandle6H = candle + "6H" - okxChannelCandle4H = candle + "4H" - okxChannelCandle2H = candle + "2H" - okxChannelCandle1H = candle + "1H" - okxChannelCandle30m = candle + "30m" - okxChannelCandle15m = candle + "15m" - okxChannelCandle5m = candle + "5m" - okxChannelCandle3m = candle + "3m" - okxChannelCandle1m = candle + "1m" - okxChannelCandle1Yutc = candle + "1Yutc" - okxChannelCandle3Mutc = candle + "3Mutc" - okxChannelCandle1Mutc = candle + "1Mutc" - okxChannelCandle1Wutc = candle + "1Wutc" - okxChannelCandle1Dutc = candle + "1Dutc" - okxChannelCandle2Dutc = candle + "2Dutc" - okxChannelCandle3Dutc = candle + "3Dutc" - okxChannelCandle5Dutc = candle + "5Dutc" - okxChannelCandle12Hutc = candle + "12Hutc" - okxChannelCandle6Hutc = candle + "6Hutc" + channelCandle1Y = candle + "1Y" + channelCandle6M = candle + "6M" + channelCandle3M = candle + "3M" + channelCandle1M = candle + "1M" + channelCandle1W = candle + "1W" + channelCandle1D = candle + "1D" + channelCandle2D = candle + "2D" + channelCandle3D = candle + "3D" + channelCandle5D = candle + "5D" + channelCandle12H = candle + "12H" + channelCandle6H = candle + "6H" + channelCandle4H = candle + "4H" + channelCandle2H = candle + "2H" + channelCandle1H = candle + "1H" + channelCandle30m = candle + "30m" + channelCandle15m = candle + "15m" + channelCandle5m = candle + "5m" + channelCandle3m = candle + "3m" + channelCandle1m = candle + "1m" + channelCandle1Yutc = candle + "1Yutc" + channelCandle3Mutc = candle + "3Mutc" + channelCandle1Mutc = candle + "1Mutc" + channelCandle1Wutc = candle + "1Wutc" + channelCandle1Dutc = candle + "1Dutc" + channelCandle2Dutc = candle + "2Dutc" + channelCandle3Dutc = candle + "3Dutc" + channelCandle5Dutc = candle + "5Dutc" + channelCandle12Hutc = candle + "12Hutc" + channelCandle6Hutc = candle + "6Hutc" // Index Candlesticks Channels - okxChannelIndexCandle1Y = indexCandlestick + okxChannelCandle1Y - okxChannelIndexCandle6M = indexCandlestick + okxChannelCandle6M - okxChannelIndexCandle3M = indexCandlestick + okxChannelCandle3M - okxChannelIndexCandle1M = indexCandlestick + okxChannelCandle1M - okxChannelIndexCandle1W = indexCandlestick + okxChannelCandle1W - okxChannelIndexCandle1D = indexCandlestick + okxChannelCandle1D - okxChannelIndexCandle2D = indexCandlestick + okxChannelCandle2D - okxChannelIndexCandle3D = indexCandlestick + okxChannelCandle3D - okxChannelIndexCandle5D = indexCandlestick + okxChannelCandle5D - okxChannelIndexCandle12H = indexCandlestick + okxChannelCandle12H - okxChannelIndexCandle6H = indexCandlestick + okxChannelCandle6H - okxChannelIndexCandle4H = indexCandlestick + okxChannelCandle4H - okxChannelIndexCandle2H = indexCandlestick + okxChannelCandle2H - okxChannelIndexCandle1H = indexCandlestick + okxChannelCandle1H - okxChannelIndexCandle30m = indexCandlestick + okxChannelCandle30m - okxChannelIndexCandle15m = indexCandlestick + okxChannelCandle15m - okxChannelIndexCandle5m = indexCandlestick + okxChannelCandle5m - okxChannelIndexCandle3m = indexCandlestick + okxChannelCandle3m - okxChannelIndexCandle1m = indexCandlestick + okxChannelCandle1m - okxChannelIndexCandle1Yutc = indexCandlestick + okxChannelCandle1Yutc - okxChannelIndexCandle3Mutc = indexCandlestick + okxChannelCandle3Mutc - okxChannelIndexCandle1Mutc = indexCandlestick + okxChannelCandle1Mutc - okxChannelIndexCandle1Wutc = indexCandlestick + okxChannelCandle1Wutc - okxChannelIndexCandle1Dutc = indexCandlestick + okxChannelCandle1Dutc - okxChannelIndexCandle2Dutc = indexCandlestick + okxChannelCandle2Dutc - okxChannelIndexCandle3Dutc = indexCandlestick + okxChannelCandle3Dutc - okxChannelIndexCandle5Dutc = indexCandlestick + okxChannelCandle5Dutc - okxChannelIndexCandle12Hutc = indexCandlestick + okxChannelCandle12Hutc - okxChannelIndexCandle6Hutc = indexCandlestick + okxChannelCandle6Hutc + channelIndexCandle1Y = indexCandlestick + channelCandle1Y + channelIndexCandle6M = indexCandlestick + channelCandle6M + channelIndexCandle3M = indexCandlestick + channelCandle3M + channelIndexCandle1M = indexCandlestick + channelCandle1M + channelIndexCandle1W = indexCandlestick + channelCandle1W + channelIndexCandle1D = indexCandlestick + channelCandle1D + channelIndexCandle2D = indexCandlestick + channelCandle2D + channelIndexCandle3D = indexCandlestick + channelCandle3D + channelIndexCandle5D = indexCandlestick + channelCandle5D + channelIndexCandle12H = indexCandlestick + channelCandle12H + channelIndexCandle6H = indexCandlestick + channelCandle6H + channelIndexCandle4H = indexCandlestick + channelCandle4H + channelIndexCandle2H = indexCandlestick + channelCandle2H + channelIndexCandle1H = indexCandlestick + channelCandle1H + channelIndexCandle30m = indexCandlestick + channelCandle30m + channelIndexCandle15m = indexCandlestick + channelCandle15m + channelIndexCandle5m = indexCandlestick + channelCandle5m + channelIndexCandle3m = indexCandlestick + channelCandle3m + channelIndexCandle1m = indexCandlestick + channelCandle1m + channelIndexCandle1Yutc = indexCandlestick + channelCandle1Yutc + channelIndexCandle3Mutc = indexCandlestick + channelCandle3Mutc + channelIndexCandle1Mutc = indexCandlestick + channelCandle1Mutc + channelIndexCandle1Wutc = indexCandlestick + channelCandle1Wutc + channelIndexCandle1Dutc = indexCandlestick + channelCandle1Dutc + channelIndexCandle2Dutc = indexCandlestick + channelCandle2Dutc + channelIndexCandle3Dutc = indexCandlestick + channelCandle3Dutc + channelIndexCandle5Dutc = indexCandlestick + channelCandle5Dutc + channelIndexCandle12Hutc = indexCandlestick + channelCandle12Hutc + channelIndexCandle6Hutc = indexCandlestick + channelCandle6Hutc // Mark price candlesticks channel - okxChannelMarkPriceCandle1Y = markPrice + okxChannelCandle1Y - okxChannelMarkPriceCandle6M = markPrice + okxChannelCandle6M - okxChannelMarkPriceCandle3M = markPrice + okxChannelCandle3M - okxChannelMarkPriceCandle1M = markPrice + okxChannelCandle1M - okxChannelMarkPriceCandle1W = markPrice + okxChannelCandle1W - okxChannelMarkPriceCandle1D = markPrice + okxChannelCandle1D - okxChannelMarkPriceCandle2D = markPrice + okxChannelCandle2D - okxChannelMarkPriceCandle3D = markPrice + okxChannelCandle3D - okxChannelMarkPriceCandle5D = markPrice + okxChannelCandle5D - okxChannelMarkPriceCandle12H = markPrice + okxChannelCandle12H - okxChannelMarkPriceCandle6H = markPrice + okxChannelCandle6H - okxChannelMarkPriceCandle4H = markPrice + okxChannelCandle4H - okxChannelMarkPriceCandle2H = markPrice + okxChannelCandle2H - okxChannelMarkPriceCandle1H = markPrice + okxChannelCandle1H - okxChannelMarkPriceCandle30m = markPrice + okxChannelCandle30m - okxChannelMarkPriceCandle15m = markPrice + okxChannelCandle15m - okxChannelMarkPriceCandle5m = markPrice + okxChannelCandle5m - okxChannelMarkPriceCandle3m = markPrice + okxChannelCandle3m - okxChannelMarkPriceCandle1m = markPrice + okxChannelCandle1m - okxChannelMarkPriceCandle1Yutc = markPrice + okxChannelCandle1Yutc - okxChannelMarkPriceCandle3Mutc = markPrice + okxChannelCandle3Mutc - okxChannelMarkPriceCandle1Mutc = markPrice + okxChannelCandle1Mutc - okxChannelMarkPriceCandle1Wutc = markPrice + okxChannelCandle1Wutc - okxChannelMarkPriceCandle1Dutc = markPrice + okxChannelCandle1Dutc - okxChannelMarkPriceCandle2Dutc = markPrice + okxChannelCandle2Dutc - okxChannelMarkPriceCandle3Dutc = markPrice + okxChannelCandle3Dutc - okxChannelMarkPriceCandle5Dutc = markPrice + okxChannelCandle5Dutc - okxChannelMarkPriceCandle12Hutc = markPrice + okxChannelCandle12Hutc - okxChannelMarkPriceCandle6Hutc = markPrice + okxChannelCandle6Hutc + channelMarkPriceCandle1Y = markPrice + channelCandle1Y + channelMarkPriceCandle6M = markPrice + channelCandle6M + channelMarkPriceCandle3M = markPrice + channelCandle3M + channelMarkPriceCandle1M = markPrice + channelCandle1M + channelMarkPriceCandle1W = markPrice + channelCandle1W + channelMarkPriceCandle1D = markPrice + channelCandle1D + channelMarkPriceCandle2D = markPrice + channelCandle2D + channelMarkPriceCandle3D = markPrice + channelCandle3D + channelMarkPriceCandle5D = markPrice + channelCandle5D + channelMarkPriceCandle12H = markPrice + channelCandle12H + channelMarkPriceCandle6H = markPrice + channelCandle6H + channelMarkPriceCandle4H = markPrice + channelCandle4H + channelMarkPriceCandle2H = markPrice + channelCandle2H + channelMarkPriceCandle1H = markPrice + channelCandle1H + channelMarkPriceCandle30m = markPrice + channelCandle30m + channelMarkPriceCandle15m = markPrice + channelCandle15m + channelMarkPriceCandle5m = markPrice + channelCandle5m + channelMarkPriceCandle3m = markPrice + channelCandle3m + channelMarkPriceCandle1m = markPrice + channelCandle1m + channelMarkPriceCandle1Yutc = markPrice + channelCandle1Yutc + channelMarkPriceCandle3Mutc = markPrice + channelCandle3Mutc + channelMarkPriceCandle1Mutc = markPrice + channelCandle1Mutc + channelMarkPriceCandle1Wutc = markPrice + channelCandle1Wutc + channelMarkPriceCandle1Dutc = markPrice + channelCandle1Dutc + channelMarkPriceCandle2Dutc = markPrice + channelCandle2Dutc + channelMarkPriceCandle3Dutc = markPrice + channelCandle3Dutc + channelMarkPriceCandle5Dutc = markPrice + channelCandle5Dutc + channelMarkPriceCandle12Hutc = markPrice + channelCandle12Hutc + channelMarkPriceCandle6Hutc = markPrice + channelCandle6Hutc + + // Copy trading websocket endpoints. + copyTrading = "copytrading-notification" ) var defaultSubscriptions = subscription.List{ @@ -210,11 +241,11 @@ var defaultSubscriptions = subscription.List{ } var subscriptionNames = map[string]string{ - subscription.AllTradesChannel: okxChannelTrades, - subscription.OrderbookChannel: okxChannelOrderBooks, - subscription.TickerChannel: okxChannelTickers, - subscription.MyAccountChannel: okxChannelAccount, - subscription.MyOrdersChannel: okxChannelOrders, + subscription.AllTradesChannel: channelTrades, + subscription.OrderbookChannel: channelOrderBooks, + subscription.TickerChannel: channelTickers, + subscription.MyAccountChannel: channelAccount, + subscription.MyOrdersChannel: channelOrders, } // WsConnect initiates a websocket connection @@ -241,11 +272,8 @@ func (ok *Okx) WsConnect() error { Message: pingMsg, Delay: time.Second * 20, }) - if ok.IsWebsocketAuthenticationSupported() { - var authDialer websocket.Dialer - authDialer.ReadBufferSize = 8192 - authDialer.WriteBufferSize = 8192 - err = ok.WsAuth(context.TODO(), &authDialer) + if ok.Websocket.CanUseAuthenticatedEndpoints() { + err = ok.WsAuth(context.TODO()) if err != nil { log.Errorf(log.ExchangeSys, "Error connecting auth socket: %s\n", err.Error()) ok.Websocket.SetCanUseAuthenticatedEndpoints(false) @@ -255,13 +283,18 @@ func (ok *Okx) WsConnect() error { } // WsAuth will connect to Okx's Private websocket connection and Authenticate with a login payload. -func (ok *Okx) WsAuth(ctx context.Context, dialer *websocket.Dialer) error { - if !ok.Websocket.CanUseAuthenticatedEndpoints() { +func (ok *Okx) WsAuth(ctx context.Context) error { + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", ok.Name) } - err := ok.Websocket.AuthConn.Dial(dialer, http.Header{}) + creds, err := ok.GetCredentials(ctx) + if err != nil { + return err + } + var dialer websocket.Dialer + err = ok.Websocket.AuthConn.Dial(&dialer, http.Header{}) if err != nil { - return fmt.Errorf("%v Websocket connection %v error. Error %v", ok.Name, okxAPIWebsocketPrivateURL, err) + return err } ok.Websocket.Wg.Add(1) go ok.wsReadData(ok.Websocket.AuthConn) @@ -270,10 +303,7 @@ func (ok *Okx) WsAuth(ctx context.Context, dialer *websocket.Dialer) error { Message: pingMsg, Delay: time.Second * 20, }) - creds, err := ok.GetCredentials(ctx) - if err != nil { - return err - } + ok.Websocket.SetCanUseAuthenticatedEndpoints(true) timeUnix := time.Now() signPath := "/users/self/verify" @@ -291,7 +321,7 @@ func (ok *Okx) WsAuth(ctx context.Context, dialer *websocket.Dialer) error { { APIKey: creds.Key, Passphrase: creds.ClientID, - Timestamp: timeUnix, + Timestamp: timeUnix.Unix(), Sign: base64Sign, }, }, @@ -319,13 +349,13 @@ func (ok *Okx) WsAuth(ctx context.Context, dialer *websocket.Dialer) error { for { select { case data := <-wsResponse: - if data.Event == operationLogin && data.Code == "0" { + if data.Event == operationLogin && data.StatusCode == "0" { ok.Websocket.SetCanUseAuthenticatedEndpoints(true) return nil } else if data.Event == "error" && - (data.Code == "60022" || data.Code == "60009") { + (data.StatusCode == "60022" || data.StatusCode == "60009" || data.StatusCode == "60004") { ok.Websocket.SetCanUseAuthenticatedEndpoints(false) - return fmt.Errorf("authentication failed with error: %v", ErrorCodes[data.Code]) + return fmt.Errorf("%w code: %s message: %s", errWebsocketStreamNotAuthenticated, data.StatusCode, data.Message) } continue case <-timer.C: @@ -448,7 +478,6 @@ func (ok *Okx) handleSubscription(operation string, subs subscription.List) erro if operation == operationUnsubscribe { return ok.Websocket.RemoveSubscriptions(ok.Websocket.Conn, channels...) } - return ok.Websocket.AddSuccessfulSubscriptions(ok.Websocket.Conn, channels...) } @@ -470,176 +499,311 @@ func (ok *Okx) WsHandleData(respRaw []byte) error { return nil } switch resp.Argument.Channel { - case okxChannelCandle1Y, okxChannelCandle6M, okxChannelCandle3M, okxChannelCandle1M, okxChannelCandle1W, - okxChannelCandle1D, okxChannelCandle2D, okxChannelCandle3D, okxChannelCandle5D, okxChannelCandle12H, - okxChannelCandle6H, okxChannelCandle4H, okxChannelCandle2H, okxChannelCandle1H, okxChannelCandle30m, - okxChannelCandle15m, okxChannelCandle5m, okxChannelCandle3m, okxChannelCandle1m, okxChannelCandle1Yutc, - okxChannelCandle3Mutc, okxChannelCandle1Mutc, okxChannelCandle1Wutc, okxChannelCandle1Dutc, - okxChannelCandle2Dutc, okxChannelCandle3Dutc, okxChannelCandle5Dutc, okxChannelCandle12Hutc, - okxChannelCandle6Hutc: + case channelCandle1Y, channelCandle6M, channelCandle3M, channelCandle1M, channelCandle1W, + channelCandle1D, channelCandle2D, channelCandle3D, channelCandle5D, channelCandle12H, + channelCandle6H, channelCandle4H, channelCandle2H, channelCandle1H, channelCandle30m, + channelCandle15m, channelCandle5m, channelCandle3m, channelCandle1m, channelCandle1Yutc, + channelCandle3Mutc, channelCandle1Mutc, channelCandle1Wutc, channelCandle1Dutc, + channelCandle2Dutc, channelCandle3Dutc, channelCandle5Dutc, channelCandle12Hutc, + channelCandle6Hutc: return ok.wsProcessCandles(respRaw) - case okxChannelIndexCandle1Y, okxChannelIndexCandle6M, okxChannelIndexCandle3M, okxChannelIndexCandle1M, - okxChannelIndexCandle1W, okxChannelIndexCandle1D, okxChannelIndexCandle2D, okxChannelIndexCandle3D, - okxChannelIndexCandle5D, okxChannelIndexCandle12H, okxChannelIndexCandle6H, okxChannelIndexCandle4H, - okxChannelIndexCandle2H, okxChannelIndexCandle1H, okxChannelIndexCandle30m, okxChannelIndexCandle15m, - okxChannelIndexCandle5m, okxChannelIndexCandle3m, okxChannelIndexCandle1m, okxChannelIndexCandle1Yutc, - okxChannelIndexCandle3Mutc, okxChannelIndexCandle1Mutc, okxChannelIndexCandle1Wutc, - okxChannelIndexCandle1Dutc, okxChannelIndexCandle2Dutc, okxChannelIndexCandle3Dutc, okxChannelIndexCandle5Dutc, - okxChannelIndexCandle12Hutc, okxChannelIndexCandle6Hutc: + case channelIndexCandle1Y, channelIndexCandle6M, channelIndexCandle3M, channelIndexCandle1M, + channelIndexCandle1W, channelIndexCandle1D, channelIndexCandle2D, channelIndexCandle3D, + channelIndexCandle5D, channelIndexCandle12H, channelIndexCandle6H, channelIndexCandle4H, + channelIndexCandle2H, channelIndexCandle1H, channelIndexCandle30m, channelIndexCandle15m, + channelIndexCandle5m, channelIndexCandle3m, channelIndexCandle1m, channelIndexCandle1Yutc, + channelIndexCandle3Mutc, channelIndexCandle1Mutc, channelIndexCandle1Wutc, + channelIndexCandle1Dutc, channelIndexCandle2Dutc, channelIndexCandle3Dutc, channelIndexCandle5Dutc, + channelIndexCandle12Hutc, channelIndexCandle6Hutc: return ok.wsProcessIndexCandles(respRaw) - case okxChannelTickers: + case channelTickers: return ok.wsProcessTickers(respRaw) - case okxChannelIndexTickers: + case channelIndexTickers: var response WsIndexTicker return ok.wsProcessPushData(respRaw, &response) - case okxChannelStatus: + case channelStatus: var response WsSystemStatusResponse return ok.wsProcessPushData(respRaw, &response) - case okxChannelPublicStrucBlockTrades: + case channelPublicStrucBlockTrades: var response WsPublicTradesResponse return ok.wsProcessPushData(respRaw, &response) - case okxChannelBlockTickers: + case channelPublicBlockTrades: + return ok.wsProcessBlockPublicTrades(respRaw) + case channelBlockTickers: var response WsBlockTicker return ok.wsProcessPushData(respRaw, &response) - case okxChannelAccountGreeks: + case channelAccountGreeks: var response WsGreeks return ok.wsProcessPushData(respRaw, &response) - case okxChannelAccount: + case channelAccount: var response WsAccountChannelPushData return ok.wsProcessPushData(respRaw, &response) - case okxChannelPositions, - okxChannelLiquidationWarning: + case channelPositions, + channelLiquidationWarning: var response WsPositionResponse return ok.wsProcessPushData(respRaw, &response) - case okxChannelBalanceAndPosition: + case channelBalanceAndPosition: var response WsBalanceAndPosition return ok.wsProcessPushData(respRaw, &response) - case okxChannelOrders: + case channelOrders: return ok.wsProcessOrders(respRaw) - case okxChannelAlgoOrders: + case channelAlgoOrders: var response WsAlgoOrder return ok.wsProcessPushData(respRaw, &response) - case okxChannelAlgoAdvance: + case channelAlgoAdvance: var response WsAdvancedAlgoOrder return ok.wsProcessPushData(respRaw, &response) - case okxChannelRfqs: - var response WsRfq + case channelRFQs: + var response WsRFQ return ok.wsProcessPushData(respRaw, &response) - case okxChannelQuotes: + case channelQuotes: var response WsQuote return ok.wsProcessPushData(respRaw, &response) - case okxChannelStructureBlockTrades: + case channelStructureBlockTrades: var response WsStructureBlocTrade return ok.wsProcessPushData(respRaw, &response) - case okxChannelSpotGridOrder: + case channelSpotGridOrder: var response WsSpotGridAlgoOrder return ok.wsProcessPushData(respRaw, &response) - case okxChannelGridOrdersContract: + case channelGridOrdersContract: var response WsContractGridAlgoOrder return ok.wsProcessPushData(respRaw, &response) - case okxChannelGridPositions: + case channelGridPositions: var response WsContractGridAlgoOrder return ok.wsProcessPushData(respRaw, &response) - case okcChannelGridSubOrders: + case channelGridSubOrders: var response WsGridSubOrderData return ok.wsProcessPushData(respRaw, &response) - case okxChannelInstruments: + case channelInstruments: var response WSInstrumentResponse return ok.wsProcessPushData(respRaw, &response) - case okxChannelOpenInterest: + case channelOpenInterest: var response WSOpenInterestResponse return ok.wsProcessPushData(respRaw, &response) - case okxChannelTrades: + case channelTrades, + channelAllTrades: return ok.wsProcessTrades(respRaw) - case okxChannelEstimatedPrice: + case channelEstimatedPrice: var response WsDeliveryEstimatedPrice return ok.wsProcessPushData(respRaw, &response) - case okxChannelMarkPrice, - okxChannelPriceLimit: + case channelMarkPrice, + channelPriceLimit: var response WsMarkPrice return ok.wsProcessPushData(respRaw, &response) - case okxChannelOrderBooks5: + case channelOrderBooks5: return ok.wsProcessOrderbook5(respRaw) - case okxChannelOrderBooks, - okxChannelOrderBooks50TBT, - okxChannelBBOTBT, - okxChannelOrderBooksTBT: + case okxSpreadOrderbookLevel1, + okxSpreadOrderbook: + return ok.wsProcessSpreadOrderbook(respRaw) + case okxSpreadPublicTrades: + return ok.wsProcessPublicSpreadTrades(respRaw) + case okxSpreadPublicTicker: + return ok.wsProcessPublicSpreadTicker(respRaw) + case channelOrderBooks, + channelOrderBooks50TBT, + channelBBOTBT, + channelOrderBooksTBT: return ok.wsProcessOrderBooks(respRaw) - case okxChannelOptSummary: + case channelOptionTrades: + return ok.wsProcessOptionTrades(respRaw) + case channelOptSummary: var response WsOptionSummary return ok.wsProcessPushData(respRaw, &response) - case okxChannelFundingRate: + case channelFundingRate: var response WsFundingRate return ok.wsProcessPushData(respRaw, &response) - case okxChannelMarkPriceCandle1Y, okxChannelMarkPriceCandle6M, okxChannelMarkPriceCandle3M, okxChannelMarkPriceCandle1M, - okxChannelMarkPriceCandle1W, okxChannelMarkPriceCandle1D, okxChannelMarkPriceCandle2D, okxChannelMarkPriceCandle3D, - okxChannelMarkPriceCandle5D, okxChannelMarkPriceCandle12H, okxChannelMarkPriceCandle6H, okxChannelMarkPriceCandle4H, - okxChannelMarkPriceCandle2H, okxChannelMarkPriceCandle1H, okxChannelMarkPriceCandle30m, okxChannelMarkPriceCandle15m, - okxChannelMarkPriceCandle5m, okxChannelMarkPriceCandle3m, okxChannelMarkPriceCandle1m, okxChannelMarkPriceCandle1Yutc, - okxChannelMarkPriceCandle3Mutc, okxChannelMarkPriceCandle1Mutc, okxChannelMarkPriceCandle1Wutc, okxChannelMarkPriceCandle1Dutc, - okxChannelMarkPriceCandle2Dutc, okxChannelMarkPriceCandle3Dutc, okxChannelMarkPriceCandle5Dutc, okxChannelMarkPriceCandle12Hutc, - okxChannelMarkPriceCandle6Hutc: + case channelMarkPriceCandle1Y, channelMarkPriceCandle6M, channelMarkPriceCandle3M, channelMarkPriceCandle1M, + channelMarkPriceCandle1W, channelMarkPriceCandle1D, channelMarkPriceCandle2D, channelMarkPriceCandle3D, + channelMarkPriceCandle5D, channelMarkPriceCandle12H, channelMarkPriceCandle6H, channelMarkPriceCandle4H, + channelMarkPriceCandle2H, channelMarkPriceCandle1H, channelMarkPriceCandle30m, channelMarkPriceCandle15m, + channelMarkPriceCandle5m, channelMarkPriceCandle3m, channelMarkPriceCandle1m, channelMarkPriceCandle1Yutc, + channelMarkPriceCandle3Mutc, channelMarkPriceCandle1Mutc, channelMarkPriceCandle1Wutc, channelMarkPriceCandle1Dutc, + channelMarkPriceCandle2Dutc, channelMarkPriceCandle3Dutc, channelMarkPriceCandle5Dutc, channelMarkPriceCandle12Hutc, + channelMarkPriceCandle6Hutc: return ok.wsHandleMarkPriceCandles(respRaw) + case okxSpreadOrders: + return ok.wsProcessSpreadOrders(respRaw) + case okxSpreadTrades: + return ok.wsProcessSpreadTrades(respRaw) + case okxWithdrawalInfo: + resp := &struct { + Arguments SubscriptionInfo `json:"arg"` + Data []WsDepositInfo `json:"data"` + }{} + return ok.wsProcessPushData(respRaw, resp) + case okxDepositInfo: + resp := &struct { + Arguments SubscriptionInfo `json:"arg"` + Data []WsWithdrawlInfo `json:"data"` + }{} + return ok.wsProcessPushData(respRaw, resp) + case channelRecurringBuy: + resp := &struct { + Arguments SubscriptionInfo `json:"arg"` + Data []RecurringBuyOrder `json:"data"` + }{} + return ok.wsProcessPushData(respRaw, resp) + case liquidationOrders: + var resp *LiquidationOrder + return ok.wsProcessPushData(respRaw, &resp) + case adlWarning: + var resp ADLWarning + return ok.wsProcessPushData(respRaw, &resp) + case economicCalendar: + var resp EconomicCalendarResponse + return ok.wsProcessPushData(respRaw, &resp) + case copyTrading: + var resp CopyTradingNotification + return ok.wsProcessPushData(respRaw, &resp) default: ok.Websocket.DataHandler <- stream.UnhandledMessageWarning{Message: ok.Name + stream.UnhandledMessage + string(respRaw)} return nil } } -// wsProcessIndexCandles processes index candlestick data -func (ok *Okx) wsProcessIndexCandles(respRaw []byte) error { +// wsProcessSpreadTrades handle and process spread order trades +func (ok *Okx) wsProcessSpreadTrades(respRaw []byte) error { if respRaw == nil { - return errNilArgument + return common.ErrNilPointer } - response := struct { - Argument SubscriptionInfo `json:"arg"` - Data [][5]string `json:"data"` - }{} - err := json.Unmarshal(respRaw, &response) + var resp WsSpreadOrderTrade + err := json.Unmarshal(respRaw, &resp) if err != nil { return err } - if len(response.Data) == 0 { - return errNoCandlestickDataFound + if len(resp.Data) == 0 { + return kline.ErrNoTimeSeriesDataToConvert } + pair, err := ok.GetPairFromInstrumentID(resp.Argument.SpreadID) + if err != nil { + return err + } + trades := make([]trade.Data, len(resp.Data)) + for x := range resp.Data { + oSide, err := order.StringToOrderSide(resp.Data[x].Side) + if err != nil { + return err + } + trades[x] = trade.Data{ + Amount: resp.Data[x].FillSize.Float64(), + AssetType: asset.Spread, + CurrencyPair: pair, + Exchange: ok.Name, + Side: oSide, + Timestamp: resp.Data[x].Timestamp.Time(), + TID: resp.Data[x].TradeID, + Price: resp.Data[x].FillPrice.Float64(), + } + } + return trade.AddTradesToBuffer(ok.Name, trades...) +} - pair, err := currency.NewPairFromString(response.Argument.InstrumentID) +// wsProcessSpreadOrders retrieve order information from the sprd-order Websocket channel. +// Data will not be pushed when first subscribed. +// Data will only be pushed when triggered by events such as placing/canceling order. +func (ok *Okx) wsProcessSpreadOrders(respRaw []byte) error { + if respRaw == nil { + return common.ErrNilPointer + } + resp := &struct { + Argument SubscriptionInfo `json:"arg"` + Data []WsSpreadOrder `json:"data"` + }{} + err := json.Unmarshal(respRaw, &resp) if err != nil { return err } - assets, err := ok.GetAssetsFromInstrumentTypeOrID(response.Argument.InstrumentType, response.Argument.InstrumentID) + if len(resp.Data) == 0 { + return kline.ErrNoTimeSeriesDataToConvert + } + pair, err := ok.GetPairFromInstrumentID(resp.Argument.SpreadID) if err != nil { return err } - candleInterval := strings.TrimPrefix(response.Argument.Channel, candle) - for i := range response.Data { - candlesData := response.Data[i] - timestamp, err := strconv.ParseInt(candlesData[0], 10, 64) + + orderDetails := make([]order.Detail, len(resp.Data)) + for x := range resp.Data { + oSide, err := order.StringToOrderSide(resp.Data[x].Side) if err != nil { return err } - myCandle := stream.KlineData{ - Pair: pair, - Exchange: ok.Name, - Timestamp: time.UnixMilli(timestamp), - Interval: candleInterval, - } - myCandle.OpenPrice, err = strconv.ParseFloat(candlesData[1], 64) + oStatus, err := order.StringToOrderStatus(resp.Data[x].State) if err != nil { return err } - myCandle.HighPrice, err = strconv.ParseFloat(candlesData[2], 64) + oType, err := order.StringToOrderType(resp.Data[x].OrderType) if err != nil { return err } - myCandle.LowPrice, err = strconv.ParseFloat(candlesData[3], 64) + orderDetails[x] = order.Detail{ + Amount: resp.Data[x].Size.Float64(), + AverageExecutedPrice: resp.Data[x].AveragePrice.Float64(), + ClientOrderID: resp.Data[x].ClientOrderID, + Date: resp.Data[x].CreationTime.Time(), + Exchange: ok.Name, + ExecutedAmount: resp.Data[x].FillSize.Float64(), + OrderID: resp.Data[x].OrderID, + Pair: pair, + Price: resp.Data[x].Price.Float64(), + QuoteAmount: resp.Data[x].Size.Float64() * resp.Data[x].Price.Float64(), + RemainingAmount: resp.Data[x].Size.Float64() - resp.Data[x].FillSize.Float64(), + Side: oSide, + Status: oStatus, + Type: oType, + LastUpdated: resp.Data[x].UpdateTime.Time(), + } + } + ok.Websocket.DataHandler <- orderDetails + return nil +} + +// wsProcessIndexCandles processes index candlestick data +func (ok *Okx) wsProcessIndexCandles(respRaw []byte) error { + if respRaw == nil { + return common.ErrNilPointer + } + response := struct { + Argument SubscriptionInfo `json:"arg"` + Data [][5]types.Number `json:"data"` + }{} + err := json.Unmarshal(respRaw, &response) + if err != nil { + return err + } + if len(response.Data) == 0 { + return kline.ErrNoTimeSeriesDataToConvert + } + + pair, err := currency.NewPairFromString(response.Argument.InstrumentID) + if err != nil { + return err + } + var assets []asset.Item + if response.Argument.InstrumentType != "" { + assetType, err := assetTypeFromInstrumentType(response.Argument.InstrumentType) if err != nil { return err } - myCandle.ClosePrice, err = strconv.ParseFloat(candlesData[4], 64) + assets = append(assets, assetType) + } else { + assets, err = ok.getAssetsFromInstrumentID(response.Argument.InstrumentID) if err != nil { return err } + } + candleInterval := strings.TrimPrefix(response.Argument.Channel, candle) + for i := range response.Data { + candlesData := response.Data[i] + myCandle := stream.KlineData{ + Pair: pair, + Exchange: ok.Name, + Timestamp: time.UnixMilli(candlesData[0].Int64()), + Interval: candleInterval, + OpenPrice: candlesData[1].Float64(), + HighPrice: candlesData[2].Float64(), + LowPrice: candlesData[3].Float64(), + ClosePrice: candlesData[4].Float64(), + } for i := range assets { myCandle.AssetType = assets[i] ok.Websocket.DataHandler <- myCandle @@ -648,6 +812,101 @@ func (ok *Okx) wsProcessIndexCandles(respRaw []byte) error { return nil } +// wsProcessPublicSpreadTicker process spread order ticker push data. +func (ok *Okx) wsProcessPublicSpreadTicker(respRaw []byte) error { + var resp WsSpreadPushData + data := []WsSpreadPublicTicker{} + resp.Data = &data + err := json.Unmarshal(respRaw, &resp) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(resp.Argument.SpreadID) + if err != nil { + return err + } + tickers := make([]ticker.Price, len(data)) + for x := range data { + tickers[x] = ticker.Price{ + Last: data[x].Last.Float64(), + Bid: data[x].BidPrice.Float64(), + Ask: data[x].AskPrice.Float64(), + Pair: pair, + ExchangeName: ok.Name, + AssetType: asset.Spread, + LastUpdated: data[x].Timestamp.Time(), + } + } + ok.Websocket.DataHandler <- tickers + return nil +} + +// wsProcessPublicSpreadTrades retrieve the recent trades data from sprd-public-trades. +// Data will be pushed whenever there is a trade. +// Every update contains only one trade. +func (ok *Okx) wsProcessPublicSpreadTrades(respRaw []byte) error { + var resp WsSpreadPushData + data := []WsSpreadPublicTrade{} + resp.Data = data + err := json.Unmarshal(respRaw, &resp) + if err != nil { + return err + } + pair, err := currency.NewPairFromString(resp.Argument.SpreadID) + if err != nil { + return err + } + trades := make([]trade.Data, len(data)) + for x := range data { + oSide, err := order.StringToOrderSide(data[x].Side) + if err != nil { + return err + } + trades[x] = trade.Data{ + TID: data[x].TradeID, + Exchange: ok.Name, + CurrencyPair: pair, + AssetType: asset.Spread, + Side: oSide, + Price: data[x].Price.Float64(), + Amount: data[x].Size.Float64(), + Timestamp: data[x].Timestamp.Time(), + } + } + return trade.AddTradesToBuffer(ok.Name, trades...) +} + +// wsProcessSpreadOrderbook process spread orderbook data. +func (ok *Okx) wsProcessSpreadOrderbook(respRaw []byte) error { + var resp WsSpreadOrderbook + err := json.Unmarshal(respRaw, &resp) + if err != nil { + return err + } + pair, err := ok.GetPairFromInstrumentID(resp.Arg.SpreadID) + if err != nil { + return err + } + extractedResponse, err := resp.ExtractSpreadOrder() + if err != nil { + return err + } + for x := range extractedResponse.Data { + err = ok.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{ + Asset: asset.Spread, + Asks: extractedResponse.Data[x].Asks, + Bids: extractedResponse.Data[x].Bids, + LastUpdated: resp.Data[x].Timestamp.Time(), + Pair: pair, + Exchange: ok.Name, + VerifyOrderbook: ok.CanVerifyOrderbook}) + if err != nil { + return err + } + } + return nil +} + // wsProcessOrderbook5 processes orderbook data func (ok *Okx) wsProcessOrderbook5(data []byte) error { var resp WsOrderbook5 @@ -659,8 +918,7 @@ func (ok *Okx) wsProcessOrderbook5(data []byte) error { if len(resp.Data) != 1 { return fmt.Errorf("%s - no data returned", ok.Name) } - - assets, err := ok.GetAssetsFromInstrumentTypeOrID("", resp.Argument.InstrumentID) + assets, err := ok.getAssetsFromInstrumentID(resp.Argument.InstrumentID) if err != nil { return err } @@ -672,28 +930,14 @@ func (ok *Okx) wsProcessOrderbook5(data []byte) error { asks := make([]orderbook.Tranche, len(resp.Data[0].Asks)) for x := range resp.Data[0].Asks { - asks[x].Price, err = strconv.ParseFloat(resp.Data[0].Asks[x][0], 64) - if err != nil { - return err - } - - asks[x].Amount, err = strconv.ParseFloat(resp.Data[0].Asks[x][1], 64) - if err != nil { - return err - } + asks[x].Price = resp.Data[0].Asks[x][0].Float64() + asks[x].Amount = resp.Data[0].Asks[x][1].Float64() } bids := make([]orderbook.Tranche, len(resp.Data[0].Bids)) for x := range resp.Data[0].Bids { - bids[x].Price, err = strconv.ParseFloat(resp.Data[0].Bids[x][0], 64) - if err != nil { - return err - } - - bids[x].Amount, err = strconv.ParseFloat(resp.Data[0].Bids[x][1], 64) - if err != nil { - return err - } + bids[x].Price = resp.Data[0].Bids[x][0].Float64() + bids[x].Amount = resp.Data[0].Bids[x][1].Float64() } for x := range assets { @@ -701,7 +945,7 @@ func (ok *Okx) wsProcessOrderbook5(data []byte) error { Asset: assets[x], Asks: asks, Bids: bids, - LastUpdated: time.UnixMilli(resp.Data[0].TimestampMilli), + LastUpdated: resp.Data[0].Timestamp.Time(), Pair: pair, Exchange: ok.Name, VerifyOrderbook: ok.CanVerifyOrderbook}) @@ -712,6 +956,38 @@ func (ok *Okx) wsProcessOrderbook5(data []byte) error { return nil } +// wsProcessOptionTrades handles options trade data +func (ok *Okx) wsProcessOptionTrades(data []byte) error { + var resp WsOptionTrades + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + trades := make([]trade.Data, len(resp.Data)) + for i := range resp.Data { + var pair currency.Pair + pair, err = ok.GetPairFromInstrumentID(resp.Data[i].InstrumentID) + if err != nil { + return err + } + oSide, err := order.StringToOrderSide(resp.Data[i].Side) + if err != nil { + return err + } + trades[i] = trade.Data{ + Amount: resp.Data[i].Size.Float64(), + AssetType: asset.Options, + CurrencyPair: pair, + Exchange: ok.Name, + Side: oSide, + Timestamp: resp.Data[i].Timestamp.Time(), + TID: resp.Data[i].TradeID, + Price: resp.Data[i].Price.Float64(), + } + } + return trade.AddTradesToBuffer(ok.Name, trades...) +} + // wsProcessOrderBooks processes "snapshot" and "update" order book func (ok *Okx) wsProcessOrderBooks(data []byte) error { var response WsOrderBook @@ -719,31 +995,40 @@ func (ok *Okx) wsProcessOrderBooks(data []byte) error { if err != nil { return err } - if response.Argument.Channel == okxChannelOrderBooks && + if response.Argument.Channel == channelOrderBooks && response.Action != wsOrderbookUpdate && response.Action != wsOrderbookSnapshot { - return errors.New("invalid order book action") + return fmt.Errorf("%w, %s", orderbook.ErrInvalidAction, response.Action) } - assets, err := ok.GetAssetsFromInstrumentTypeOrID(response.Argument.InstrumentType, response.Argument.InstrumentID) - if err != nil { - return err + var assets []asset.Item + if response.Argument.InstrumentType != "" { + assetType, err := assetTypeFromInstrumentType(response.Argument.InstrumentType) + if err != nil { + return err + } + assets = append(assets, assetType) + } else { + assets, err = ok.getAssetsFromInstrumentID(response.Argument.InstrumentID) + if err != nil { + return err + } } pair, err := currency.NewPairFromString(response.Argument.InstrumentID) if err != nil { return err } if !pair.IsPopulated() { - return errIncompleteCurrencyPair + return currency.ErrCurrencyPairsEmpty } pair.Delimiter = currency.DashDelimiter for i := range response.Data { if response.Action == wsOrderbookSnapshot { - err = ok.WsProcessSnapshotOrderBook(response.Data[i], pair, assets) + err = ok.WsProcessSnapshotOrderBook(&response.Data[i], pair, assets) } else { if len(response.Data[i].Asks) == 0 && len(response.Data[i].Bids) == 0 { return nil } - err = ok.WsProcessUpdateOrderbook(response.Data[i], pair, assets) + err = ok.WsProcessUpdateOrderbook(&response.Data[i], pair, assets) } if err != nil { if errors.Is(err, errInvalidChecksum) { @@ -772,7 +1057,7 @@ func (ok *Okx) wsProcessOrderBooks(data []byte) error { } // WsProcessSnapshotOrderBook processes snapshot order books -func (ok *Okx) WsProcessSnapshotOrderBook(data WsOrderBookData, pair currency.Pair, assets []asset.Item) error { +func (ok *Okx) WsProcessSnapshotOrderBook(data *WsOrderBookData, pair currency.Pair, assets []asset.Item) error { signedChecksum, err := ok.CalculateOrderbookChecksum(data) if err != nil { return fmt.Errorf("%w %v: unable to calculate orderbook checksum: %s", @@ -815,7 +1100,7 @@ func (ok *Okx) WsProcessSnapshotOrderBook(data WsOrderBookData, pair currency.Pa // WsProcessUpdateOrderbook updates an existing orderbook using websocket data // After merging WS data, it will sort, validate and finally update the existing // orderbook -func (ok *Okx) WsProcessUpdateOrderbook(data WsOrderBookData, pair currency.Pair, assets []asset.Item) error { +func (ok *Okx) WsProcessUpdateOrderbook(data *WsOrderBookData, pair currency.Pair, assets []asset.Item) error { update := orderbook.Update{ Pair: pair, UpdateTime: data.Timestamp.Time(), @@ -842,18 +1127,10 @@ func (ok *Okx) WsProcessUpdateOrderbook(data WsOrderBookData, pair currency.Pair } // AppendWsOrderbookItems adds websocket orderbook data bid/asks into an orderbook item array -func (ok *Okx) AppendWsOrderbookItems(entries [][4]string) ([]orderbook.Tranche, error) { - items := make([]orderbook.Tranche, len(entries)) +func (ok *Okx) AppendWsOrderbookItems(entries [][4]types.Number) (orderbook.Tranches, error) { + items := make(orderbook.Tranches, len(entries)) for j := range entries { - amount, err := strconv.ParseFloat(entries[j][1], 64) - if err != nil { - return nil, err - } - price, err := strconv.ParseFloat(entries[j][0], 64) - if err != nil { - return nil, err - } - items[j] = orderbook.Tranche{Amount: amount, Price: price} + items[j] = orderbook.Tranche{Amount: entries[j][1].Float64(), Price: entries[j][0].Float64()} } return items, nil } @@ -885,12 +1162,12 @@ func (ok *Okx) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base, c } // CalculateOrderbookChecksum alternates over the first 25 bid and ask entries from websocket data. -func (ok *Okx) CalculateOrderbookChecksum(orderbookData WsOrderBookData) (int32, error) { +func (ok *Okx) CalculateOrderbookChecksum(orderbookData *WsOrderBookData) (int32, error) { var checksum strings.Builder for i := range allowableIterations { if len(orderbookData.Bids)-1 >= i { - bidPrice := orderbookData.Bids[i][0] - bidAmount := orderbookData.Bids[i][1] + bidPrice := orderbookData.Bids[i][0].String() + bidAmount := orderbookData.Bids[i][1].String() checksum.WriteString( bidPrice + wsOrderbookChecksumDelimiter + @@ -898,8 +1175,8 @@ func (ok *Okx) CalculateOrderbookChecksum(orderbookData WsOrderBookData) (int32, wsOrderbookChecksumDelimiter) } if len(orderbookData.Asks)-1 >= i { - askPrice := orderbookData.Asks[i][0] - askAmount := orderbookData.Asks[i][1] + askPrice := orderbookData.Asks[i][0].String() + askAmount := orderbookData.Asks[i][1].String() checksum.WriteString(askPrice + wsOrderbookChecksumDelimiter + askAmount + @@ -913,49 +1190,21 @@ func (ok *Okx) CalculateOrderbookChecksum(orderbookData WsOrderBookData) (int32, // wsHandleMarkPriceCandles processes candlestick mark price push data as a result of subscription to "mark-price-candle*" channel. func (ok *Okx) wsHandleMarkPriceCandles(data []byte) error { tempo := &struct { - Argument SubscriptionInfo `json:"arg"` - Data [][5]string `json:"data"` + Argument SubscriptionInfo `json:"arg"` + Data [][5]types.Number `json:"data"` }{} - var err error - err = json.Unmarshal(data, tempo) + err := json.Unmarshal(data, tempo) if err != nil { return err } - var tsInt int64 - var ts time.Time - var op float64 - var hp float64 - var lp float64 - var cp float64 candles := make([]CandlestickMarkPrice, len(tempo.Data)) for x := range tempo.Data { - tsInt, err = strconv.ParseInt(tempo.Data[x][0], 10, 64) - if err != nil { - return err - } - ts = time.UnixMilli(tsInt) - op, err = strconv.ParseFloat(tempo.Data[x][1], 64) - if err != nil { - return err - } - hp, err = strconv.ParseFloat(tempo.Data[x][2], 64) - if err != nil { - return err - } - lp, err = strconv.ParseFloat(tempo.Data[x][3], 64) - if err != nil { - return err - } - cp, err = strconv.ParseFloat(tempo.Data[x][4], 64) - if err != nil { - return err - } candles[x] = CandlestickMarkPrice{ - Timestamp: ts, - OpenPrice: op, - HighestPrice: hp, - LowestPrice: lp, - ClosePrice: cp, + Timestamp: time.UnixMilli(tempo.Data[x][0].Int64()), + OpenPrice: tempo.Data[x][1].Float64(), + HighestPrice: tempo.Data[x][2].Float64(), + LowestPrice: tempo.Data[x][3].Float64(), + ClosePrice: tempo.Data[x][4].Float64(), } } ok.Websocket.DataHandler <- candles @@ -969,9 +1218,18 @@ func (ok *Okx) wsProcessTrades(data []byte) error { if err != nil { return err } - assets, err := ok.GetAssetsFromInstrumentTypeOrID(response.Argument.InstrumentType, response.Argument.InstrumentID) - if err != nil { - return err + var assets []asset.Item + if response.Argument.InstrumentType != "" { + assetType, err := assetTypeFromInstrumentType(response.Argument.InstrumentType) + if err != nil { + return err + } + assets = append(assets, assetType) + } else { + assets, err = ok.getAssetsFromInstrumentID(response.Argument.InstrumentID) + if err != nil { + return err + } } trades := make([]trade.Data, 0, len(response.Data)*len(assets)) for i := range response.Data { @@ -1002,7 +1260,10 @@ func (ok *Okx) wsProcessOrders(respRaw []byte) error { if err != nil { return err } - a := GetAssetTypeFromInstrumentType(response.Argument.InstrumentType) + a, err := assetTypeFromInstrumentType(response.Argument.InstrumentType) + if err != nil { + return err + } for x := range response.Data { orderType, err := order.StringToOrderType(response.Data[x].OrderType) if err != nil { @@ -1087,71 +1348,50 @@ func (ok *Okx) wsProcessOrders(respRaw []byte) error { // wsProcessCandles handler to get a list of candlestick messages. func (ok *Okx) wsProcessCandles(respRaw []byte) error { if respRaw == nil { - return errNilArgument + return common.ErrNilPointer } response := struct { - Argument SubscriptionInfo `json:"arg"` - Data [][7]string `json:"data"` + Argument SubscriptionInfo `json:"arg"` + Data [][7]types.Number `json:"data"` }{} err := json.Unmarshal(respRaw, &response) if err != nil { return err } if len(response.Data) == 0 { - return errNoCandlestickDataFound + return kline.ErrNoTimeSeriesDataToConvert } pair, err := currency.NewPairFromString(response.Argument.InstrumentID) if err != nil { return err } var assets []asset.Item - assets, err = ok.GetAssetsFromInstrumentTypeOrID(response.Argument.InstrumentType, response.Argument.InstrumentID) - if err != nil { - return err - } - candleInterval := strings.TrimPrefix(response.Argument.Channel, candle) - for i := range response.Data { - var ticks int64 - var timestamp time.Time - var o, h, l, c, v float64 - ticks, err = strconv.ParseInt(response.Data[i][0], 10, 64) - if err != nil { - return err - } - timestamp = time.UnixMilli(ticks) - o, err = strconv.ParseFloat(response.Data[i][1], 64) + if response.Argument.InstrumentType != "" { + assetType, err := assetTypeFromInstrumentType(response.Argument.InstrumentType) if err != nil { return err } - h, err = strconv.ParseFloat(response.Data[i][2], 64) + assets = append(assets, assetType) + } else { + assets, err = ok.getAssetsFromInstrumentID(response.Argument.InstrumentID) if err != nil { return err } - l, err = strconv.ParseFloat(response.Data[i][3], 64) - if err != nil { - return err - } - c, err = strconv.ParseFloat(response.Data[i][4], 64) - if err != nil { - return err - } - v, err = strconv.ParseFloat(response.Data[i][5], 64) - if err != nil { - return err - } - + } + candleInterval := strings.TrimPrefix(response.Argument.Channel, candle) + for i := range response.Data { for j := range assets { ok.Websocket.DataHandler <- stream.KlineData{ - Timestamp: timestamp, + Timestamp: time.UnixMilli(response.Data[i][0].Int64()), Pair: pair, AssetType: assets[j], Exchange: ok.Name, Interval: candleInterval, - OpenPrice: o, - ClosePrice: c, - HighPrice: h, - LowPrice: l, - Volume: v, + OpenPrice: response.Data[i][1].Float64(), + ClosePrice: response.Data[i][4].Float64(), + HighPrice: response.Data[i][2].Float64(), + LowPrice: response.Data[i][3].Float64(), + Volume: response.Data[i][5].Float64(), } } } @@ -1167,9 +1407,17 @@ func (ok *Okx) wsProcessTickers(data []byte) error { } for i := range response.Data { var assets []asset.Item - assets, err = ok.GetAssetsFromInstrumentTypeOrID(response.Argument.InstrumentType, response.Data[i].InstrumentID) - if err != nil { - return err + if response.Argument.InstrumentType != "" { + assetType, err := assetTypeFromInstrumentType(response.Argument.InstrumentType) + if err != nil { + return err + } + assets = append(assets, assetType) + } else { + assets, err = ok.getAssetsFromInstrumentID(response.Argument.InstrumentID) + if err != nil { + return err + } } c, err := currency.NewPairFromString(response.Data[i].InstrumentID) if err != nil { @@ -1218,10 +1466,42 @@ func (ok *Okx) GetSubscriptionTemplate(_ *subscription.Subscription) (*template. "channelName": channelName, "isSymbolChannel": isSymbolChannel, "isAssetChannel": isAssetChannel, - "instType": ok.GetInstrumentTypeFromAssetItem, + "instType": GetInstrumentTypeFromAssetItem, }).Parse(subTplText) } +// wsProcessBlockPublicTrades handles the recent block trades data by individual legs. +func (ok *Okx) wsProcessBlockPublicTrades(data []byte) error { + var resp PublicBlockTrades + err := json.Unmarshal(data, &resp) + if err != nil { + return err + } + trades := make([]trade.Data, len(resp.Data)) + for i := range resp.Data { + var pair currency.Pair + pair, err = ok.GetPairFromInstrumentID(resp.Data[i].InstrumentID) + if err != nil { + return err + } + oSide, err := order.StringToOrderSide(resp.Data[i].Side) + if err != nil { + return err + } + trades[i] = trade.Data{ + Amount: resp.Data[i].Size.Float64(), + AssetType: asset.Options, + CurrencyPair: pair, + Exchange: ok.Name, + Side: oSide, + Timestamp: resp.Data[i].Timestamp.Time(), + TID: resp.Data[i].TradeID, + Price: resp.Data[i].Price.Float64(), + } + } + return trade.AddTradesToBuffer(ok.Name, trades...) +} + // wsProcessPushData processes push data coming through the websocket channel func (ok *Okx) wsProcessPushData(data []byte, resp interface{}) error { if err := json.Unmarshal(data, resp); err != nil { @@ -1234,19 +1514,22 @@ func (ok *Okx) wsProcessPushData(data []byte, resp interface{}) error { // Websocket Trade methods // WsPlaceOrder places an order thought the websocket connection stream, and returns a SubmitResponse and error message. -func (ok *Okx) WsPlaceOrder(arg *PlaceOrderRequestParam) (*OrderData, error) { - if arg == nil { - return nil, errNilArgument +func (ok *Okx) WsPlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam) (*OrderData, error) { + if arg == nil || *arg == (PlaceOrderRequestParam{}) { + return nil, common.ErrNilPointer } err := ok.validatePlaceOrderParams(arg) if err != nil { return nil, err } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) if err != nil { return nil, err } - input := WsPlaceOrderInput{ + input := WsOperationInput{ ID: randomID, Arguments: []PlaceOrderRequestParam{*arg}, Operation: okxOpOrder, @@ -1266,20 +1549,15 @@ func (ok *Okx) WsPlaceOrder(arg *PlaceOrderRequestParam) (*OrderData, error) { select { case data := <-wsResponse: if data.Operation == okxOpOrder && data.ID == input.ID { - if data.Code == "0" || data.Code == "1" { - resp, err := data.copyToPlaceOrderResponse() - if err != nil { - return nil, err - } - if len(resp.Data) != 1 { - return nil, errNoValidResponseFromServer - } - if data.Code == "1" { - return nil, fmt.Errorf("error code:%s message: %s", resp.Data[0].SCode, resp.Data[0].SMessage) - } - return &resp.Data[0], nil + var dataHolder *OrderData + err = ok.handleIncomingData(data, &dataHolder) + if err != nil { + return nil, err } - return nil, fmt.Errorf("error code:%s message: %v", data.Code, ErrorCodes[data.Code]) + if data.StatusCode == "1" { + return nil, fmt.Errorf("error code:%s error message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) + } + return dataHolder, nil } continue case <-timer.C: @@ -1291,8 +1569,11 @@ func (ok *Okx) WsPlaceOrder(arg *PlaceOrderRequestParam) (*OrderData, error) { } } -// WsPlaceMultipleOrder creates an order through the websocket stream. -func (ok *Okx) WsPlaceMultipleOrder(args []PlaceOrderRequestParam) ([]OrderData, error) { +// WsPlaceMultipleOrders creates an order through the websocket stream. +func (ok *Okx) WsPlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequestParam) ([]OrderData, error) { + if len(args) == 0 { + return nil, order.ErrSubmissionIsNil + } var err error for x := range args { arg := args[x] @@ -1301,11 +1582,14 @@ func (ok *Okx) WsPlaceMultipleOrder(args []PlaceOrderRequestParam) ([]OrderData, return nil, err } } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } randomID, err := common.GenerateRandomString(4, common.NumberCharacters) if err != nil { return nil, err } - input := WsPlaceOrderInput{ + input := WsOperationInput{ ID: randomID, Arguments: args, Operation: okxOpBatchOrders, @@ -1325,8 +1609,8 @@ func (ok *Okx) WsPlaceMultipleOrder(args []PlaceOrderRequestParam) ([]OrderData, select { case data := <-wsResponse: if data.Operation == okxOpBatchOrders && data.ID == input.ID { - if data.Code == "0" || data.Code == "2" { - var resp *WSOrderResponse + if data.StatusCode == "0" || data.StatusCode == "2" { + var resp *WsPlaceOrderResponse resp, err = data.copyToPlaceOrderResponse() if err != nil { return nil, err @@ -1343,12 +1627,12 @@ func (ok *Okx) WsPlaceMultipleOrder(args []PlaceOrderRequestParam) ([]OrderData, return nil, err } if len(data.Data) == 0 { - return nil, fmt.Errorf("error code:%s message: %v", data.Code, ErrorCodes[data.Code]) + return nil, fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) } var errs error for x := range resp.Data { - if resp.Data[x].SCode != "0" { - errs = common.AppendError(errs, fmt.Errorf("error code:%s message: %s", resp.Data[x].SCode, resp.Data[x].SMessage)) + if resp.Data[x].StatusCode != "0" { + errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %s", resp.Data[x].StatusCode, resp.Data[x].StatusMessage)) } } return nil, errs @@ -1364,23 +1648,29 @@ func (ok *Okx) WsPlaceMultipleOrder(args []PlaceOrderRequestParam) ([]OrderData, } // WsCancelOrder websocket function to cancel a trade order -func (ok *Okx) WsCancelOrder(arg CancelOrderRequestParam) (*OrderData, error) { +func (ok *Okx) WsCancelOrder(ctx context.Context, arg *CancelOrderRequestParam) (*OrderData, error) { + if arg == nil || *arg == (CancelOrderRequestParam{}) { + return nil, common.ErrNilPointer + } if arg.InstrumentID == "" { return nil, errMissingInstrumentID } if arg.OrderID == "" && arg.ClientOrderID == "" { - return nil, errors.New("either order id or client supplier id is required") + return nil, order.ErrOrderIDNotSet + } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated } randomID, err := common.GenerateRandomString(4, common.NumberCharacters) if err != nil { return nil, err } - input := WsCancelOrderInput{ + input := WsOperationInput{ ID: randomID, - Arguments: []CancelOrderRequestParam{arg}, + Arguments: []CancelOrderRequestParam{*arg}, Operation: okxOpCancelOrder, } - err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), cancelOrderEPL, input) + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, cancelOrderEPL, input) if err != nil { return nil, err } @@ -1395,20 +1685,15 @@ func (ok *Okx) WsCancelOrder(arg CancelOrderRequestParam) (*OrderData, error) { select { case data := <-wsResponse: if data.Operation == okxOpCancelOrder && data.ID == input.ID { - if data.Code == "0" || data.Code == "1" { - resp, err := data.copyToPlaceOrderResponse() - if err != nil { - return nil, err - } - if len(resp.Data) != 1 { - return nil, errNoValidResponseFromServer - } - if data.Code == "1" { - return nil, fmt.Errorf("error code: %s message: %s", resp.Data[0].SCode, resp.Data[0].SMessage) - } - return &resp.Data[0], nil + var dataHolder *OrderData + err = ok.handleIncomingData(data, &dataHolder) + if err != nil { + return nil, err + } + if data.StatusCode == "1" { + return nil, fmt.Errorf("error code:%s error message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) } - return nil, fmt.Errorf("error code: %s message: %v", data.Code, ErrorCodes[data.Code]) + return dataHolder, nil } continue case <-timer.C: @@ -1421,21 +1706,24 @@ func (ok *Okx) WsCancelOrder(arg CancelOrderRequestParam) (*OrderData, error) { } // WsCancelMultipleOrder cancel multiple order through the websocket channel. -func (ok *Okx) WsCancelMultipleOrder(args []CancelOrderRequestParam) ([]OrderData, error) { +func (ok *Okx) WsCancelMultipleOrder(ctx context.Context, args []CancelOrderRequestParam) ([]OrderData, error) { for x := range args { arg := args[x] if arg.InstrumentID == "" { return nil, errMissingInstrumentID } if arg.OrderID == "" && arg.ClientOrderID == "" { - return nil, errors.New("either order id or client supplier id is required") + return nil, order.ErrOrderIDNotSet } } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } randomID, err := common.GenerateRandomString(4, common.NumberCharacters) if err != nil { return nil, err } - input := WsCancelOrderInput{ + input := WsOperationInput{ ID: randomID, Arguments: args, Operation: okxOpBatchCancelOrders, @@ -1455,8 +1743,8 @@ func (ok *Okx) WsCancelMultipleOrder(args []CancelOrderRequestParam) ([]OrderDat select { case data := <-wsResponse: if data.Operation == okxOpBatchCancelOrders && data.ID == input.ID { - if data.Code == "0" || data.Code == "2" { - var resp *WSOrderResponse + if data.StatusCode == "0" || data.StatusCode == "2" { + var resp *WsPlaceOrderResponse resp, err = data.copyToPlaceOrderResponse() if err != nil { return nil, err @@ -1464,7 +1752,7 @@ func (ok *Okx) WsCancelMultipleOrder(args []CancelOrderRequestParam) ([]OrderDat return resp.Data, nil } if len(data.Data) == 0 { - return nil, fmt.Errorf("error code:%s message: %v", data.Code, ErrorCodes[data.Code]) + return nil, fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) } var resp WsOrderActionResponse err = resp.populateFromIncomingData(data) @@ -1477,8 +1765,8 @@ func (ok *Okx) WsCancelMultipleOrder(args []CancelOrderRequestParam) ([]OrderDat } var errs error for x := range resp.Data { - if resp.Data[x].SCode != "0" { - errs = common.AppendError(errs, fmt.Errorf("error code:%s message: %v", resp.Data[x].SCode, resp.Data[x].SMessage)) + if resp.Data[x].StatusCode != "0" { + errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %v", resp.Data[x].StatusCode, resp.Data[x].StatusMessage)) } } return nil, errs @@ -1494,29 +1782,32 @@ func (ok *Okx) WsCancelMultipleOrder(args []CancelOrderRequestParam) ([]OrderDat } // WsAmendOrder method to amend trade order using a request thought the websocket channel. -func (ok *Okx) WsAmendOrder(arg *AmendOrderRequestParams) (*OrderData, error) { +func (ok *Okx) WsAmendOrder(ctx context.Context, arg *AmendOrderRequestParams) (*OrderData, error) { if arg == nil { - return nil, errNilArgument + return nil, common.ErrNilPointer } if arg.InstrumentID == "" { return nil, errMissingInstrumentID } if arg.ClientOrderID == "" && arg.OrderID == "" { - return nil, errMissingClientOrderIDOrOrderID + return nil, order.ErrOrderIDNotSet } if arg.NewQuantity <= 0 && arg.NewPrice <= 0 { return nil, errInvalidNewSizeOrPriceInformation } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } randomID, err := common.GenerateRandomString(4, common.NumberCharacters) if err != nil { return nil, err } - input := WsAmendOrderInput{ + input := WsOperationInput{ ID: randomID, Operation: okxOpAmendOrder, Arguments: []AmendOrderRequestParams{*arg}, } - err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), amendOrderEPL, input) + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, amendOrderEPL, input) if err != nil { return nil, err } @@ -1531,20 +1822,15 @@ func (ok *Okx) WsAmendOrder(arg *AmendOrderRequestParams) (*OrderData, error) { select { case data := <-wsResponse: if data.Operation == okxOpAmendOrder && data.ID == input.ID { - if data.Code == "0" || data.Code == "1" { - resp, err := data.copyToPlaceOrderResponse() - if err != nil { - return nil, err - } - if len(resp.Data) != 1 { - return nil, errNoValidResponseFromServer - } - if data.Code == "1" { - return nil, fmt.Errorf("error code: %s message: %s", resp.Data[0].SCode, resp.Data[0].SMessage) - } - return &resp.Data[0], nil + var dataHolder *OrderData + err = ok.handleIncomingData(data, &dataHolder) + if err != nil { + return nil, err + } + if data.StatusCode == "1" { + return nil, fmt.Errorf("error code:%s error message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) } - return nil, fmt.Errorf("error code: %s message: %v", data.Code, ErrorCodes[data.Code]) + return dataHolder, nil } continue case <-timer.C: @@ -1557,28 +1843,31 @@ func (ok *Okx) WsAmendOrder(arg *AmendOrderRequestParams) (*OrderData, error) { } // WsAmendMultipleOrders a request through the websocket connection to amend multiple trade orders. -func (ok *Okx) WsAmendMultipleOrders(args []AmendOrderRequestParams) ([]OrderData, error) { +func (ok *Okx) WsAmendMultipleOrders(ctx context.Context, args []AmendOrderRequestParams) ([]OrderData, error) { for x := range args { if args[x].InstrumentID == "" { return nil, errMissingInstrumentID } if args[x].ClientOrderID == "" && args[x].OrderID == "" { - return nil, errMissingClientOrderIDOrOrderID + return nil, order.ErrOrderIDNotSet } if args[x].NewQuantity <= 0 && args[x].NewPrice <= 0 { return nil, errInvalidNewSizeOrPriceInformation } } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } randomID, err := common.GenerateRandomString(4, common.NumberCharacters) if err != nil { return nil, err } - input := &WsAmendOrderInput{ + input := &WsOperationInput{ ID: randomID, Operation: okxOpBatchAmendOrders, Arguments: args, } - err = ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), amendMultipleOrdersEPL, input) + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, amendMultipleOrdersEPL, input) if err != nil { return nil, err } @@ -1593,8 +1882,8 @@ func (ok *Okx) WsAmendMultipleOrders(args []AmendOrderRequestParams) ([]OrderDat select { case data := <-wsResponse: if data.Operation == okxOpBatchAmendOrders && data.ID == input.ID { - if data.Code == "0" || data.Code == "2" { - var resp *WSOrderResponse + if data.StatusCode == "0" || data.StatusCode == "2" { + var resp *WsPlaceOrderResponse resp, err = data.copyToPlaceOrderResponse() if err != nil { return nil, err @@ -1602,7 +1891,7 @@ func (ok *Okx) WsAmendMultipleOrders(args []AmendOrderRequestParams) ([]OrderDat return resp.Data, nil } if len(data.Data) == 0 { - return nil, fmt.Errorf("error code:%s message: %v", data.Code, ErrorCodes[data.Code]) + return nil, fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) } var resp WsOrderActionResponse err = resp.populateFromIncomingData(data) @@ -1615,8 +1904,8 @@ func (ok *Okx) WsAmendMultipleOrders(args []AmendOrderRequestParams) ([]OrderDat } var errs error for x := range resp.Data { - if resp.Data[x].SCode != "0" { - errs = common.AppendError(errs, fmt.Errorf("error code:%s message: %v", resp.Data[x].SCode, resp.Data[x].SMessage)) + if resp.Data[x].StatusCode != "0" { + errs = common.AppendError(errs, fmt.Errorf("error code:%s error message: %v", resp.Data[x].StatusCode, resp.Data[x].StatusMessage)) } } return nil, errs @@ -1631,6 +1920,72 @@ func (ok *Okx) WsAmendMultipleOrders(args []AmendOrderRequestParams) ([]OrderDat } } +// WsMassCancelOrders cancel all the MMP pending orders of an instrument family. +// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required. +func (ok *Okx) WsMassCancelOrders(ctx context.Context, args []CancelMassReqParam) (bool, error) { + for x := range args { + if args[x] == (CancelMassReqParam{}) { + return false, common.ErrEmptyParams + } + if args[x].InstrumentType == "" { + return false, fmt.Errorf("%w, instrument type can not be empty", errInvalidInstrumentType) + } + if args[x].InstrumentFamily == "" { + return false, errInstrumentFamilyRequired + } + } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return false, errWebsocketStreamNotAuthenticated + } + randomID, err := common.GenerateRandomString(4, common.NumberCharacters) + if err != nil { + return false, err + } + input := &WsOperationInput{ + ID: randomID, + Operation: okxOpMassCancelOrder, + Arguments: args, + } + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.Unset, input) + if err != nil { + return false, err + } + timer := time.NewTimer(ok.WebsocketResponseMaxLimit) + wsResponse := make(chan *wsIncomingData) + ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ + ID: randomID, + Chan: wsResponse, + } + defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() + for { + select { + case data := <-wsResponse: + if data.Operation == okxOpMassCancelOrder && data.ID == input.ID { + if data.StatusCode == "0" || data.StatusCode == "2" { + resp := []struct { + Result bool `json:"result"` + }{} + err := json.Unmarshal(data.Data, &resp) + if err != nil { + return false, err + } + if len(data.Data) == 0 { + return false, fmt.Errorf("error code:%s message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) + } + return resp[0].Result, nil + } + return false, fmt.Errorf("error code:%s message: %v", data.StatusCode, data.Message) + } + continue + case <-timer.C: + timer.Stop() + return false, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", + ok.Name, + input.Operation) + } + } +} + // Run this functions distributes websocket request responses to func (m *wsRequestDataChannelsMultiplexer) Run() { tickerData := time.NewTicker(time.Second) @@ -1657,8 +2012,8 @@ func (m *wsRequestDataChannelsMultiplexer) Run() { } for _, myChan := range m.WsResponseChannelsMap { if (msg.Event == "error" || myChan.Event == operationLogin) && - (msg.Code == "60009" || msg.Code == "60022" || msg.Code == "0") && - strings.Contains(msg.Msg, myChan.Channel) { + (msg.StatusCode == "60009" || msg.StatusCode == "60004" || msg.StatusCode == "60022" || msg.StatusCode == "0") && + strings.Contains(msg.Message, myChan.Channel) { myChan.Chan <- msg continue } else if msg.Event != myChan.Event || @@ -1681,7 +2036,7 @@ func (m *wsRequestDataChannelsMultiplexer) Shutdown() { } // wsChannelSubscription sends a subscription or unsubscription request for different channels through the websocket stream. -func (ok *Okx) wsChannelSubscription(operation, channel string, assetType asset.Item, pair currency.Pair, tInstrumentType, tInstrumentID, tUnderlying bool) error { +func (ok *Okx) wsChannelSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair, tInstrumentType, tInstrumentID, tUnderlying bool) error { if operation != operationSubscribe && operation != operationUnsubscribe { return errInvalidWebsocketEvent } @@ -1694,13 +2049,13 @@ func (ok *Okx) wsChannelSubscription(operation, channel string, assetType asset. var format currency.PairFormat var err error if tInstrumentType { - instrumentType = ok.GetInstrumentTypeFromAssetItem(assetType) - if instrumentType != okxInstTypeSpot && - instrumentType != okxInstTypeMargin && - instrumentType != okxInstTypeSwap && - instrumentType != okxInstTypeFutures && - instrumentType != okxInstTypeOption { - instrumentType = okxInstTypeANY + instrumentType = GetInstrumentTypeFromAssetItem(assetType) + if instrumentType != instTypeSpot && + instrumentType != instTypeMargin && + instrumentType != instTypeSwap && + instrumentType != instTypeFutures && + instrumentType != instTypeOption { + instrumentType = instTypeANY } } if tUnderlying { @@ -1714,7 +2069,7 @@ func (ok *Okx) wsChannelSubscription(operation, channel string, assetType asset. return err } if !pair.IsPopulated() { - return errIncompleteCurrencyPair + return currency.ErrCurrencyPairsEmpty } instrumentID = format.Format(pair) } @@ -1731,13 +2086,13 @@ func (ok *Okx) wsChannelSubscription(operation, channel string, assetType asset. } ok.WsRequestSemaphore <- 1 defer func() { <-ok.WsRequestSemaphore }() - return ok.Websocket.Conn.SendJSONMessage(context.TODO(), request.Unset, input) + return ok.Websocket.Conn.SendJSONMessage(ctx, request.Unset, input) } // Private Channel Websocket methods // wsAuthChannelSubscription send a subscription or unsubscription request for different channels through the websocket stream. -func (ok *Okx) wsAuthChannelSubscription(operation, channel string, assetType asset.Item, pair currency.Pair, uid, algoID string, params wsSubscriptionParameters) error { +func (ok *Okx) wsAuthChannelSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair, uid, algoID string, params wsSubscriptionParameters) error { if operation != operationSubscribe && operation != operationUnsubscribe { return errInvalidWebsocketEvent } @@ -1746,12 +2101,12 @@ func (ok *Okx) wsAuthChannelSubscription(operation, channel string, assetType as var instrumentType string var ccy string if params.InstrumentType { - instrumentType = ok.GetInstrumentTypeFromAssetItem(assetType) - if instrumentType != okxInstTypeMargin && - instrumentType != okxInstTypeSwap && - instrumentType != okxInstTypeFutures && - instrumentType != okxInstTypeOption { - instrumentType = okxInstTypeANY + instrumentType = GetInstrumentTypeFromAssetItem(assetType) + if instrumentType != instTypeMargin && + instrumentType != instTypeSwap && + instrumentType != instTypeFutures && + instrumentType != instTypeOption { + instrumentType = instTypeANY } } if params.Underlying { @@ -1761,7 +2116,7 @@ func (ok *Okx) wsAuthChannelSubscription(operation, channel string, assetType as } if params.InstrumentID { if !pair.IsPopulated() { - return errIncompleteCurrencyPair + return currency.ErrCurrencyPairsEmpty } format, err := ok.GetPairFormat(assetType, false) if err != nil { @@ -1797,85 +2152,85 @@ func (ok *Okx) wsAuthChannelSubscription(operation, channel string, assetType as } ok.WsRequestSemaphore <- 1 defer func() { <-ok.WsRequestSemaphore }() - return ok.Websocket.AuthConn.SendJSONMessage(context.TODO(), request.Unset, input) + return ok.Websocket.AuthConn.SendJSONMessage(ctx, request.Unset, input) } // WsAccountSubscription retrieve account information. Data will be pushed when triggered by // events such as placing order, canceling order, transaction execution, etc. // It will also be pushed in regular interval according to subscription granularity. -func (ok *Okx) WsAccountSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(operation, okxChannelAccount, assetType, pair, "", "", wsSubscriptionParameters{Currency: true}) +func (ok *Okx) WsAccountSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelAccount, assetType, pair, "", "", wsSubscriptionParameters{Currency: true}) } // WsPositionChannel retrieve the position data. The first snapshot will be sent in accordance with the granularity of the subscription. Data will be pushed when certain actions, such placing or canceling an order, trigger it. It will also be pushed periodically based on the granularity of the subscription. -func (ok *Okx) WsPositionChannel(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(operation, okxChannelPositions, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true}) +func (ok *Okx) WsPositionChannel(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelPositions, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true}) } // BalanceAndPositionSubscription retrieve account balance and position information. Data will be pushed when triggered by events such as filled order, funding transfer. -func (ok *Okx) BalanceAndPositionSubscription(operation, uid string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelBalanceAndPosition, asset.Empty, currency.EMPTYPAIR, uid, "", wsSubscriptionParameters{}) +func (ok *Okx) BalanceAndPositionSubscription(ctx context.Context, operation, uid string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelBalanceAndPosition, asset.Empty, currency.EMPTYPAIR, uid, "", wsSubscriptionParameters{}) } // WsOrderChannel for subscribing for orders. -func (ok *Okx) WsOrderChannel(operation string, assetType asset.Item, pair currency.Pair, _ string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelOrders, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) +func (ok *Okx) WsOrderChannel(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, _ string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelOrders, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) } // AlgoOrdersSubscription for subscribing to algo - order channels -func (ok *Okx) AlgoOrdersSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(operation, okxChannelAlgoOrders, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) +func (ok *Okx) AlgoOrdersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelAlgoOrders, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) } // AdvanceAlgoOrdersSubscription algo order subscription to retrieve advance algo orders (including Iceberg order, TWAP order, Trailing order). Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) AdvanceAlgoOrdersSubscription(operation string, assetType asset.Item, pair currency.Pair, algoID string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelAlgoAdvance, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, InstrumentID: true}) +func (ok *Okx) AdvanceAlgoOrdersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, algoID string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelAlgoAdvance, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, InstrumentID: true}) } // PositionRiskWarningSubscription this push channel is only used as a risk warning, and is not recommended as a risk judgment for strategic trading // In the case that the market is not moving violently, there may be the possibility that the position has been liquidated at the same time that this message is pushed. -func (ok *Okx) PositionRiskWarningSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(operation, okxChannelLiquidationWarning, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) +func (ok *Okx) PositionRiskWarningSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelLiquidationWarning, assetType, pair, "", "", wsSubscriptionParameters{InstrumentType: true, InstrumentID: true, Underlying: true}) } // AccountGreeksSubscription algo order subscription to retrieve account greeks information. Data will be pushed when triggered by events such as increase/decrease positions or cash balance in account, and will also be pushed in regular interval according to subscription granularity. -func (ok *Okx) AccountGreeksSubscription(operation string, pair currency.Pair) error { - return ok.wsAuthChannelSubscription(operation, okxChannelAccountGreeks, asset.Empty, pair, "", "", wsSubscriptionParameters{Currency: true}) +func (ok *Okx) AccountGreeksSubscription(ctx context.Context, operation string, pair currency.Pair) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelAccountGreeks, asset.Empty, pair, "", "", wsSubscriptionParameters{Currency: true}) } -// RfqSubscription subscription to retrieve Rfq updates on Rfq orders. -func (ok *Okx) RfqSubscription(operation, uid string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelRfqs, asset.Empty, currency.EMPTYPAIR, uid, "", wsSubscriptionParameters{}) +// RFQSubscription subscription to retrieve RFQ updates on RFQ orders. +func (ok *Okx) RFQSubscription(ctx context.Context, operation, uid string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelRFQs, asset.Empty, currency.EMPTYPAIR, uid, "", wsSubscriptionParameters{}) } // QuotesSubscription subscription to retrieve Quote subscription -func (ok *Okx) QuotesSubscription(operation string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelQuotes, asset.Empty, currency.EMPTYPAIR, "", "", wsSubscriptionParameters{}) +func (ok *Okx) QuotesSubscription(ctx context.Context, operation string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelQuotes, asset.Empty, currency.EMPTYPAIR, "", "", wsSubscriptionParameters{}) } // StructureBlockTradesSubscription to retrieve Structural block subscription -func (ok *Okx) StructureBlockTradesSubscription(operation string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelStructureBlockTrades, asset.Empty, currency.EMPTYPAIR, "", "", wsSubscriptionParameters{}) +func (ok *Okx) StructureBlockTradesSubscription(ctx context.Context, operation string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelStructureBlockTrades, asset.Empty, currency.EMPTYPAIR, "", "", wsSubscriptionParameters{}) } // SpotGridAlgoOrdersSubscription to retrieve spot grid algo orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) SpotGridAlgoOrdersSubscription(operation string, assetType asset.Item, pair currency.Pair, algoID string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelSpotGridOrder, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, Underlying: true}) +func (ok *Okx) SpotGridAlgoOrdersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, algoID string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelSpotGridOrder, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, Underlying: true}) } // ContractGridAlgoOrders to retrieve contract grid algo orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) ContractGridAlgoOrders(operation string, assetType asset.Item, pair currency.Pair, algoID string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelGridOrdersContract, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, Underlying: true}) +func (ok *Okx) ContractGridAlgoOrders(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair, algoID string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelGridOrdersContract, assetType, pair, "", algoID, wsSubscriptionParameters{InstrumentType: true, Underlying: true}) } // GridPositionsSubscription to retrieve grid positions. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing/canceling order. -func (ok *Okx) GridPositionsSubscription(operation, algoID string) error { - return ok.wsAuthChannelSubscription(operation, okxChannelGridPositions, asset.Empty, currency.EMPTYPAIR, "", algoID, wsSubscriptionParameters{}) +func (ok *Okx) GridPositionsSubscription(ctx context.Context, operation, algoID string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelGridPositions, asset.Empty, currency.EMPTYPAIR, "", algoID, wsSubscriptionParameters{}) } // GridSubOrders to retrieve grid sub orders. Data will be pushed when first subscribed. Data will be pushed when triggered by events such as placing order. -func (ok *Okx) GridSubOrders(operation, algoID string) error { - return ok.wsAuthChannelSubscription(operation, okcChannelGridSubOrders, asset.Empty, currency.EMPTYPAIR, "", algoID, wsSubscriptionParameters{}) +func (ok *Okx) GridSubOrders(ctx context.Context, operation, algoID string) error { + return ok.wsAuthChannelSubscription(ctx, operation, channelGridSubOrders, asset.Empty, currency.EMPTYPAIR, "", algoID, wsSubscriptionParameters{}) } // Public Websocket stream subscription @@ -1883,114 +2238,363 @@ func (ok *Okx) GridSubOrders(operation, algoID string) error { // InstrumentsSubscription to subscribe for instruments. The full instrument list will be pushed // for the first time after subscription. Subsequently, the instruments will be pushed if there is any change to the instrument’s state (such as delivery of FUTURES, // exercise of OPTION, listing of new contracts / trading pairs, trading suspension, etc.). -func (ok *Okx) InstrumentsSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelInstruments, assetType, pair, true, false, false) +func (ok *Okx) InstrumentsSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelInstruments, assetType, pair, true, false, false) } // TickersSubscription subscribing to "ticker" channel to retrieve the last traded price, bid price, ask price and 24-hour trading volume of instruments. Data will be pushed every 100 ms. -func (ok *Okx) TickersSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelTickers, assetType, pair, false, true, false) +func (ok *Okx) TickersSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelTickers, assetType, pair, false, true, false) } -// OpenInterestSubscription to subscribe or unsubscribe to "open-interest" channel to retrieve the open interest. Data will by pushed every 3 seconds. -func (ok *Okx) OpenInterestSubscription(operation string, assetType asset.Item, pair currency.Pair) error { +// OpenInterestSubscription to subscribe or unsubscribe to "open-interest" channel to retrieve the open interest. Data will be pushed every 3 seconds. +func (ok *Okx) OpenInterestSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { if assetType != asset.Futures && assetType != asset.Options && assetType != asset.PerpetualSwap { return fmt.Errorf("%w, received '%v' only FUTURES, SWAP and OPTION asset types are supported", errInvalidInstrumentType, assetType) } - return ok.wsChannelSubscription(operation, okxChannelOpenInterest, assetType, pair, false, true, false) + return ok.wsChannelSubscription(ctx, operation, channelOpenInterest, assetType, pair, false, true, false) } // CandlesticksSubscription to subscribe or unsubscribe to "candle" channels to retrieve the candlesticks data of an instrument. the push frequency is the fastest interval 500ms push the data. -func (ok *Okx) CandlesticksSubscription(operation, channel string, assetType asset.Item, pair currency.Pair) error { +func (ok *Okx) CandlesticksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { if _, okay := candlestickChannelsMap[channel]; !okay { return errMissingValidChannelInformation } - return ok.wsChannelSubscription(operation, channel, assetType, pair, false, true, false) + return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) } // TradesSubscription to subscribe or unsubscribe to "trades" channel to retrieve the recent trades data. Data will be pushed whenever there is a trade. Every update contain only one trade. -func (ok *Okx) TradesSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelTrades, assetType, pair, false, true, false) +func (ok *Okx) TradesSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelTrades, assetType, pair, false, true, false) } // EstimatedDeliveryExercisePriceSubscription to subscribe or unsubscribe to "estimated-price" channel to retrieve the estimated delivery/exercise price of FUTURES contracts and OPTION. -func (ok *Okx) EstimatedDeliveryExercisePriceSubscription(operation string, assetType asset.Item, pair currency.Pair) error { +func (ok *Okx) EstimatedDeliveryExercisePriceSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { if assetType != asset.Futures && assetType != asset.Options { return fmt.Errorf("%w, received '%v' only FUTURES and OPTION asset types are supported", errInvalidInstrumentType, assetType) } - return ok.wsChannelSubscription(operation, okxChannelEstimatedPrice, assetType, pair, true, true, false) + return ok.wsChannelSubscription(ctx, operation, channelEstimatedPrice, assetType, pair, true, true, false) } // MarkPriceSubscription to subscribe or unsubscribe to the "mark-price" to retrieve the mark price. Data will be pushed every 200 ms when the mark price changes, and will be pushed every 10 seconds when the mark price does not change. -func (ok *Okx) MarkPriceSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelMarkPrice, assetType, pair, false, true, false) +func (ok *Okx) MarkPriceSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelMarkPrice, assetType, pair, false, true, false) } // MarkPriceCandlesticksSubscription to subscribe or unsubscribe to "mark-price-candles" channels to retrieve the candlesticks data of the mark price. Data will be pushed every 500 ms. -func (ok *Okx) MarkPriceCandlesticksSubscription(operation, channel string, assetType asset.Item, pair currency.Pair) error { +func (ok *Okx) MarkPriceCandlesticksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { if _, okay := candlesticksMarkPriceMap[channel]; !okay { return fmt.Errorf("%w channel: %v", errMissingValidChannelInformation, channel) } - return ok.wsChannelSubscription(operation, channel, assetType, pair, false, true, false) + return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) } // PriceLimitSubscription subscribe or unsubscribe to "price-limit" channel to retrieve the maximum buy price and minimum sell price of the instrument. Data will be pushed every 5 seconds when there are changes in limits, and will not be pushed when there is no changes on limit. -func (ok *Okx) PriceLimitSubscription(operation string, pair currency.Pair) error { +func (ok *Okx) PriceLimitSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { if operation != operationSubscribe && operation != operationUnsubscribe { return errInvalidWebsocketEvent } - return ok.wsChannelSubscription(operation, okxChannelPriceLimit, asset.Empty, pair, false, true, false) + return ok.wsChannelSubscription(ctx, operation, channelPriceLimit, assetType, pair, false, true, false) } // OrderBooksSubscription subscribe or unsubscribe to "books*" channel to retrieve order book data. -func (ok *Okx) OrderBooksSubscription(operation, channel string, assetType asset.Item, pair currency.Pair) error { - if channel != okxChannelOrderBooks && channel != okxChannelOrderBooks5 && channel != okxChannelOrderBooks50TBT && channel != okxChannelOrderBooksTBT && channel != okxChannelBBOTBT { +func (ok *Okx) OrderBooksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { + if channel != channelOrderBooks && channel != channelOrderBooks5 && channel != channelOrderBooks50TBT && channel != channelOrderBooksTBT && channel != channelBBOTBT { return fmt.Errorf("%w channel: %v", errMissingValidChannelInformation, channel) } - return ok.wsChannelSubscription(operation, channel, assetType, pair, false, true, false) + return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) } // OptionSummarySubscription a method to subscribe or unsubscribe to "opt-summary" channel // to retrieve detailed pricing information of all OPTION contracts. Data will be pushed at once. -func (ok *Okx) OptionSummarySubscription(operation string, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelOptSummary, asset.Options, pair, false, false, true) +func (ok *Okx) OptionSummarySubscription(ctx context.Context, operation string, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelOptSummary, asset.Options, pair, false, false, true) } // FundingRateSubscription a method to subscribe and unsubscribe to "funding-rate" channel. // retrieve funding rate. Data will be pushed in 30s to 90s. -func (ok *Okx) FundingRateSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelFundingRate, assetType, pair, false, true, false) +func (ok *Okx) FundingRateSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelFundingRate, assetType, pair, false, true, false) } // IndexCandlesticksSubscription a method to subscribe and unsubscribe to "index-candle*" channel // to retrieve the candlesticks data of the index. Data will be pushed every 500 ms. -func (ok *Okx) IndexCandlesticksSubscription(operation, channel string, assetType asset.Item, pair currency.Pair) error { +func (ok *Okx) IndexCandlesticksSubscription(ctx context.Context, operation, channel string, assetType asset.Item, pair currency.Pair) error { if _, okay := candlesticksIndexPriceMap[channel]; !okay { return fmt.Errorf("%w channel: %v", errMissingValidChannelInformation, channel) } - return ok.wsChannelSubscription(operation, channel, assetType, pair, false, true, false) + return ok.wsChannelSubscription(ctx, operation, channel, assetType, pair, false, true, false) } // IndexTickerChannel a method to subscribe and unsubscribe to "index-tickers" channel -func (ok *Okx) IndexTickerChannel(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelIndexTickers, assetType, pair, false, true, false) +func (ok *Okx) IndexTickerChannel(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelIndexTickers, assetType, pair, false, true, false) } // StatusSubscription get the status of system maintenance and push when the system maintenance status changes. // First subscription: "Push the latest change data"; every time there is a state change, push the changed content -func (ok *Okx) StatusSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelStatus, assetType, pair, false, false, false) +func (ok *Okx) StatusSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelStatus, assetType, pair, false, false, false) } // PublicStructureBlockTradesSubscription a method to subscribe or unsubscribe to "public-struc-block-trades" channel -func (ok *Okx) PublicStructureBlockTradesSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelPublicStrucBlockTrades, assetType, pair, false, false, false) +func (ok *Okx) PublicStructureBlockTradesSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelPublicStrucBlockTrades, assetType, pair, false, false, false) } // BlockTickerSubscription a method to subscribe and unsubscribe to a "block-tickers" channel to retrieve the latest block trading volume in the last 24 hours. // The data will be pushed when triggered by transaction execution event. In addition, it will also be pushed in 5 minutes interval according to subscription granularity. -func (ok *Okx) BlockTickerSubscription(operation string, assetType asset.Item, pair currency.Pair) error { - return ok.wsChannelSubscription(operation, okxChannelBlockTickers, assetType, pair, false, true, false) +func (ok *Okx) BlockTickerSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelBlockTickers, assetType, pair, false, true, false) +} + +// PublicBlockTradesSubscription a method to subscribe and unsubscribe to a "public-block-trades" channel to retrieve the recent block trades data by individual legs. +// Each leg in a block trade is pushed in a separate update. Data will be pushed whenever there is a block trade. +func (ok *Okx) PublicBlockTradesSubscription(ctx context.Context, operation string, assetType asset.Item, pair currency.Pair) error { + return ok.wsChannelSubscription(ctx, operation, channelPublicBlockTrades, assetType, pair, false, true, false) +} + +// Websocket Spread Trade methods + +// handleIncomingData extracts the incoming data to the dataHolder interface after few checks and return nil or return error message otherwise +func (ok *Okx) handleIncomingData(data *wsIncomingData, dataHolder any) error { + if data.StatusCode == "0" || data.StatusCode == "1" { + err := data.copyResponseToInterface(dataHolder) + if err != nil { + return err + } + if dataHolder == nil { + return fmt.Errorf("%w, invalid incoming data", common.ErrNoResponse) + } + return nil + } + return fmt.Errorf("error code:%s error message: %v", data.StatusCode, ErrorCodes[data.StatusCode]) +} + +// WsPlaceSpreadOrder places a spread order thought the websocket connection stream, and returns a SubmitResponse and error message. +func (ok *Okx) WsPlaceSpreadOrder(ctx context.Context, arg *SpreadOrderParam) (*SpreadOrderResponse, error) { + if arg == nil || *arg == (SpreadOrderParam{}) { + return nil, common.ErrNilPointer + } + err := ok.validatePlaceSpreadOrderParam(arg) + if err != nil { + return nil, err + } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } + randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) + if err != nil { + return nil, err + } + input := WsOperationInput{ + ID: randomID, + Arguments: []SpreadOrderParam{*arg}, + Operation: okxSpreadOrder, + } + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) + if err != nil { + return nil, err + } + timer := time.NewTimer(ok.WebsocketResponseMaxLimit) + wsResponse := make(chan *wsIncomingData) + ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ + ID: randomID, + Chan: wsResponse, + } + defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() + for { + select { + case data := <-wsResponse: + if data.Operation == okxSpreadOrder && data.ID == input.ID { + var dataHolder *SpreadOrderResponse + err = ok.handleIncomingData(data, &dataHolder) + if err != nil { + return nil, err + } + if data.StatusCode == "1" { + return nil, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) + } + return dataHolder, nil + } + continue + case <-timer.C: + timer.Stop() + return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", + ok.Name, + input.Operation) + } + } +} + +// WsAmandSpreadOrder amends incomplete spread order through the websocket channel. +func (ok *Okx) WsAmandSpreadOrder(ctx context.Context, arg *AmendSpreadOrderParam) (*SpreadOrderResponse, error) { + if arg == nil || *arg == (AmendSpreadOrderParam{}) { + return nil, common.ErrEmptyParams + } + if arg.OrderID == "" && arg.ClientOrderID == "" { + return nil, order.ErrOrderIDNotSet + } + if arg.NewPrice == 0 && arg.NewSize == 0 { + return nil, errSizeOrPriceIsRequired + } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } + randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) + if err != nil { + return nil, err + } + input := WsOperationInput{ + ID: randomID, + Arguments: []AmendSpreadOrderParam{*arg}, + Operation: okxSpreadAmendOrder, + } + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) + if err != nil { + return nil, err + } + timer := time.NewTimer(ok.WebsocketResponseMaxLimit) + wsResponse := make(chan *wsIncomingData) + ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ + ID: randomID, + Chan: wsResponse, + } + defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() + for { + select { + case data := <-wsResponse: + if data.Operation == okxSpreadAmendOrder && data.ID == input.ID { + var dataHolder *SpreadOrderResponse + err = ok.handleIncomingData(data, &dataHolder) + if err != nil { + return nil, err + } + if data.StatusCode == "1" { + return nil, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) + } + return dataHolder, nil + } + continue + case <-timer.C: + timer.Stop() + return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", + ok.Name, + input.Operation) + } + } +} + +// WsCancelSpreadOrder cancels an incomplete spread order through the websocket connection. +func (ok *Okx) WsCancelSpreadOrder(ctx context.Context, orderID, clientOrderID string) (*SpreadOrderResponse, error) { + if orderID == "" && clientOrderID == "" { + return nil, order.ErrOrderIDNotSet + } + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return nil, errWebsocketStreamNotAuthenticated + } + arg := make(map[string]string) + if orderID != "" { + arg["ordId"] = orderID + } + if clientOrderID != "" { + arg["clOrdId"] = clientOrderID + } + randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) + if err != nil { + return nil, err + } + input := WsOperationInput{ + ID: randomID, + Arguments: []map[string]string{arg}, + Operation: okxSpreadCancelOrder, + } + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) + if err != nil { + return nil, err + } + timer := time.NewTimer(ok.WebsocketResponseMaxLimit) + wsResponse := make(chan *wsIncomingData) + ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ + ID: randomID, + Chan: wsResponse, + } + defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() + for { + select { + case data := <-wsResponse: + if data.Operation == okxSpreadCancelOrder && data.ID == input.ID { + var dataHolder *SpreadOrderResponse + err = ok.handleIncomingData(data, &dataHolder) + if err != nil { + return nil, err + } + if data.StatusCode == "1" { + return nil, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) + } + return dataHolder, nil + } + continue + case <-timer.C: + timer.Stop() + return nil, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", + ok.Name, + input.Operation) + } + } +} + +// WsCancelAllSpreadOrders cancels all spread orders and return success message through the websocket channel. +func (ok *Okx) WsCancelAllSpreadOrders(ctx context.Context, spreadID string) (bool, error) { + if !ok.AreCredentialsValid(ctx) || !ok.Websocket.CanUseAuthenticatedEndpoints() { + return false, errWebsocketStreamNotAuthenticated + } + arg := make(map[string]string, 1) + if spreadID != "" { + arg["sprdId"] = spreadID + } + randomID, err := common.GenerateRandomString(32, common.SmallLetters, common.CapitalLetters, common.NumberCharacters) + if err != nil { + return false, err + } + input := WsOperationInput{ + ID: randomID, + Arguments: []map[string]string{arg}, + Operation: okxSpreadCancelAllOrders, + } + err = ok.Websocket.AuthConn.SendJSONMessage(ctx, request.UnAuth, input) + if err != nil { + return false, err + } + timer := time.NewTimer(ok.WebsocketResponseMaxLimit) + wsResponse := make(chan *wsIncomingData) + ok.WsResponseMultiplexer.Register <- &wsRequestInfo{ + ID: randomID, + Chan: wsResponse, + } + defer func() { ok.WsResponseMultiplexer.Unregister <- randomID }() + for { + select { + case data := <-wsResponse: + if data.Operation == okxSpreadCancelAllOrders && data.ID == input.ID { + dataHolder := &ResponseSuccess{} + err = ok.handleIncomingData(data, &dataHolder) + if err != nil { + return false, err + } + if data.StatusCode == "1" { + return false, fmt.Errorf("error code:%s message: %s", dataHolder.StatusCode, dataHolder.StatusMessage) + } + return dataHolder.Result, nil + } + continue + case <-timer.C: + timer.Stop() + return false, fmt.Errorf("%s websocket connection: timeout waiting for response with an operation: %v", + ok.Name, + input.Operation) + } + } } // channelName converts global subscription channel names to exchange specific names @@ -2009,7 +2613,7 @@ func isAssetChannel(s *subscription.Subscription) bool { // isSymbolChannel returns if the channel expects one Symbol per subscription func isSymbolChannel(s *subscription.Subscription) bool { switch s.Channel { - case subscription.CandlesChannel, subscription.TickerChannel, subscription.OrderbookChannel, subscription.AllTradesChannel, okxChannelFundingRate: + case subscription.CandlesChannel, subscription.TickerChannel, subscription.OrderbookChannel, subscription.AllTradesChannel, channelFundingRate: return true } return false diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index 344fa4ba883..db4c7f15613 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -37,7 +37,7 @@ import ( ) const ( - okxWebsocketResponseMaxLimit = time.Second * 3 + websocketResponseMaxLimit = time.Second * 3 ) // SetDefaults sets the basic defaults for Okx @@ -51,12 +51,15 @@ func (ok *Okx) SetDefaults() { ok.API.CredentialsValidator.RequiresSecret = true ok.API.CredentialsValidator.RequiresClientID = true + ok.instrumentsInfoMap = make(map[string][]Instrument) + cpf := ¤cy.PairFormat{ Delimiter: currency.DashDelimiter, Uppercase: true, } - err := ok.SetGlobalPairsManager(cpf, cpf, asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options, asset.Margin) + // In this exchange, we represent deliverable futures contracts as 'FUTURES'/asset.Futures and perpetual futures as 'SWAP'/asset.PerpetualSwap + err := ok.SetGlobalPairsManager(cpf, cpf, asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options, asset.Margin, asset.Spread) if err != nil { log.Errorln(log.ExchangeSys, err) } @@ -167,16 +170,16 @@ func (ok *Okx) SetDefaults() { ok.API.Endpoints = ok.NewEndpoints() err = ok.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ - exchange.RestSpot: okxAPIURL, - exchange.WebsocketSpot: okxAPIWebsocketPublicURL, + exchange.RestSpot: apiURL, + exchange.WebsocketSpot: apiWebsocketPublicURL, }) if err != nil { log.Errorln(log.ExchangeSys, err) } ok.Websocket = stream.NewWebsocket() - ok.WebsocketResponseMaxLimit = okxWebsocketResponseMaxLimit - ok.WebsocketResponseCheckTimeout = okxWebsocketResponseMaxLimit + ok.WebsocketResponseMaxLimit = websocketResponseMaxLimit + ok.WebsocketResponseCheckTimeout = websocketResponseMaxLimit ok.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit } @@ -207,7 +210,7 @@ func (ok *Okx) Setup(exch *config.Exchange) error { } if err := ok.Websocket.Setup(&stream.WebsocketSetup{ ExchangeConfig: exch, - DefaultURL: okxAPIWebsocketPublicURL, + DefaultURL: apiWebsocketPublicURL, RunningURL: wsRunningEndpoint, Connector: ok.WsConnect, Subscriber: ok.Subscribe, @@ -226,18 +229,18 @@ func (ok *Okx) Setup(exch *config.Exchange) error { go ok.WsResponseMultiplexer.Run() if err := ok.Websocket.SetupNewConnection(&stream.ConnectionSetup{ - URL: okxAPIWebsocketPublicURL, + URL: apiWebsocketPublicURL, ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: okxWebsocketResponseMaxLimit, + ResponseMaxLimit: websocketResponseMaxLimit, RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1), }); err != nil { return err } return ok.Websocket.SetupNewConnection(&stream.ConnectionSetup{ - URL: okxAPIWebsocketPrivateURL, + URL: apiWebsocketPrivateURL, ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, - ResponseMaxLimit: okxWebsocketResponseMaxLimit, + ResponseMaxLimit: websocketResponseMaxLimit, Authenticated: true, RateLimit: request.NewRateLimitWithWeight(time.Second, 2, 1), }) @@ -257,32 +260,60 @@ func (ok *Okx) Shutdown() error { // GetServerTime returns the current exchange server time. func (ok *Okx) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) { - return ok.GetSystemTime(ctx) + t, err := ok.GetSystemTime(ctx) + return t.Time(), err } // FetchTradablePairs returns a list of the exchanges tradable pairs func (ok *Okx) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { - insts, err := ok.getInstrumentsForAsset(ctx, a) - if err != nil { - return nil, err - } - pf, err := ok.CurrencyPairs.GetFormat(a, false) - if err != nil { - return nil, err - } - pairs := make([]currency.Pair, len(insts)) - for x := range insts { - pairs[x], err = currency.NewPairDelimiter(insts[x].InstrumentID, pf.Delimiter) + switch a { + case asset.Options, asset.Futures, asset.Spot, asset.PerpetualSwap, asset.Margin: + format, err := ok.GetPairFormat(a, true) + if err != nil { + return nil, err + } + insts, err := ok.getInstrumentsForAsset(ctx, a) + if err != nil { + return nil, err + } + var pair currency.Pair + pairs := make([]currency.Pair, 0, len(insts)) + for x := range insts { + if insts[x].State != "live" { + continue + } + pair, err = currency.NewPairDelimiter(insts[x].InstrumentID, format.Delimiter) + if err != nil { + return nil, err + } + pairs = append(pairs, pair) + } + return pairs, nil + case asset.Spread: + format, err := ok.GetPairFormat(a, true) if err != nil { return nil, err } + spreadInstruments, err := ok.GetPublicSpreads(ctx, "", "", "", "live") + if err != nil { + return nil, fmt.Errorf("%w asset type: %v", err, a) + } + pairs := make(currency.Pairs, len(spreadInstruments)) + for x := range spreadInstruments { + pairs[x], err = currency.NewPairDelimiter(spreadInstruments[x].SpreadID, format.Delimiter) + if err != nil { + return nil, err + } + } + return pairs, nil + default: + return nil, fmt.Errorf("%w asset type: %v", asset.ErrNotSupported, a) } - return pairs, nil } // UpdateTradablePairs updates the exchanges available pairs and stores them in the exchanges config func (ok *Okx) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error { - assetTypes := ok.GetAssetTypes(false) + assetTypes := ok.GetAssetTypes(true) for i := range assetTypes { pairs, err := ok.FetchTradablePairs(ctx, assetTypes[i]) if err != nil { @@ -298,45 +329,69 @@ func (ok *Okx) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error // UpdateOrderExecutionLimits sets exchange execution order limits for an asset type func (ok *Okx) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { - insts, err := ok.getInstrumentsForAsset(ctx, a) - if err != nil { - return err - } - if len(insts) == 0 { - return errNoInstrumentFound - } - limits := make([]order.MinMaxLevel, len(insts)) - for x := range insts { - pair, err := currency.NewPairFromString(insts[x].InstrumentID) + switch a { + case asset.Spot, asset.Margin, asset.Options, + asset.PerpetualSwap, asset.Futures: + insts, err := ok.getInstrumentsForAsset(ctx, a) if err != nil { return err } - - limits[x] = order.MinMaxLevel{ - Pair: pair, - Asset: a, - PriceStepIncrementSize: insts[x].TickSize.Float64(), - MinimumBaseAmount: insts[x].MinimumOrderSize.Float64(), + if len(insts) == 0 { + return common.ErrNoResponse + } + limits := make([]order.MinMaxLevel, len(insts)) + for x := range insts { + pair, err := currency.NewPairFromString(insts[x].InstrumentID) + if err != nil { + return err + } + limits[x] = order.MinMaxLevel{ + Pair: pair, + Asset: a, + PriceStepIncrementSize: insts[x].TickSize.Float64(), + MinimumBaseAmount: insts[x].MinimumOrderSize.Float64(), + } } + return ok.LoadLimits(limits) + case asset.Spread: + insts, err := ok.GetPublicSpreads(ctx, "", "", "", "live") + if err != nil { + return err + } + if len(insts) == 0 { + return common.ErrNoResponse + } + limits := make([]order.MinMaxLevel, len(insts)) + for x := range insts { + pair, err := currency.NewPairFromString(insts[x].SpreadID) + if err != nil { + return err + } + limits[x] = order.MinMaxLevel{ + Pair: pair, + Asset: a, + PriceStepIncrementSize: insts[x].MinSize.Float64(), + MinimumBaseAmount: insts[x].MinSize.Float64(), + QuoteStepIncrementSize: insts[x].TickSize.Float64(), + } + } + return ok.LoadLimits(limits) + default: + return fmt.Errorf("%w %v", asset.ErrNotSupported, a) } - - return ok.LoadLimits(limits) } // UpdateTicker updates and returns the ticker for a currency pair func (ok *Okx) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) { - pairFormat, err := ok.GetPairFormat(a, true) + var err error + p, err = ok.FormatExchangeCurrency(p, a) if err != nil { return nil, err } - if p.IsEmpty() { - return nil, currency.ErrCurrencyPairEmpty - } - instrumentID := pairFormat.Format(p) if !ok.SupportsAsset(a) { return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a) } - mdata, err := ok.GetTicker(ctx, instrumentID) + mdata, err := ok.GetTicker(ctx, p.String()) if err != nil { return nil, err } @@ -374,52 +429,92 @@ func (ok *Okx) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) // UpdateTickers updates all currency pairs of a given asset type func (ok *Okx) UpdateTickers(ctx context.Context, assetType asset.Item) error { - pairs, err := ok.GetEnabledPairs(assetType) - if err != nil { - return err - } - - instrumentType := ok.GetInstrumentTypeFromAssetItem(assetType) - if assetType == asset.Margin { - instrumentType = okxInstTypeSpot - } - ticks, err := ok.GetTickers(ctx, instrumentType, "", "") - if err != nil { - return err - } - - for y := range ticks { - pair, err := currency.NewPairFromString(ticks[y].InstrumentID) + switch assetType { + case asset.Spread: + format, err := ok.GetPairFormat(asset.Spread, true) if err != nil { return err } - for i := range pairs { - pairFmt, err := ok.FormatExchangeCurrency(pairs[i], assetType) + pairs, err := ok.GetEnabledPairs(assetType) + if err != nil { + return err + } + for y := range pairs { + var spreadTickers []SpreadTicker + spreadTickers, err = ok.GetPublicSpreadTickers(ctx, format.Format(pairs[y])) if err != nil { return err } - if !pair.Equal(pairFmt) { - continue + for x := range spreadTickers { + pair, err := currency.NewPairDelimiter(spreadTickers[x].SpreadID, format.Delimiter) + if err != nil { + return err + } + err = ticker.ProcessTicker(&ticker.Price{ + Last: spreadTickers[x].Last.Float64(), + Bid: spreadTickers[x].BidPrice.Float64(), + BidSize: spreadTickers[x].BidSize.Float64(), + Ask: spreadTickers[x].AskPrice.Float64(), + AskSize: spreadTickers[x].AskSize.Float64(), + Pair: pair, + ExchangeName: ok.Name, + AssetType: assetType, + }) + if err != nil { + return err + } } - err = ticker.ProcessTicker(&ticker.Price{ - Last: ticks[y].LastTradePrice.Float64(), - High: ticks[y].High24H.Float64(), - Low: ticks[y].Low24H.Float64(), - Bid: ticks[y].BestBidPrice.Float64(), - BidSize: ticks[y].BestBidSize.Float64(), - Ask: ticks[y].BestAskPrice.Float64(), - AskSize: ticks[y].BestAskSize.Float64(), - Volume: ticks[y].Vol24H.Float64(), - QuoteVolume: ticks[y].VolCcy24H.Float64(), - Open: ticks[y].Open24H.Float64(), - Pair: pairFmt, - ExchangeName: ok.Name, - AssetType: assetType, - }) + } + case asset.Spot, asset.PerpetualSwap, asset.Futures, asset.Options, asset.Margin: + pairs, err := ok.GetEnabledPairs(assetType) + if err != nil { + return err + } + + instrumentType := GetInstrumentTypeFromAssetItem(assetType) + if assetType == asset.Margin { + instrumentType = instTypeSpot + } + ticks, err := ok.GetTickers(ctx, instrumentType, "", "") + if err != nil { + return err + } + + for y := range ticks { + pair, err := ok.GetPairFromInstrumentID(ticks[y].InstrumentID) if err != nil { return err } + for i := range pairs { + pairFmt, err := ok.FormatExchangeCurrency(pairs[i], assetType) + if err != nil { + return err + } + if !pair.Equal(pairFmt) { + continue + } + err = ticker.ProcessTicker(&ticker.Price{ + Last: ticks[y].LastTradePrice.Float64(), + High: ticks[y].High24H.Float64(), + Low: ticks[y].Low24H.Float64(), + Bid: ticks[y].BestBidPrice.Float64(), + BidSize: ticks[y].BestBidSize.Float64(), + Ask: ticks[y].BestAskPrice.Float64(), + AskSize: ticks[y].BestAskSize.Float64(), + Volume: ticks[y].Vol24H.Float64(), + QuoteVolume: ticks[y].VolCcy24H.Float64(), + Open: ticks[y].Open24H.Float64(), + Pair: pairFmt, + ExchangeName: ok.Name, + AssetType: assetType, + }) + if err != nil { + return err + } + } } + default: + return fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } return nil } @@ -451,56 +546,91 @@ func (ok *Okx) UpdateOrderbook(ctx context.Context, pair currency.Pair, assetTyp if pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } - if err := ok.CurrencyPairs.IsAssetEnabled(assetType); err != nil { - return nil, err - } - book := &orderbook.Base{ - Exchange: ok.Name, - Pair: pair, - Asset: assetType, - VerifyOrderbook: ok.CanVerifyOrderbook, - } - var orderbookNew *OrderBookResponse var err error - err = ok.CurrencyPairs.IsAssetEnabled(assetType) - if err != nil { - return nil, err - } - var instrumentID string - pairFormat, err := ok.GetPairFormat(assetType, true) - if err != nil { - return nil, err - } - if !pair.IsPopulated() { - return nil, errIncompleteCurrencyPair - } - instrumentID = pairFormat.Format(pair) - orderbookNew, err = ok.GetOrderBookDepth(ctx, instrumentID, 400) - if err != nil { - return book, err - } + switch assetType { + case asset.Spread: + var ( + pairFormat currency.PairFormat + spreadOrderbook []SpreadOrderbook + ) + pairFormat, err = ok.GetPairFormat(assetType, true) + if err != nil { + return nil, err + } + spreadOrderbook, err = ok.GetPublicSpreadOrderBooks(ctx, pairFormat.Format(pair), 50) + if err != nil { + return nil, err + } + for y := range spreadOrderbook { + book := &orderbook.Base{ + Exchange: ok.Name, + Pair: pair, + Asset: assetType, + VerifyOrderbook: ok.CanVerifyOrderbook, + } + book.Bids = make(orderbook.Tranches, len(spreadOrderbook[y].Bids)) + for a := range spreadOrderbook[y].Bids { + book.Bids[a].Price = spreadOrderbook[y].Bids[a][0].Float64() + book.Bids[a].Amount = spreadOrderbook[y].Bids[a][1].Float64() + book.Bids[a].OrderCount = spreadOrderbook[y].Bids[a][2].Int64() + } + book.Asks = make(orderbook.Tranches, len(spreadOrderbook[y].Asks)) + for a := range spreadOrderbook[y].Asks { + book.Asks[a].Price = spreadOrderbook[y].Asks[a][0].Float64() + book.Asks[a].Amount = spreadOrderbook[y].Asks[a][1].Float64() + book.Asks[a].OrderCount = spreadOrderbook[y].Asks[a][2].Int64() + } + err = book.Process() + if err != nil { + return book, err + } + } + case asset.Spot, asset.Options, asset.Margin, asset.PerpetualSwap, asset.Futures: + err = ok.CurrencyPairs.IsAssetEnabled(assetType) + if err != nil { + return nil, err + } + var instrumentID string + pairFormat, err := ok.GetPairFormat(assetType, true) + if err != nil { + return nil, err + } + if !pair.IsPopulated() { + return nil, currency.ErrCurrencyPairsEmpty + } + instrumentID = pairFormat.Format(pair) + book := &orderbook.Base{ + Exchange: ok.Name, + Pair: pair, + Asset: assetType, + VerifyOrderbook: ok.CanVerifyOrderbook, + } + var orderBookD *OrderBookResponseDetail + orderBookD, err = ok.GetOrderBookDepth(ctx, instrumentID, 400) + if err != nil { + return book, err + } - orderBookD, err := orderbookNew.GetOrderBookResponseDetail() - if err != nil { - return nil, err - } - book.Bids = make(orderbook.Tranches, len(orderBookD.Bids)) - for x := range orderBookD.Bids { - book.Bids[x] = orderbook.Tranche{ - Amount: orderBookD.Bids[x].BaseCurrencies, - Price: orderBookD.Bids[x].DepthPrice, + book.Bids = make(orderbook.Tranches, len(orderBookD.Bids)) + for x := range orderBookD.Bids { + book.Bids[x] = orderbook.Tranche{ + Amount: orderBookD.Bids[x].Amount.Float64(), + Price: orderBookD.Bids[x].DepthPrice.Float64(), + } } - } - book.Asks = make(orderbook.Tranches, len(orderBookD.Asks)) - for x := range orderBookD.Asks { - book.Asks[x] = orderbook.Tranche{ - Amount: orderBookD.Asks[x].NumberOfContracts, - Price: orderBookD.Asks[x].DepthPrice, + book.Asks = make(orderbook.Tranches, len(orderBookD.Asks)) + for x := range orderBookD.Asks { + book.Asks[x] = orderbook.Tranche{ + Amount: orderBookD.Asks[x].Amount.Float64(), + Price: orderBookD.Asks[x].DepthPrice.Float64(), + } } - } - err = book.Process() - if err != nil { - return book, err + err = book.Process() + if err != nil { + return book, err + } + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } return orderbook.Get(ok.Name, pair, assetType) } @@ -517,7 +647,7 @@ func (ok *Okx) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (acc if !ok.SupportsAsset(assetType) { return info, fmt.Errorf("%w: %v", asset.ErrNotSupported, assetType) } - accountBalances, err := ok.AccountBalance(ctx, "") + accountBalances, err := ok.AccountBalance(ctx, currency.EMPTYCODE) if err != nil { return info, err } @@ -560,11 +690,12 @@ func (ok *Okx) FetchAccountInfo(ctx context.Context, assetType asset.Item) (acco // GetAccountFundingHistory returns funding history, deposits and withdrawals func (ok *Okx) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) { - depositHistories, err := ok.GetCurrencyDepositHistory(ctx, "", "", "", time.Time{}, time.Time{}, -1, 0) + depositHistories, err := ok.GetCurrencyDepositHistory(ctx, currency.EMPTYCODE, "", "", "", "", time.Time{}, time.Time{}, -1, 0) if err != nil { return nil, err } - withdrawalHistories, err := ok.GetWithdrawalHistory(ctx, "", "", "", "", "", time.Time{}, time.Time{}, -5) + + withdrawalHistories, err := ok.GetWithdrawalHistory(ctx, currency.EMPTYCODE, "", "", "", "", time.Time{}, time.Time{}, -5) if err != nil { return nil, err } @@ -572,7 +703,7 @@ func (ok *Okx) GetAccountFundingHistory(ctx context.Context) ([]exchange.Funding for x := range depositHistories { resp = append(resp, exchange.FundingHistory{ ExchangeName: ok.Name, - Status: strconv.Itoa(depositHistories[x].State), + Status: strconv.FormatInt(depositHistories[x].State.Int64(), 10), Timestamp: depositHistories[x].Timestamp.Time(), Currency: depositHistories[x].Currency, Amount: depositHistories[x].Amount.Float64(), @@ -601,7 +732,7 @@ func (ok *Okx) GetAccountFundingHistory(ctx context.Context) ([]exchange.Funding // GetWithdrawalsHistory returns previous withdrawals data func (ok *Okx) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) { - withdrawals, err := ok.GetWithdrawalHistory(ctx, c.String(), "", "", "", "", time.Time{}, time.Time{}, -5) + withdrawals, err := ok.GetWithdrawalHistory(ctx, c, "", "", "", "", time.Time{}, time.Time{}, -5) if err != nil { return nil, err } @@ -625,31 +756,62 @@ func (ok *Okx) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ ass // GetRecentTrades returns the most recent trades for a currency and asset func (ok *Okx) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) { - pairFormat, err := ok.GetPairFormat(assetType, true) - if err != nil { - return nil, err - } - if p.IsEmpty() { - return nil, currency.ErrCurrencyPairEmpty - } - instrumentID := pairFormat.Format(p) - tradeData, err := ok.GetTrades(ctx, instrumentID, 1000) + format, err := ok.GetPairFormat(assetType, true) if err != nil { return nil, err } + var resp []trade.Data + switch assetType { + case asset.Spread: + var spreadTrades []SpreadPublicTradeItem + spreadTrades, err = ok.GetPublicSpreadTrades(ctx, "") + if err != nil { + return nil, err + } + resp = make([]trade.Data, len(spreadTrades)) + var oSide order.Side + for x := range spreadTrades { + oSide, err = order.StringToOrderSide(spreadTrades[x].Side) + if err != nil { + return nil, err + } + resp[x] = trade.Data{ + TID: spreadTrades[x].TradeID, + Exchange: ok.Name, + CurrencyPair: p, + AssetType: assetType, + Side: oSide, + Price: spreadTrades[x].Price.Float64(), + Amount: spreadTrades[x].Size.Float64(), + Timestamp: spreadTrades[x].Timestamp.Time(), + } + } + case asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options: + if p.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + instrumentID := format.Format(p) + var tradeData []TradeResponse + tradeData, err = ok.GetTrades(ctx, instrumentID, 1000) + if err != nil { + return nil, err + } - resp := make([]trade.Data, len(tradeData)) - for x := range tradeData { - resp[x] = trade.Data{ - TID: tradeData[x].TradeID, - Exchange: ok.Name, - CurrencyPair: p, - AssetType: assetType, - Side: tradeData[x].Side, - Price: tradeData[x].Price.Float64(), - Amount: tradeData[x].Quantity.Float64(), - Timestamp: tradeData[x].Timestamp.Time(), + resp = make([]trade.Data, len(tradeData)) + for x := range tradeData { + resp[x] = trade.Data{ + TID: tradeData[x].TradeID, + Exchange: ok.Name, + CurrencyPair: p, + AssetType: assetType, + Side: tradeData[x].Side, + Price: tradeData[x].Price.Float64(), + Amount: tradeData[x].Quantity.Float64(), + Timestamp: tradeData[x].Timestamp.Time(), + } } + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } if ok.IsSaveTradeDataEnabled() { err = trade.AddTradesToBuffer(ok.Name, resp...) @@ -719,14 +881,11 @@ allTrades: // SubmitOrder submits a new order func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(ok.GetTradingRequirements()); err != nil { - return nil, err - } if !ok.SupportsAsset(s.AssetType) { return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, s.AssetType) } if s.Amount <= 0 { - return nil, errors.New("amount, or size (sz) of quantity to buy or sell hast to be greater than zero") + return nil, order.ErrAmountBelowMin } pairFormat, err := ok.GetPairFormat(s.AssetType, true) if err != nil { @@ -735,18 +894,18 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR if s.Pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } - instrumentID := pairFormat.Format(s.Pair) + pairString := pairFormat.Format(s.Pair) tradeMode := ok.marginTypeToString(s.MarginType) - if s.Leverage != 0 && s.Leverage != 1 { + if s.AssetType.IsFutures() && s.Leverage != 0 && s.Leverage != 1 { return nil, fmt.Errorf("%w received '%v'", order.ErrSubmitLeverageNotSupported, s.Leverage) } - var sideType string - if s.Side.IsLong() { - sideType = order.Buy.Lower() - } else { - sideType = order.Sell.Lower() + var sideType, positionSide string + switch s.AssetType { + case asset.Spot, asset.Margin, asset.Spread: + sideType = s.Side.String() + case asset.Futures, asset.PerpetualSwap, asset.Options: + positionSide = s.Side.Lower() } - amount := s.Amount var targetCurrency string if s.AssetType == asset.Spot && s.Type == order.Market { @@ -756,59 +915,224 @@ func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitR targetCurrency = "quote_ccy" } } - - var orderRequest = &PlaceOrderRequestParam{ - InstrumentID: instrumentID, - TradeMode: tradeMode, - Side: sideType, - OrderType: s.Type.Lower(), - Amount: amount, - ClientOrderID: s.ClientOrderID, - Price: s.Price, - QuantityType: targetCurrency, - } - switch s.Type.Lower() { - case OkxOrderLimit, OkxOrderPostOnly, OkxOrderFOK, OkxOrderIOC: - orderRequest.Price = s.Price - } - var placeOrderResponse *OrderData - if s.AssetType == asset.PerpetualSwap || s.AssetType == asset.Futures { - if s.Type.Lower() == "" { - orderRequest.OrderType = OkxOrderOptimalLimitIOC + // If asset type is spread + if s.AssetType == asset.Spread { + spreadParam := &SpreadOrderParam{ + SpreadID: pairString, + ClientOrderID: s.ClientOrderID, + Side: sideType, + OrderType: s.Type.Lower(), + Size: s.Amount, + Price: s.Price, } - // TODO: handle positionSideLong while side is Short and positionSideShort while side is Long - if s.Side.IsLong() { - orderRequest.PositionSide = positionSideLong + var placeSpreadOrderResponse *SpreadOrderResponse + if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + placeSpreadOrderResponse, err = ok.WsPlaceSpreadOrder(ctx, spreadParam) + if err != nil { + return nil, err + } } else { - orderRequest.PositionSide = positionSideShort + placeSpreadOrderResponse, err = ok.PlaceSpreadOrder(ctx, spreadParam) + if err != nil { + return nil, err + } } + return s.DeriveSubmitResponse(placeSpreadOrderResponse.OrderID) } - if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - placeOrderResponse, err = ok.WsPlaceOrder(orderRequest) - if err != nil { - return nil, err + orderTypeString, err := orderTypeString(s.Type) + if err != nil { + return nil, err + } + var placeOrderResponse *OrderData + var result *AlgoOrder + switch orderTypeString { + case orderLimit, orderMarket, orderPostOnly, orderFOK, orderIOC, orderOptimalLimitIOC, "mmp", "mmp_and_post_only": + var orderRequest = &PlaceOrderRequestParam{ + InstrumentID: pairString, + TradeMode: tradeMode, + Side: sideType, + PositionSide: positionSide, + OrderType: orderTypeString, + Amount: amount, + ClientOrderID: s.ClientOrderID, + Price: s.Price, + QuantityType: targetCurrency, + AssetType: s.AssetType, + } + switch s.Type.Lower() { + case orderLimit, orderPostOnly, orderFOK, orderIOC: + orderRequest.Price = s.Price + } + if s.AssetType == asset.PerpetualSwap || s.AssetType == asset.Futures { + if s.Type.Lower() == "" { + orderRequest.OrderType = orderOptimalLimitIOC + } + // TODO: handle positionSideLong while side is Short and positionSideShort while side is Long + if s.Side.IsLong() { + orderRequest.PositionSide = positionSideLong + } else { + orderRequest.PositionSide = positionSideShort + } } - } else { - placeOrderResponse, err = ok.PlaceOrder(ctx, orderRequest, s.AssetType) - if err != nil { - return nil, err + if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + placeOrderResponse, err = ok.WsPlaceOrder(ctx, orderRequest) + if err != nil { + return nil, err + } + } else { + placeOrderResponse, err = ok.PlaceOrder(ctx, orderRequest) + if err != nil { + return nil, err + } } + return s.DeriveSubmitResponse(placeOrderResponse.OrderID) + case "trigger": + result, err = ok.PlaceTriggerAlgoOrder(ctx, &AlgoOrderParams{ + InstrumentID: pairString, + TradeMode: tradeMode, + Side: s.Side.Lower(), + PositionSide: positionSide, + OrderType: orderTypeString, + Size: s.Amount, + ReduceOnly: s.ReduceOnly, + TriggerPrice: s.TriggerPrice, + TriggerPriceType: priceTypeString(s.TriggerPriceType), + }) + case "conditional": + // Trigger Price and type are used as a stop losss trigger price and type. + result, err = ok.PlaceTakeProfitStopLossOrder(ctx, &AlgoOrderParams{ + InstrumentID: pairString, + TradeMode: tradeMode, + Side: s.Side.Lower(), + PositionSide: positionSide, + OrderType: orderTypeString, + Size: s.Amount, + ReduceOnly: s.ReduceOnly, + StopLossTriggerPrice: s.TriggerPrice, + StopLossOrderPrice: s.Price, + StopLossTriggerPriceType: priceTypeString(s.TriggerPriceType), + }) + case "chase": + if s.TrackingMode == order.UnknownTrackingMode { + return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode) + } + if s.TrackingValue == 0 { + return nil, fmt.Errorf("%w, tracking value required", order.ErrAmountBelowMin) + } + result, err = ok.PlaceChaseAlgoOrder(ctx, &AlgoOrderParams{ + InstrumentID: pairString, + TradeMode: tradeMode, + Side: s.Side.Lower(), + PositionSide: positionSide, + OrderType: orderTypeString, + Size: s.Amount, + ReduceOnly: s.ReduceOnly, + MaxChaseType: s.TrackingMode.String(), + MaxChaseValue: s.TrackingValue, + }) + case "move_order_stop": + if s.TrackingMode == order.UnknownTrackingMode { + return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode) + } + var callbackSpread, callbackRatio float64 + switch s.TrackingMode { + case order.Distance: + callbackSpread = s.TrackingValue + case order.Percentage: + callbackRatio = s.TrackingValue + } + result, err = ok.PlaceTrailingStopOrder(ctx, &AlgoOrderParams{ + InstrumentID: pairString, + TradeMode: tradeMode, + Side: sideType, + PositionSide: positionSide, + OrderType: orderTypeString, + Size: s.Amount, + ReduceOnly: s.ReduceOnly, + CallbackRatio: callbackRatio, + CallbackSpreadVariance: callbackSpread, + ActivePrice: s.TriggerPrice, + }) + case "twap": + if s.TrackingMode == order.UnknownTrackingMode { + return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode) + } + var priceVar, priceSpread float64 + switch s.TrackingMode { + case order.Distance: + priceSpread = s.TrackingValue + case order.Percentage: + priceVar = s.TrackingValue + } + result, err = ok.PlaceTWAPOrder(ctx, &AlgoOrderParams{ + InstrumentID: pairString, + TradeMode: tradeMode, + Side: sideType, + PositionSide: positionSide, + OrderType: orderTypeString, + Size: s.Amount, + ReduceOnly: s.ReduceOnly, + PriceVariance: priceVar, + PriceSpread: priceSpread, + SizeLimit: s.Amount, + LimitPrice: s.Price, + TimeInterval: kline.FifteenMin, + }) + case "oco": + switch { + case s.RiskManagementModes.TakeProfit.Price <= 0: + return nil, fmt.Errorf("%w, take profit price is required", order.ErrPriceBelowMin) + case s.RiskManagementModes.StopLoss.Price <= 0: + return nil, fmt.Errorf("%w, stop loss price is required", order.ErrPriceBelowMin) + } + result, err = ok.PlaceAlgoOrder(ctx, &AlgoOrderParams{ + InstrumentID: pairString, + TradeMode: tradeMode, + Side: sideType, + PositionSide: positionSide, + OrderType: orderTypeString, + Size: s.Amount, + ReduceOnly: s.ReduceOnly, + + TakeProfitTriggerPrice: s.RiskManagementModes.TakeProfit.Price, + TakeProfitOrderPrice: s.RiskManagementModes.TakeProfit.LimitPrice, + TakeProfitTriggerPriceType: priceTypeString(s.TriggerPriceType), + + StopLossTriggerPrice: s.RiskManagementModes.TakeProfit.Price, + StopLossOrderPrice: s.RiskManagementModes.StopLoss.LimitPrice, + StopLossTriggerPriceType: priceTypeString(s.TriggerPriceType), + }) + default: + return nil, fmt.Errorf("%w, order type %s", order.ErrTypeIsInvalid, orderTypeString) } if err != nil { return nil, err } - return s.DeriveSubmitResponse(placeOrderResponse.OrderID) + return s.DeriveSubmitResponse(result.AlgoID) } -func (ok *Okx) marginTypeToString(m margin.Type) string { - switch m { - case margin.Isolated: - return "isolated" - case margin.Multi: - return "cross" +func priceTypeString(pt order.PriceType) string { + switch pt { + case order.LastPrice: + return "last" + case order.IndexPrice: + return "index" + case order.MarkPrice: + return "mark" default: - return "cash" + return "" + } +} + +var allowedMarginTypes = margin.Isolated | margin.NoMargin | margin.SpotIsolated + +func (ok *Okx) marginTypeToString(m margin.Type) string { + if allowedMarginTypes&m == m { + return m.String() + } else if margin.Multi == m { + return TradeModeCross } + return "" } // ModifyOrder will allow of changing orderbook placement and limit to market conversion @@ -818,8 +1142,28 @@ func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.Mo } var err error if math.Trunc(action.Amount) != action.Amount { - return nil, errors.New("okx contract amount can not be decimal") + return nil, errors.New("contract amount can not be decimal") + } + // When asset type is asset.Spread + if action.AssetType == asset.Spread { + amendSpreadOrder := &AmendSpreadOrderParam{ + OrderID: action.OrderID, + ClientOrderID: action.ClientOrderID, + NewSize: action.Amount, + NewPrice: action.Price, + } + if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + _, err = ok.WsAmandSpreadOrder(ctx, amendSpreadOrder) + } else { + _, err = ok.AmendSpreadOrder(ctx, amendSpreadOrder) + } + if err != nil { + return nil, err + } + return action.DeriveModifyResponse() } + + // For other asset type instances. pairFormat, err := ok.GetPairFormat(action.AssetType, true) if err != nil { return nil, err @@ -827,31 +1171,103 @@ func (ok *Okx) ModifyOrder(ctx context.Context, action *order.Modify) (*order.Mo if action.Pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } - amendRequest := AmendOrderRequestParams{ - InstrumentID: pairFormat.Format(action.Pair), - NewQuantity: action.Amount, - OrderID: action.OrderID, - ClientOrderID: action.ClientOrderID, - } - if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - _, err = ok.WsAmendOrder(&amendRequest) - } else { - _, err = ok.AmendOrder(ctx, &amendRequest) - } - if err != nil { - return nil, err + switch action.Type { + case order.UnknownType, order.Market, order.Limit, order.PostOnly, order.FillOrKill, order.ImmediateOrCancel, + order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: + amendRequest := AmendOrderRequestParams{ + InstrumentID: pairFormat.Format(action.Pair), + NewQuantity: action.Amount, + OrderID: action.OrderID, + ClientOrderID: action.ClientOrderID, + } + if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + _, err = ok.WsAmendOrder(ctx, &amendRequest) + } else { + _, err = ok.AmendOrder(ctx, &amendRequest) + } + if err != nil { + return nil, err + } + case order.Trigger: + if action.TriggerPrice == 0 { + return nil, fmt.Errorf("%w, trigger price required", order.ErrPriceBelowMin) + } + var postTriggerTPSLOrders []SubTPSLParams + if action.RiskManagementModes.StopLoss.Price > 0 && action.RiskManagementModes.TakeProfit.Price > 0 { + postTriggerTPSLOrders = []SubTPSLParams{ + { + NewTakeProfitTriggerPrice: action.RiskManagementModes.TakeProfit.Price, + NewTakeProfitOrderPrice: action.RiskManagementModes.TakeProfit.LimitPrice, + NewStopLossTriggerPrice: action.RiskManagementModes.StopLoss.Price, + NewStopLossOrderPrice: action.RiskManagementModes.StopLoss.Price, + NewTakeProfitTriggerPriceType: priceTypeString(action.RiskManagementModes.TakeProfit.TriggerPriceType), + NewStopLossTriggerPriceType: priceTypeString(action.RiskManagementModes.StopLoss.TriggerPriceType), + }, + } + } + _, err = ok.AmendAlgoOrder(ctx, &AmendAlgoOrderParam{ + InstrumentID: pairFormat.Format(action.Pair), + AlgoID: action.OrderID, + ClientSuppliedAlgoOrderID: action.ClientOrderID, + NewSize: action.Amount, + + NewTriggerPrice: action.TriggerPrice, + NewOrderPrice: action.Price, + NewTriggerPriceType: priceTypeString(action.TriggerPriceType), + + // An one-cancel-other order to be placed after executing the trigger order + AttachAlgoOrders: postTriggerTPSLOrders, + }) + if err != nil { + return nil, err + } + case order.OCO: + switch { + case action.RiskManagementModes.TakeProfit.Price <= 0 && + action.RiskManagementModes.TakeProfit.LimitPrice <= 0: + return nil, fmt.Errorf("%w, either take profit trigger price or order price is required", order.ErrPriceBelowMin) + case action.RiskManagementModes.StopLoss.Price <= 0 && + action.RiskManagementModes.StopLoss.LimitPrice <= 0: + return nil, fmt.Errorf("%w, either stop loss trigger price or order price is required", order.ErrPriceBelowMin) + } + _, err = ok.AmendAlgoOrder(ctx, &AmendAlgoOrderParam{ + InstrumentID: pairFormat.Format(action.Pair), + AlgoID: action.OrderID, + ClientSuppliedAlgoOrderID: action.ClientOrderID, + NewSize: action.Amount, + + NewTakeProfitTriggerPrice: action.RiskManagementModes.TakeProfit.Price, + NewTakeProfitOrderPrice: action.RiskManagementModes.TakeProfit.LimitPrice, + + NewStopLossTriggerPrice: action.RiskManagementModes.StopLoss.Price, + NewStopLossOrderPrice: action.RiskManagementModes.StopEntry.LimitPrice, + + NewTakeProfitTriggerPriceType: priceTypeString(action.RiskManagementModes.TakeProfit.TriggerPriceType), + NewStopLossTriggerPriceType: priceTypeString(action.RiskManagementModes.StopLoss.TriggerPriceType), + }) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("%w, could not amend order of type %v", order.ErrUnsupportedOrderType, action.Type) } return action.DeriveModifyResponse() } // CancelOrder cancels an order by its corresponding ID number func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error { - if err := ord.Validate(ord.StandardCancel()); err != nil { - return err - } if !ok.SupportsAsset(ord.AssetType) { return fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType) } + var err error + if ord.AssetType == asset.Spread { + if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + _, err = ok.WsCancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID) + } else { + _, err = ok.CancelSpreadOrder(ctx, ord.OrderID, ord.ClientOrderID) + } + return err + } pairFormat, err := ok.GetPairFormat(ord.AssetType, true) if err != nil { return err @@ -860,15 +1276,37 @@ func (ok *Okx) CancelOrder(ctx context.Context, ord *order.Cancel) error { return currency.ErrCurrencyPairEmpty } instrumentID := pairFormat.Format(ord.Pair) - req := CancelOrderRequestParam{ - InstrumentID: instrumentID, - OrderID: ord.OrderID, - ClientOrderID: ord.ClientOrderID, - } - if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - _, err = ok.WsCancelOrder(req) - } else { - _, err = ok.CancelSingleOrder(ctx, req) + switch ord.Type { + case order.UnknownType, order.Market, order.Limit, order.PostOnly, order.FillOrKill, order.ImmediateOrCancel, + order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: + req := CancelOrderRequestParam{ + InstrumentID: instrumentID, + OrderID: ord.OrderID, + ClientOrderID: ord.ClientOrderID, + } + if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + _, err = ok.WsCancelOrder(ctx, &req) + } else { + _, err = ok.CancelSingleOrder(ctx, &req) + } + case order.Trigger, order.OCO, order.ConditionalStop, + order.TWAP, order.TrailingStop, order.Chase: + var response *AlgoOrder + response, err = ok.CancelAdvanceAlgoOrder(ctx, []AlgoOrderCancelParams{ + { + AlgoOrderID: ord.OrderID, + InstrumentID: instrumentID, + }, + }) + if err != nil { + return err + } + if response.StatusCode != "0" { + return fmt.Errorf("sCode: %s sMessage: %s", response.StatusCode, response.StatusMessage) + } + return nil + default: + return fmt.Errorf("%w, order type %v", order.ErrUnsupportedOrderType, ord.Type) } return err } @@ -880,14 +1318,11 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order. } else if len(o) == 0 { return nil, fmt.Errorf("%w, must have at least 1 cancel order", order.ErrCancelOrderIsNil) } - cancelOrderParams := make([]CancelOrderRequestParam, len(o)) + cancelOrderParams := make([]CancelOrderRequestParam, 0, len(o)) + cancelAlgoOrderParams := make([]AlgoOrderCancelParams, 0, len(o)) var err error for x := range o { ord := o[x] - err = ord.Validate(ord.StandardCancel()) - if err != nil { - return nil, err - } if !ok.SupportsAsset(ord.AssetType) { return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, ord.AssetType) } @@ -897,31 +1332,68 @@ func (ok *Okx) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order. return nil, err } if !ord.Pair.IsPopulated() { - return nil, errIncompleteCurrencyPair + return nil, currency.ErrCurrencyPairsEmpty } - cancelOrderParams[x] = CancelOrderRequestParam{ - InstrumentID: pairFormat.Format(ord.Pair), - OrderID: ord.OrderID, - ClientOrderID: ord.ClientOrderID, + switch ord.Type { + case order.UnknownType, order.Market, order.Limit, order.PostOnly, order.FillOrKill, order.ImmediateOrCancel, + order.OptimalLimitIOC, order.MarketMakerProtection, order.MarketMakerProtectionAndPostOnly: + if o[x].ClientID == "" && o[x].OrderID == "" { + return nil, fmt.Errorf("%w, order ID required for order of type %v", order.ErrOrderIDNotSet, o[x].Type) + } + cancelOrderParams = append(cancelOrderParams, CancelOrderRequestParam{ + InstrumentID: pairFormat.Format(ord.Pair), + OrderID: ord.OrderID, + ClientOrderID: ord.ClientOrderID, + }) + case order.Trigger, order.OCO, order.ConditionalStop, + order.TWAP, order.TrailingStop, order.Chase: + if o[x].OrderID == "" { + return nil, fmt.Errorf("%w, order ID required for order of type %v", order.ErrOrderIDNotSet, o[x].Type) + } + cancelAlgoOrderParams = append(cancelAlgoOrderParams, AlgoOrderCancelParams{ + AlgoOrderID: o[x].OrderID, + InstrumentID: pairFormat.Format(ord.Pair), + }) + default: + return nil, fmt.Errorf("%w order of type %v not supported", order.ErrUnsupportedOrderType, o[x].Type) } } - var canceledOrders []OrderData - if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - canceledOrders, err = ok.WsCancelMultipleOrder(cancelOrderParams) - } else { - canceledOrders, err = ok.CancelMultipleOrders(ctx, cancelOrderParams) - } - if err != nil { - return nil, err - } resp := &order.CancelBatchResponse{Status: make(map[string]string)} - for x := range canceledOrders { - resp.Status[canceledOrders[x].OrderID] = func() string { - if canceledOrders[x].SCode != "0" && canceledOrders[x].SCode != "2" { - return "" + if len(cancelOrderParams) > 0 { + var canceledOrders []OrderData + if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { + canceledOrders, err = ok.WsCancelMultipleOrder(ctx, cancelOrderParams) + } else { + canceledOrders, err = ok.CancelMultipleOrders(ctx, cancelOrderParams) + } + if err != nil { + return nil, err + } + for x := range canceledOrders { + resp.Status[canceledOrders[x].OrderID] = func() string { + if canceledOrders[x].StatusCode != "0" && canceledOrders[x].StatusCode != "2" { + return "" + } + return order.Cancelled.String() + }() + } + } + if len(cancelAlgoOrderParams) > 0 { + cancelationResponse, err := ok.CancelAdvanceAlgoOrder(ctx, cancelAlgoOrderParams) + if err != nil { + if len(resp.Status) > 0 { + return resp, nil + } + return nil, err + } else if cancelationResponse.StatusCode != "0" { + if len(resp.Status) > 0 { + return resp, nil } - return order.Cancelled.String() - }() + return resp, fmt.Errorf("sCode: %s sMessage: %s", cancelationResponse.StatusCode, cancelationResponse.StatusMessage) + } + for x := range cancelAlgoOrderParams { + resp.Status[cancelAlgoOrderParams[x].AlgoOrderID] = order.Cancelled.String() + } } return resp, nil } @@ -935,17 +1407,29 @@ func (ok *Okx) CancelAllOrders(ctx context.Context, orderCancellation *order.Can cancelAllResponse := order.CancelAllResponse{ Status: map[string]string{}, } + + // For asset.Spread asset orders cancellation + if orderCancellation.AssetType == asset.Spread { + var success bool + success, err = ok.CancelAllSpreadOrders(ctx, orderCancellation.OrderID) + if err != nil { + return cancelAllResponse, err + } + cancelAllResponse.Status[orderCancellation.OrderID] = strconv.FormatBool(success) + return cancelAllResponse, nil + } + var instrumentType string if orderCancellation.AssetType.IsValid() { err = ok.CurrencyPairs.IsAssetEnabled(orderCancellation.AssetType) if err != nil { return order.CancelAllResponse{}, err } - instrumentType = ok.GetInstrumentTypeFromAssetItem(orderCancellation.AssetType) + instrumentType = GetInstrumentTypeFromAssetItem(orderCancellation.AssetType) } var oType string if orderCancellation.Type != order.UnknownType && orderCancellation.Type != order.AnyType { - oType, err = ok.OrderTypeString(orderCancellation.Type) + oType, err = orderTypeString(orderCancellation.Type) if err != nil { return order.CancelAllResponse{}, err } @@ -996,14 +1480,14 @@ ordersLoop: var response []OrderData if len(remaining) > 20 { if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - response, err = ok.WsCancelMultipleOrder(remaining[:20]) + response, err = ok.WsCancelMultipleOrder(ctx, remaining[:20]) } else { response, err = ok.CancelMultipleOrders(ctx, remaining[:20]) } remaining = remaining[20:] } else { if ok.Websocket.CanUseAuthenticatedWebsocketForWrapper() { - response, err = ok.WsCancelMultipleOrder(remaining) + response, err = ok.WsCancelMultipleOrder(ctx, remaining) } else { response, err = ok.CancelMultipleOrders(ctx, remaining) } @@ -1014,10 +1498,10 @@ ordersLoop: } } for y := range response { - if response[y].SCode == "0" { + if response[y].StatusCode == "0" { cancelAllResponse.Status[response[y].OrderID] = order.Cancelled.String() } else { - cancelAllResponse.Status[response[y].OrderID] = response[y].SMessage + cancelAllResponse.Status[response[y].OrderID] = response[y].StatusMessage } } } @@ -1026,19 +1510,65 @@ ordersLoop: // GetOrderInfo returns order information based on order ID func (ok *Okx) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) { + if !ok.SupportsAsset(assetType) { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) + } + if assetType == asset.Spread { + var resp *SpreadOrder + resp, err := ok.GetSpreadOrderDetails(ctx, orderID, "") + if err != nil { + return nil, err + } + oSide, err := order.StringToOrderSide(resp.Side) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(resp.OrderType) + if err != nil { + return nil, err + } + oStatus, err := order.StringToOrderStatus(resp.State) + if err != nil { + return nil, err + } + cp, err := currency.NewPairFromString(resp.InstrumentID) + if err != nil { + return nil, err + } + if !pair.IsEmpty() && !cp.Equal(pair) { + return nil, fmt.Errorf("%w, unexpected instrument ID %v for order ID %s", order.ErrOrderNotFound, pair, orderID) + } + return &order.Detail{ + Amount: resp.Size.Float64(), + Exchange: ok.Name, + OrderID: resp.OrderID, + ClientOrderID: resp.ClientOrderID, + Side: oSide, + Type: oType, + Pair: cp, + Cost: resp.Price.Float64(), + AssetType: assetType, + Status: oStatus, + Price: resp.Price.Float64(), + ExecutedAmount: resp.FillSize.Float64(), + Date: resp.CreationTime.Time(), + LastUpdated: resp.UpdateTime.Time(), + AverageExecutedPrice: resp.AveragePrice.Float64(), + RemainingAmount: resp.Size.Float64() - resp.FillSize.Float64(), + }, nil + } if pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } if err := ok.CurrencyPairs.IsAssetEnabled(assetType); err != nil { return nil, err } - pairFormat, err := ok.GetPairFormat(assetType, false) if err != nil { return nil, err } if !pair.IsPopulated() { - return nil, errIncompleteCurrencyPair + return nil, currency.ErrCurrencyPairsEmpty } instrumentID := pairFormat.Format(pair) if !ok.SupportsAsset(assetType) { @@ -1055,7 +1585,7 @@ func (ok *Okx) GetOrderInfo(ctx context.Context, orderID string, pair currency.P if err != nil { return nil, err } - orderType, err := ok.OrderTypeFromString(orderDetail.OrderType) + orderType, err := orderTypeFromString(orderDetail.OrderType) if err != nil { return nil, err } @@ -1073,14 +1603,14 @@ func (ok *Okx) GetOrderInfo(ctx context.Context, orderID string, pair currency.P Status: status, Price: orderDetail.Price.Float64(), ExecutedAmount: orderDetail.RebateAmount.Float64(), - Date: orderDetail.CreationTime, - LastUpdated: orderDetail.UpdateTime, + Date: orderDetail.CreationTime.Time(), + LastUpdated: orderDetail.UpdateTime.Time(), }, nil } // GetDepositAddress returns a deposit address for a specified currency func (ok *Okx) GetDepositAddress(ctx context.Context, c currency.Code, _, chain string) (*deposit.Address, error) { - response, err := ok.GetCurrencyDepositAddress(ctx, c.String()) + response, err := ok.GetCurrencyDepositAddress(ctx, c) if err != nil { return nil, err } @@ -1112,7 +1642,7 @@ func (ok *Okx) GetDepositAddress(ctx context.Context, c currency.Code, _, chain Chain: response[x].Chain, }, nil } - return nil, errDepositAddressNotFound + return nil, deposit.ErrAddressNotFound } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is submitted @@ -1123,7 +1653,7 @@ func (ok *Okx) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest input := WithdrawalInput{ ChainName: withdrawRequest.Crypto.Chain, Amount: withdrawRequest.Amount, - Currency: withdrawRequest.Currency.String(), + Currency: withdrawRequest.Currency, ToAddress: withdrawRequest.Crypto.Address, TransactionFee: withdrawRequest.Crypto.FeeAmount, WithdrawalDestination: "3", @@ -1160,16 +1690,72 @@ func (ok *Okx) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest if !ok.SupportsAsset(req.AssetType) { return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, req.AssetType) } - instrumentType := ok.GetInstrumentTypeFromAssetItem(req.AssetType) + + var resp []order.Detail + var format currency.PairFormat + if req.AssetType == asset.Spread { + var spreads []SpreadOrder + spreads, err = ok.GetActiveSpreadOrders(ctx, "", req.Type.String(), "", req.FromOrderID, "", 0) + if err != nil { + return nil, err + } + for x := range spreads { + format, err = ok.GetPairFormat(asset.Spread, true) + if err != nil { + return nil, err + } + var ( + pair currency.Pair + oType order.Type + oSide order.Side + oStatus order.Status + ) + + pair, err = currency.NewPairDelimiter(spreads[x].SpreadID, format.Delimiter) + if err != nil { + return nil, err + } + oType, err = order.StringToOrderType(spreads[x].OrderType) + if err != nil { + return nil, err + } + oSide, err = order.StringToOrderSide(spreads[x].Side) + if err != nil { + return nil, err + } + oStatus, err = order.StringToOrderStatus(spreads[x].State) + if err != nil { + return nil, err + } + resp = append(resp, order.Detail{ + Amount: spreads[x].Size.Float64(), + Pair: pair, + Price: spreads[x].Price.Float64(), + ExecutedAmount: spreads[x].FillSize.Float64(), + RemainingAmount: spreads[x].Size.Float64() - spreads[x].FillSize.Float64(), + Exchange: ok.Name, + OrderID: spreads[x].OrderID, + ClientOrderID: spreads[x].ClientOrderID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: req.AssetType, + Date: spreads[x].CreationTime.Time(), + LastUpdated: spreads[x].UpdateTime.Time(), + }) + } + return req.Filter(ok.Name, resp), nil + } + + instrumentType := GetInstrumentTypeFromAssetItem(req.AssetType) var orderType string if req.Type != order.UnknownType && req.Type != order.AnyType { - orderType, err = ok.OrderTypeString(req.Type) + orderType, err = orderTypeString(req.Type) if err != nil { return nil, err } } endTime := req.EndTime - var resp []order.Detail allOrders: for { requestParam := &OrderListRequestParams{ @@ -1186,9 +1772,9 @@ allOrders: break } for i := range orderList { - if req.StartTime.Equal(orderList[i].CreationTime) || - orderList[i].CreationTime.Before(req.StartTime) || - endTime == orderList[i].CreationTime { + if req.StartTime.Equal(orderList[i].CreationTime.Time()) || + orderList[i].CreationTime.Time().Before(req.StartTime) || + endTime == orderList[i].CreationTime.Time() { // reached end of orders to crawl break allOrders } @@ -1214,7 +1800,7 @@ allOrders: return nil, err } var oType order.Type - oType, err = ok.OrderTypeFromString(orderList[i].OrderType) + oType, err = orderTypeFromString(orderList[i].OrderType) if err != nil { return nil, err } @@ -1233,8 +1819,8 @@ allOrders: Side: orderSide, Status: orderStatus, AssetType: req.AssetType, - Date: orderList[i].CreationTime, - LastUpdated: orderList[i].UpdateTime, + Date: orderList[i].CreationTime.Time(), + LastUpdated: orderList[i].UpdateTime.Time(), }) } if len(orderList) < 100 { @@ -1243,7 +1829,7 @@ allOrders: // If not, break out of the loop to not send another request. break } - endTime = orderList[len(orderList)-1].CreationTime + endTime = orderList[len(orderList)-1].CreationTime.Time() } return req.Filter(ok.Name, resp), nil } @@ -1256,15 +1842,69 @@ func (ok *Okx) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest if !req.StartTime.IsZero() && req.StartTime.Before(time.Now().Add(-kline.ThreeMonth.Duration())) { return nil, errOnlyThreeMonthsSupported } - if len(req.Pairs) == 0 { - return nil, errMissingAtLeast1CurrencyPair - } if !ok.SupportsAsset(req.AssetType) { return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, req.AssetType) } - instrumentType := ok.GetInstrumentTypeFromAssetItem(req.AssetType) - endTime := req.EndTime var resp []order.Detail + // For Spread orders. + if req.AssetType == asset.Spread { + oType, err := orderTypeString(req.Type) + if err != nil { + return nil, err + } + spreadOrders, err := ok.GetCompletedSpreadOrdersLast7Days(ctx, "", oType, "", req.FromOrderID, "", req.StartTime, req.EndTime, 0) + if err != nil { + return nil, err + } + for x := range spreadOrders { + var format currency.PairFormat + format, err = ok.GetPairFormat(asset.Spread, true) + if err != nil { + return nil, err + } + var pair currency.Pair + pair, err = currency.NewPairDelimiter(spreadOrders[x].SpreadID, format.Delimiter) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(spreadOrders[x].OrderType) + if err != nil { + return nil, err + } + oSide, err := order.StringToOrderSide(spreadOrders[x].Side) + if err != nil { + return nil, err + } + oStatus, err := order.StringToOrderStatus(spreadOrders[x].State) + if err != nil { + return nil, err + } + resp = append(resp, order.Detail{ + Price: spreadOrders[x].Price.Float64(), + AverageExecutedPrice: spreadOrders[x].AveragePrice.Float64(), + Amount: spreadOrders[x].Size.Float64(), + ExecutedAmount: spreadOrders[x].FillSize.Float64(), + RemainingAmount: spreadOrders[x].PendingFillSize.Float64(), + Exchange: ok.Name, + OrderID: spreadOrders[x].OrderID, + ClientOrderID: spreadOrders[x].ClientOrderID, + Type: oType, + Side: oSide, + Status: oStatus, + AssetType: req.AssetType, + Date: spreadOrders[x].CreationTime.Time(), + LastUpdated: spreadOrders[x].UpdateTime.Time(), + Pair: pair, + }) + } + return req.Filter(ok.Name, resp), nil + } + + if len(req.Pairs) == 0 { + return nil, currency.ErrCurrencyPairsEmpty + } + instrumentType := GetInstrumentTypeFromAssetItem(req.AssetType) + endTime := req.EndTime allOrders: for { orderList, err := ok.Get3MonthOrderHistory(ctx, &OrderHistoryRequestParams{ @@ -1280,9 +1920,9 @@ allOrders: break } for i := range orderList { - if req.StartTime.Equal(orderList[i].CreationTime) || - orderList[i].CreationTime.Before(req.StartTime) || - endTime == orderList[i].CreationTime { + if req.StartTime.Equal(orderList[i].CreationTime.Time()) || + orderList[i].CreationTime.Time().Before(req.StartTime) || + endTime == orderList[i].CreationTime.Time() { // reached end of orders to crawl break allOrders } @@ -1304,7 +1944,7 @@ allOrders: } orderSide := orderList[i].Side var oType order.Type - oType, err = ok.OrderTypeFromString(orderList[i].OrderType) + oType, err = orderTypeFromString(orderList[i].OrderType) if err != nil { return nil, err } @@ -1333,8 +1973,8 @@ allOrders: Side: orderSide, Status: orderStatus, AssetType: req.AssetType, - Date: orderList[i].CreationTime, - LastUpdated: orderList[i].UpdateTime, + Date: orderList[i].CreationTime.Time(), + LastUpdated: orderList[i].UpdateTime.Time(), Pair: pair, Cost: orderList[i].AveragePrice.Float64() * orderList[i].AccumulatedFillSize.Float64(), CostAsset: currency.NewCode(orderList[i].RebateCurrency), @@ -1344,7 +1984,7 @@ allOrders: if len(orderList) < 100 { break } - endTime = orderList[len(orderList)-1].CreationTime + endTime = orderList[len(orderList)-1].CreationTime.Time() } return req.Filter(ok.Name, resp), nil } @@ -1388,12 +2028,12 @@ func (ok *Okx) GetHistoricCandles(ctx context.Context, pair currency.Pair, a ass timeSeries := make([]kline.Candle, len(candles)) for x := range candles { timeSeries[x] = kline.Candle{ - Time: candles[x].OpenTime, - Open: candles[x].OpenPrice, - High: candles[x].HighestPrice, - Low: candles[x].LowestPrice, - Close: candles[x].ClosePrice, - Volume: candles[x].Volume, + Time: candles[x].OpenTime.Time(), + Open: candles[x].OpenPrice.Float64(), + High: candles[x].HighestPrice.Float64(), + Low: candles[x].LowestPrice.Float64(), + Close: candles[x].ClosePrice.Float64(), + Volume: candles[x].Volume.Float64(), } } return req.ProcessResponse(timeSeries) @@ -1429,12 +2069,12 @@ func (ok *Okx) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pai } for x := range candles { timeSeries = append(timeSeries, kline.Candle{ - Time: candles[x].OpenTime, - Open: candles[x].OpenPrice, - High: candles[x].HighestPrice, - Low: candles[x].LowestPrice, - Close: candles[x].ClosePrice, - Volume: candles[x].Volume, + Time: candles[x].OpenTime.Time(), + Open: candles[x].OpenPrice.Float64(), + High: candles[x].HighestPrice.Float64(), + Low: candles[x].LowestPrice.Float64(), + Close: candles[x].ClosePrice.Float64(), + Volume: candles[x].Volume.Float64(), }) } } @@ -1444,7 +2084,7 @@ func (ok *Okx) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pai // GetAvailableTransferChains returns the available transfer blockchains for the specific // cryptocurrency func (ok *Okx) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) { - currencyChains, err := ok.GetFundingCurrencies(ctx) + currencyChains, err := ok.GetFundingCurrencies(ctx, cryptocurrency) if err != nil { return nil, err } @@ -1466,7 +2106,7 @@ func (ok *Okx) GetAvailableTransferChains(ctx context.Context, cryptocurrency cu // getInstrumentsForOptions returns the instruments for options asset type func (ok *Okx) getInstrumentsForOptions(ctx context.Context) ([]Instrument, error) { - underlyings, err := ok.GetPublicUnderlyings(context.Background(), okxInstTypeOption) + underlyings, err := ok.GetPublicUnderlyings(context.Background(), instTypeOption) if err != nil { return nil, err } @@ -1474,7 +2114,7 @@ func (ok *Okx) getInstrumentsForOptions(ctx context.Context) ([]Instrument, erro for x := range underlyings { var instruments []Instrument instruments, err = ok.GetInstruments(ctx, &InstrumentsFetchParams{ - InstrumentType: okxInstTypeOption, + InstrumentType: instTypeOption, Underlying: underlyings[x], }) if err != nil { @@ -1491,23 +2131,39 @@ func (ok *Okx) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Inst return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a) } + var instruments []Instrument var instType string + var err error switch a { case asset.Options: - return ok.getInstrumentsForOptions(ctx) + instruments, err = ok.getInstrumentsForOptions(ctx) + if err != nil { + return nil, err + } + ok.instrumentsInfoMapLock.Lock() + ok.instrumentsInfoMap[instTypeOption] = instruments + ok.instrumentsInfoMapLock.Unlock() + return instruments, nil case asset.Spot: - instType = okxInstTypeSpot + instType = instTypeSpot case asset.Futures: - instType = okxInstTypeFutures + instType = instTypeFutures case asset.PerpetualSwap: - instType = okxInstTypeSwap + instType = instTypeSwap case asset.Margin: - instType = okxInstTypeMargin + instType = instTypeMargin } - return ok.GetInstruments(ctx, &InstrumentsFetchParams{ + instruments, err = ok.GetInstruments(ctx, &InstrumentsFetchParams{ InstrumentType: instType, }) + if err != nil { + return nil, err + } + ok.instrumentsInfoMapLock.Lock() + ok.instrumentsInfoMap[instType] = instruments + ok.instrumentsInfoMapLock.Unlock() + return instruments, nil } // GetLatestFundingRates returns the latest funding rates data @@ -1609,7 +2265,7 @@ func (ok *Okx) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.His } pairRate.FundingRates = append(pairRate.FundingRates, fundingrate.Rate{ Time: frh[i].FundingTime.Time(), - Rate: frh[i].RealisedRate.Decimal(), + Rate: frh[i].FundingRate.Decimal(), }) } if len(frh) < requestLimit { @@ -1659,8 +2315,8 @@ func (ok *Okx) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.His } var billDetails []BillsDetailResponse billDetails, err = billDetailsFunc(ctx, &BillsDetailQueryParameter{ - InstrumentType: ok.GetInstrumentTypeFromAssetItem(r.Asset), - Currency: pairRate.PaymentCurrency.String(), + InstrumentType: GetInstrumentTypeFromAssetItem(r.Asset), + Currency: pairRate.PaymentCurrency, BillType: 137, BeginTime: sd, EndTime: r.EndDate, @@ -1714,16 +2370,16 @@ func (ok *Okx) GetCollateralMode(ctx context.Context, item asset.Item) (collater return 0, err } switch cfg[0].AccountLevel { - case 1: + case "1": if item != asset.Spot { return 0, fmt.Errorf("%w %v", asset.ErrNotSupported, item) } fallthrough - case 2: - return collateral.SingleMode, nil - case 3: + case "2": + return collateral.SpotFuturesMode, nil + case "3": return collateral.MultiMode, nil - case 4: + case "4": return collateral.PortfolioMode, nil default: return collateral.UnknownMode, fmt.Errorf("%w %v", order.ErrCollateralInvalid, cfg[0].AccountLevel) @@ -1762,10 +2418,10 @@ func (ok *Okx) ChangePositionMargin(ctx context.Context, req *margin.PositionCha req.MarginSide = "net" } r := &IncreaseDecreaseMarginInput{ - InstrumentID: fPair.String(), - PositionSide: req.MarginSide, - Type: marginType, - Amount: amt, + InstrumentID: fPair.String(), + PositionSide: req.MarginSide, + MarginBalanceType: marginType, + Amount: amt, } if req.Asset == asset.Margin { @@ -1776,7 +2432,6 @@ func (ok *Okx) ChangePositionMargin(ctx context.Context, req *margin.PositionCha if err != nil { return nil, err } - return &margin.PositionChangeResponse{ Exchange: ok.Name, Pair: req.Pair, @@ -1801,7 +2456,7 @@ func (ok *Okx) GetFuturesPositionSummary(ctx context.Context, req *futures.Posit if err != nil { return nil, err } - instrumentType := ok.GetInstrumentTypeFromAssetItem(req.Asset) + instrumentType := GetInstrumentTypeFromAssetItem(req.Asset) var contracts []futures.Contract contracts, err = ok.GetFuturesContractDetails(ctx, req.Asset) @@ -1835,11 +2490,11 @@ func (ok *Okx) GetFuturesPositionSummary(ctx context.Context, req *futures.Posit return nil, fmt.Errorf("%w, received '%v', no positions found", errOnlyOneResponseExpected, len(positionSummaries)) } marginMode := margin.Isolated - if positionSummary.MarginMode == "cross" { + if positionSummary.MarginMode == TradeModeCross { marginMode = margin.Multi } - acc, err := ok.AccountBalance(ctx, "") + acc, err := ok.AccountBalance(ctx, currency.EMPTYCODE) if err != nil { return nil, err } @@ -1948,7 +2603,7 @@ func (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *futures.Positi if err != nil { return nil, err } - instrumentType := ok.GetInstrumentTypeFromAssetItem(req.Asset) + instrumentType := GetInstrumentTypeFromAssetItem(req.Asset) multiplier := 1.0 var contractSettlementType futures.ContractSettlementType @@ -1997,7 +2652,7 @@ func (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *futures.Positi } orderSide := positions[j].Side var oType order.Type - oType, err = ok.OrderTypeFromString(positions[j].OrderType) + oType, err = orderTypeFromString(positions[j].OrderType) if err != nil { return nil, err } @@ -2031,8 +2686,8 @@ func (ok *Okx) GetFuturesPositionOrders(ctx context.Context, req *futures.Positi Side: orderSide, Status: orderStatus, AssetType: req.Asset, - Date: positions[j].CreationTime, - LastUpdated: positions[j].UpdateTime, + Date: positions[j].CreationTime.Time(), + LastUpdated: positions[j].UpdateTime.Time(), Pair: req.Pairs[i], Cost: cost, CostAsset: currency.NewCode(positions[j].RebateCurrency), @@ -2050,13 +2705,13 @@ func (ok *Okx) SetLeverage(ctx context.Context, item asset.Item, pair currency.P if marginType == margin.Isolated { switch { case orderSide == order.UnknownSide: - return errOrderSideRequired + return order.ErrSideIsInvalid case orderSide.IsLong(): posSide = "long" case orderSide.IsShort(): posSide = "short" default: - return fmt.Errorf("%w %v requires long/short", errInvalidOrderSide, orderSide) + return fmt.Errorf("%w %v requires long/short", order.ErrSideIsInvalid, orderSide) } } fallthrough @@ -2067,7 +2722,7 @@ func (ok *Okx) SetLeverage(ctx context.Context, item asset.Item, pair currency.P } marginMode := ok.marginTypeToString(marginType) - _, err = ok.SetLeverageRate(ctx, SetLeverageInput{ + _, err = ok.SetLeverageRate(ctx, &SetLeverageInput{ Leverage: amount, MarginMode: marginMode, InstrumentID: instrumentID, @@ -2087,11 +2742,11 @@ func (ok *Okx) GetLeverage(ctx context.Context, item asset.Item, pair currency.P if marginType == margin.Isolated { switch { case orderSide == order.UnknownSide: - return 0, errOrderSideRequired + return 0, order.ErrSideIsInvalid case orderSide.IsLong(), orderSide.IsShort(): inspectLeverage = true default: - return 0, fmt.Errorf("%w %v requires long/short", errInvalidOrderSide, orderSide) + return 0, fmt.Errorf("%w '%v', requires long/short", order.ErrSideIsInvalid, orderSide) } } fallthrough @@ -2101,7 +2756,7 @@ func (ok *Okx) GetLeverage(ctx context.Context, item asset.Item, pair currency.P return -1, err } marginMode := ok.marginTypeToString(marginType) - lev, err := ok.GetLeverageRate(ctx, instrumentID, marginMode) + lev, err := ok.GetLeverageRate(ctx, instrumentID, marginMode, currency.EMPTYCODE) if err != nil { return -1, err } @@ -2128,67 +2783,101 @@ func (ok *Okx) GetFuturesContractDetails(ctx context.Context, item asset.Item) ( if !item.IsFutures() { return nil, futures.ErrNotFuturesAsset } - if !ok.SupportsAsset(item) || item == asset.Options { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) - } - instType := ok.GetInstrumentTypeFromAssetItem(item) - result, err := ok.GetInstruments(ctx, &InstrumentsFetchParams{ - InstrumentType: instType, - }) - if err != nil { - return nil, err - } - resp := make([]futures.Contract, len(result)) - for i := range result { - var cp, underlying currency.Pair - underlying, err = currency.NewPairFromString(result[i].Underlying) + switch item { + case asset.Futures, asset.PerpetualSwap: + instType := GetInstrumentTypeFromAssetItem(item) + result, err := ok.GetInstruments(ctx, &InstrumentsFetchParams{ + InstrumentType: instType, + }) if err != nil { return nil, err } - cp, err = currency.NewPairFromString(result[i].InstrumentID) + resp := make([]futures.Contract, len(result)) + for i := range result { + var cp, underlying currency.Pair + underlying, err = currency.NewPairFromString(result[i].Underlying) + if err != nil { + return nil, err + } + cp, err = currency.NewPairFromString(result[i].InstrumentID) + if err != nil { + return nil, err + } + settleCurr := currency.NewCode(result[i].SettlementCurrency) + var ct futures.ContractType + if item == asset.PerpetualSwap { + ct = futures.Perpetual + } else { + switch result[i].Alias { + case "this_week", "next_week": + ct = futures.Weekly + case "quarter", "next_quarter": + ct = futures.Quarterly + } + } + contractSettlementType := futures.Linear + if result[i].SettlementCurrency == result[i].BaseCurrency { + contractSettlementType = futures.Inverse + } + resp[i] = futures.Contract{ + Exchange: ok.Name, + Name: cp, + Underlying: underlying, + Asset: item, + StartDate: result[i].ListTime.Time(), + EndDate: result[i].ExpTime.Time(), + IsActive: result[i].State == "live", + Status: result[i].State, + Type: ct, + SettlementType: contractSettlementType, + SettlementCurrencies: currency.Currencies{settleCurr}, + MarginCurrency: settleCurr, + Multiplier: result[i].ContractValue.Float64(), + MaxLeverage: result[i].MaxLeverage.Float64(), + } + } + return resp, nil + case asset.Spread: + results, err := ok.GetPublicSpreads(ctx, "", "", "", "") if err != nil { return nil, err } - settleCurr := currency.NewCode(result[i].SettlementCurrency) - var ct futures.ContractType - if item == asset.PerpetualSwap { - ct = futures.Perpetual - } else { - switch result[i].Alias { - case "this_week", "next_week": - ct = futures.Weekly - case "quarter", "next_quarter": - ct = futures.Quarterly + resp := make([]futures.Contract, len(results)) + for s := range results { + var cp currency.Pair + cp, err = currency.NewPairFromString(results[s].SpreadID) + if err != nil { + return nil, err + } + contractSettlementType, err := futures.StringToContractSettlementType(results[s].SpreadType) + if err != nil { + return nil, err + } + resp[s] = futures.Contract{ + Exchange: ok.Name, + Name: cp, + Asset: asset.Spread, + StartDate: results[s].ListTime.Time(), + EndDate: results[s].ExpTime.Time(), + IsActive: results[s].State == "live", + Status: results[s].State, + Type: futures.LongDated, + SettlementType: contractSettlementType, + MarginCurrency: currency.NewCode(results[s].QuoteCurrency), } } - contractSettlementType := futures.Linear - if result[i].SettlementCurrency == result[i].BaseCurrency { - contractSettlementType = futures.Inverse - } - resp[i] = futures.Contract{ - Exchange: ok.Name, - Name: cp, - Underlying: underlying, - Asset: item, - StartDate: result[i].ListTime.Time, - EndDate: result[i].ExpTime.Time, - IsActive: result[i].State == "live", - Status: result[i].State, - Type: ct, - SettlementType: contractSettlementType, - SettlementCurrencies: currency.Currencies{settleCurr}, - MarginCurrency: settleCurr, - Multiplier: result[i].ContractValue.Float64(), - MaxLeverage: result[i].MaxLeverage.Float64(), - } + return resp, nil + default: + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) } - return resp, nil } // GetOpenInterest returns the open interest rate for a given asset pair func (ok *Okx) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) { for i := range k { - if k[i].Asset != asset.Futures && k[i].Asset != asset.PerpetualSwap { + switch k[i].Asset { + case asset.Futures, asset.PerpetualSwap, asset.Options: + default: // avoid API calls or returning errors after a successful retrieval return nil, fmt.Errorf("%w %v %v", asset.ErrNotSupported, k[i].Asset, k[i].Pair()) } @@ -2197,16 +2886,39 @@ func (ok *Okx) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futur var resp []futures.OpenInterest // TODO: Options support instTypes := map[string]asset.Item{ - "SWAP": asset.PerpetualSwap, - "FUTURES": asset.Futures, + instTypeSwap: asset.PerpetualSwap, + instTypeFutures: asset.Futures, + instTypeOption: asset.Options, } for instType, v := range instTypes { - oid, err := ok.GetOpenInterestData(ctx, instType, "", "") - if err != nil { - return nil, err + var oid []OpenInterest + var err error + switch instType { + case instTypeOption: + var underlyings []string + underlyings, err = ok.GetPublicUnderlyings(context.Background(), instTypeOption) + if err != nil { + return nil, err + } + for u := range underlyings { + var incOID []OpenInterest + incOID, err = ok.GetOpenInterestData(ctx, instType, underlyings[u], "", "") + if err != nil { + return nil, err + } + oid = append(oid, incOID...) + } + case instTypeSwap, + instTypeFutures: + oid, err = ok.GetOpenInterestData(ctx, instType, "", "", "") + if err != nil { + return nil, err + } } for j := range oid { - p, isEnabled, err := ok.MatchSymbolCheckEnabled(oid[j].InstrumentID, v, true) + var isEnabled bool + var p currency.Pair + p, isEnabled, err = ok.MatchSymbolCheckEnabled(oid[j].InstrumentID, v, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return nil, err } @@ -2245,9 +2957,27 @@ func (ok *Okx) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futur if err != nil { return nil, err } - oid, err := ok.GetOpenInterestData(ctx, instTypes[k[0].Asset], "", pFmt) - if err != nil { - return nil, err + var oid []OpenInterest + switch instTypes[k[0].Asset] { + case instTypeOption: + var underlyings []string + underlyings, err = ok.GetPublicUnderlyings(context.Background(), instTypeOption) + if err != nil { + return nil, err + } + for u := range underlyings { + var incOID []OpenInterest + incOID, err = ok.GetOpenInterestData(ctx, instTypes[k[0].Asset], underlyings[u], "", "") + if err != nil { + return nil, err + } + oid = append(oid, incOID...) + } + case instTypeSwap, instTypeFutures: + oid, err = ok.GetOpenInterestData(ctx, instTypes[k[0].Asset], "", "", pFmt) + if err != nil { + return nil, err + } } for i := range oid { p, isEnabled, err := ok.MatchSymbolCheckEnabled(oid[i].InstrumentID, k[0].Asset, true) @@ -2279,20 +3009,22 @@ func (ok *Okx) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp currenc cp.Delimiter = currency.DashDelimiter switch a { case asset.Spot: - return baseURL + tradeSpot + cp.Lower().String(), nil + return baseURL + "trade-spot/" + cp.Lower().String(), nil case asset.Margin: - return baseURL + tradeMargin + cp.Lower().String(), nil + return baseURL + "trade-margin/" + cp.Lower().String(), nil case asset.PerpetualSwap: - return baseURL + tradePerps + cp.Lower().String(), nil + return baseURL + "trade-swap/" + cp.Lower().String(), nil case asset.Options: - return baseURL + tradeOptions + cp.Base.Lower().String() + "-usd", nil + return baseURL + "trade-option/" + cp.Base.Lower().String() + "-usd", nil + case asset.Spread: + return baseURL, nil case asset.Futures: cp, err = ok.FormatExchangeCurrency(cp, a) if err != nil { return "", err } insts, err := ok.GetInstruments(ctx, &InstrumentsFetchParams{ - InstrumentType: okxInstTypeFutures, + InstrumentType: instTypeFutures, InstrumentID: cp.String(), }) if err != nil { @@ -2316,8 +3048,33 @@ func (ok *Okx) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp currenc case "next_quarter": ct = "-biquarterly" } - return baseURL + tradeFutures + strings.ToLower(insts[0].Underlying) + ct, nil + return baseURL + "trade-futures/" + strings.ToLower(insts[0].Underlying) + ct, nil default: return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) } } + +func (ok *Okx) underlyingFromInstID(instrumentType, instID string) (string, error) { + ok.instrumentsInfoMapLock.Lock() + defer ok.instrumentsInfoMapLock.Unlock() + if instrumentType != "" { + insts, okay := ok.instrumentsInfoMap[instrumentType] + if !okay { + return "", errInvalidInstrumentType + } + for a := range insts { + if insts[a].InstrumentID == instID { + return insts[a].Underlying, nil + } + } + } else { + for _, insts := range ok.instrumentsInfoMap { + for a := range insts { + if insts[a].InstrumentID == instID { + return insts[a].Underlying, nil + } + } + } + } + return "", fmt.Errorf("underlying not found for instrument %s", instID) +} diff --git a/exchanges/okx/ratelimit.go b/exchanges/okx/ratelimit.go index ca0ec47133b..e81fafea36a 100644 --- a/exchanges/okx/ratelimit.go +++ b/exchanges/okx/ratelimit.go @@ -16,198 +16,12 @@ const ( ) const ( - // Trade Endpoints - placeOrderRate = 60 - placeMultipleOrdersRate = 300 - cancelOrderRate = 60 - cancelMultipleOrdersRate = 300 - amendOrderRate = 60 - amendMultipleOrdersRate = 300 - closePositionsRate = 20 - getOrderDetails = 60 - getOrderListRate = 60 - getOrderHistory7DaysRate = 40 - getOrderHistory3MonthsRate = 20 - getTransactionDetail3DaysRate = 60 - getTransactionDetail3MonthsRate = 10 - placeAlgoOrderRate = 20 - cancelAlgoOrderRate = 20 - cancelAdvanceAlgoOrderRate = 20 - getAlgoOrderListRate = 20 - getAlgoOrderHistoryRate = 20 - getEasyConvertCurrencyListRate = 1 - placeEasyConvert = 1 - getEasyConvertHistory = 1 - oneClickRepayCurrencyList = 1 - tradeOneClickRepay = 1 - getOneClickRepayHistory = 1 - - // Block Trading endpoints - getCounterpartiesRate = 5 - createRfqRate = 5 - cancelRfqRate = 5 - cancelMultipleRfqRate = 2 - cancelAllRfqsRate = 2 - executeQuoteRate = 2 - setQuoteProducts = 5 - restMMPStatus = 5 - createQuoteRate = 50 - cancelQuoteRate = 50 - cancelMultipleQuotesRate = 2 - cancelAllQuotes = 2 - getRfqsRate = 2 - getQuotesRate = 2 - getTradesRate = 5 - getTradesHistoryRate = 10 - getPublicTradesRate = 5 - - // Funding - getCurrenciesRate = 6 - getBalanceRate = 6 - getAccountAssetValuationRate = 1 - fundsTransferRate = 1 - getFundsTransferStateRate = 1 - assetBillsDetailsRate = 6 - lightningDepositsRate = 2 - getDepositAddressRate = 6 - getDepositHistoryRate = 6 - withdrawalRate = 6 - lightningWithdrawalsRate = 2 - cancelWithdrawalRate = 6 - getWithdrawalHistoryRate = 6 - smallAssetsConvertRate = 1 - - // Savings - getSavingBalanceRate = 6 - savingsPurchaseRedemptionRate = 6 - setLendingRateRate = 6 - getLendingHistoryRate = 6 - getPublicBorrowInfoRate = 6 - getPublicBorrowHistoryRate = 6 - - // Convert - getConvertCurrenciesRate = 6 - getConvertCurrencyPairRate = 6 - estimateQuoteRate = 10 - convertTradeRate = 10 - getConvertHistoryRate = 6 - - // Account - getAccountBalanceRate = 10 - getPositionsRate = 10 - getPositionsHistoryRate = 1 - getAccountAndPositionRiskRate = 10 - getBillsDetailsRate = 6 - getAccountConfigurationRate = 5 - setPositionModeRate = 5 - setLeverageRate = 20 - getMaximumBuyOrSellAmountRate = 20 - getMaximumAvailableTradableAmountRate = 20 - increaseOrDecreaseMarginRate = 20 - getLeverageRate = 20 - getTheMaximumLoanOfInstrumentRate = 20 - getFeeRatesRate = 5 - getInterestAccruedDataRate = 5 - getInterestRateRate = 5 - setGreeksRate = 5 - isolatedMarginTradingSettingsRate = 5 - getMaximumWithdrawalsRate = 20 - getAccountRiskStateRate = 10 - vipLoansBorrowAndRepayRate = 6 - getBorrowAnsRepayHistoryHistoryRate = 5 - getBorrowInterestAndLimitRate = 5 - positionBuilderRate = 2 - getGreeksRate = 10 - getPMLimitation = 10 - - // Sub Account Endpoints - viewSubaccountListRate = 2 - resetSubAccountAPIKey = 1 - getSubaccountTradingBalanceRate = 2 - getSubaccountFundingBalanceRate = 2 - historyOfSubaccountTransferRate = 6 - masterAccountsManageTransfersBetweenSubaccountRate = 1 - setPermissionOfTransferOutRate = 1 - getCustodyTradingSubaccountListRate = 1 - gridTradingRate = 20 - amendGridAlgoOrderRate = 20 - stopGridAlgoOrderRate = 20 - getGridAlgoOrderListRate = 20 - getGridAlgoOrderHistoryRate = 20 - getGridAlgoOrderDetailsRate = 20 - getGridAlgoSubOrdersRate = 20 - getGridAlgoOrderPositionsRate = 20 - spotGridWithdrawIncomeRate = 20 - computeMarginBalance = 20 - adjustMarginBalance = 20 - getGridAIParameter = 20 - - // Earn - getOffer = 3 - purchase = 2 - redeem = 2 - cancelPurchaseOrRedemption = 2 - getEarnActiveOrders = 3 - getFundingOrderHistory = 3 - - // Market Data - getTickersRate = 20 - getIndexTickersRate = 20 - getOrderBookRate = 20 - getCandlesticksRate = 40 - getCandlesticksHistoryRate = 20 - getIndexCandlesticksRate = 20 - getMarkPriceCandlesticksRate = 20 - getTradesRequestRate = 100 - get24HTotalVolumeRate = 2 - getOracleRate = 1 - getExchangeRateRequestRate = 1 - getIndexComponentsRate = 20 - getBlockTickersRate = 20 - getBlockTradesRate = 20 - - // Public Data Endpoints - getInstrumentsRate = 20 - getDeliveryExerciseHistoryRate = 40 - getOpenInterestRate = 20 - getFundingRate = 20 - getFundingRateHistoryRate = 20 - getLimitPriceRate = 20 - getOptionMarketDateRate = 20 - getEstimatedDeliveryExercisePriceRate = 10 - getDiscountRateAndInterestFreeQuotaRate = 2 - getSystemTimeRate = 10 - getLiquidationOrdersRate = 40 - getMarkPriceRate = 10 - getPositionTiersRate = 10 - getInterestRateAndLoanQuotaRate = 2 - getInterestRateAndLoanQuoteForVIPLoansRate = 2 - getUnderlyingRate = 20 - getInsuranceFundRate = 10 - unitConvertRate = 10 - - // Trading Data Endpoints - getSupportCoinRate = 5 - getTakerVolumeRate = 5 - getMarginLendingRatioRate = 5 - getLongShortRatioRate = 5 - getContractsOpenInterestAndVolumeRate = 5 - getOptionsOpenInterestAndVolumeRate = 5 - getPutCallRatioRate = 5 - getOpenInterestAndVolumeRate = 5 - getTakerFlowRate = 5 - - // Status Endpoints - getEventStatusRate = 1 -) - -const ( - placeOrderEPL request.EndpointLimit = iota + 1 // This endpoint limit is shared with `Place order` Websocket API endpoints - placeMultipleOrdersEPL // This endpoint limit is shared with `Place multiple orders` Websocket API endpoints - cancelOrderEPL // This endpoint limit is shared with `Cancel order` Websocket API endpoints - cancelMultipleOrdersEPL // This endpoint limit is shared with `Cancel multiple orders` Websocket API endpoints - amendOrderEPL // This endpoint limit is shared with `Amend order` Websocket API endpoints - amendMultipleOrdersEPL // This endpoint limit is shared with `Amend multiple orders` Websocket API endpoints + placeOrderEPL request.EndpointLimit = iota + placeMultipleOrdersEPL + cancelOrderEPL + cancelMultipleOrdersEPL + amendOrderEPL + amendMultipleOrdersEPL closePositionEPL getOrderDetEPL getOrderListEPL @@ -215,9 +29,16 @@ const ( getOrderHistory3MonthsEPL getTransactionDetail3DaysEPL getTransactionDetail3MonthsEPL + setTransactionDetail2YearIntervalEPL + getTransactionDetailLast2YearsEPL + cancelAllAfterCountdownEPL + getTradeAccountRateLimitEPL + orderPreCheckEPL placeAlgoOrderEPL cancelAlgoOrderEPL + amendAlgoOrderEPL cancelAdvanceAlgoOrderEPL + getAlgoOrderDetailEPL getAlgoOrderListEPL getAlgoOrderHistoryEPL getEasyConvertCurrencyListEPL @@ -226,25 +47,32 @@ const ( getOneClickRepayHistoryEPL oneClickRepayCurrencyListEPL tradeOneClickRepayEPL + massCancemMMPOrderEPL getCounterpartiesEPL - createRfqEPL - cancelRfqEPL - cancelMultipleRfqEPL - cancelAllRfqsEPL + createRFQEPL + cancelRFQEPL + cancelMultipleRFQEPL + cancelAllRFQsEPL executeQuoteEPL + getQuoteProductsEPL setQuoteProductsEPL - restMMPStatusEPL + resetRFQMMPEPL + setMMPEPL + getMMPConfigEPL createQuoteEPL cancelQuoteEPL cancelMultipleQuotesEPL cancelAllQuotesEPL - getRfqsEPL + getRFQsEPL getQuotesEPL getTradesEPL getTradesHistoryEPL + optionInstrumentTradeFamilyEPL + optionTradesEPL getPublicTradesEPL getCurrenciesEPL getBalanceEPL + getNonTradableAssetsEPL getAccountAssetValuationEPL fundsTransferEPL getFundsTransferStateEPL @@ -256,7 +84,9 @@ const ( lightningWithdrawalsEPL cancelWithdrawalEPL getWithdrawalHistoryEPL + getDepositWithdrawalStatusEPL smallAssetsConvertEPL + getPublicExchangeListEPL getSavingBalanceEPL savingsPurchaseRedemptionEPL setLendingRateEPL @@ -264,6 +94,8 @@ const ( getPublicBorrowInfoEPL getPublicBorrowHistoryEPL getConvertCurrenciesEPL + getMonthlyStatementEPL + applyForMonthlyStatementEPL getConvertCurrencyPairEPL estimateQuoteEPL convertTradeEPL @@ -273,6 +105,9 @@ const ( getPositionsHistoryEPL getAccountAndPositionRiskEPL getBillsDetailsEPL + getBillsDetailArchiveEPL + billHistoryArchiveEPL + getBillHistoryArchiveEPL getAccountConfigurationEPL setPositionModeEPL setLeverageEPL @@ -280,6 +115,7 @@ const ( getMaximumAvailableTradableAmountEPL increaseOrDecreaseMarginEPL getLeverageEPL + getLeverateEstimatedInfoEPL getTheMaximumLoanOfInstrumentEPL getFeeRatesEPL getInterestAccruedDataEPL @@ -288,23 +124,55 @@ const ( isolatedMarginTradingSettingsEPL getMaximumWithdrawalsEPL getAccountRiskStateEPL + manualBorrowAndRepayEPL + getBorrowAndRepayHistoryEPL vipLoansBorrowAnsRepayEPL getBorrowAnsRepayHistoryHistoryEPL + getVIPInterestAccruedDataEPL + getVIPInterestDeductedDataEPL + getVIPLoanOrderListEPL + getVIPLoanOrderDetailEPL getBorrowInterestAndLimitEPL + getFixedLoanBorrowLimitEPL + getFixedLoanBorrowQuoteEPL + placeFixedLoanBorrowingOrderEPL + amendFixedLaonBorrowingOrderEPL + manualRenewFixedLoanBorrowingOrderEPL + repayFixedLoanBorrowingOrderEPL + convertFixedLoanToMarketLoanEPL + reduceLiabilitiesForFixedLoanEPL + getFixedLoanBorrowOrderListEPL + manualBorrowOrRepayEPL + setAutoRepayEPL + getBorrowRepayHistoryEPL + newPositionBuilderEPL + setRiskOffsetAmountEPL positionBuilderEPL getGreeksEPL getPMLimitationEPL + setRiskOffsetLimiterEPL + activateOptionEPL + setAutoLoanEPL + setAccountLevelEPL + resetMMPStatusEPL viewSubaccountListEPL resetSubAccountAPIKeyEPL getSubaccountTradingBalanceEPL getSubaccountFundingBalanceEPL + getSubAccountMaxWithdrawalEPL historyOfSubaccountTransferEPL + managedSubAccountTransferEPL masterAccountsManageTransfersBetweenSubaccountEPL setPermissionOfTransferOutEPL getCustodyTradingSubaccountListEPL + setSubAccountVIPLoanAllocationEPL + getSubAccountBorrowInterestAndLimitEPL gridTradingEPL amendGridAlgoOrderEPL stopGridAlgoOrderEPL + closePositionForForContractGridEPL + cancelClosePositionOrderForContractGridEPL + instantTriggerGridAlgoOrderEPL getGridAlgoOrderListEPL getGridAlgoOrderHistoryEPL getGridAlgoOrderDetailsEPL @@ -314,16 +182,61 @@ const ( computeMarginBalanceEPL adjustMarginBalanceEPL getGridAIParameterEPL + computeMinInvestmentEPL + rsiBackTestingEPL + signalBotOrderDetailsEPL + signalBotOrderPositionsEPL + signalBotSubOrdersEPL + signalBotEventHistoryEPL + placeRecurringBuyOrderEPL + amendRecurringBuyOrderEPL + stopRecurringBuyOrderEPL + getRecurringBuyOrderListEPL + getRecurringBuyOrderHistoryEPL + getRecurringBuyOrderDetailEPL + getRecurringBuySubOrdersEPL + getExistingLeadingPositionsEPL + getLeadingPositionHistoryEPL + placeLeadingStopOrderEPL + closeLeadingPositionEPL + getLeadingInstrumentsEPL + getProfitSharingLimitEPL + getTotalProfitSharingEPL + setFirstCopySettingsEPL + amendFirstCopySettingsEPL + stopCopyingEPL + getCopySettingsEPL + getMultipleLeveragesEPL + setBatchLeverageEPL + getMyLeadTradersEPL + getLeadTraderRanksEPL + getLeadTraderWeeklyPNLEPL + getLeadTraderDailyPNLEPL + getLeadTraderStatsEPL + getLeadTraderCurrencyPreferencesEPL + getTraderCurrentLeadPositionsEPL + getLeadTraderLeadPositionHistoryEPL getOfferEPL purchaseEPL redeemEPL cancelPurchaseOrRedemptionEPL getEarnActiveOrdersEPL getFundingOrderHistoryEPL + getProductInfoEPL + + purchaseETHStakingEPL + redeemETHStakingEPL + getBETHBalanceEPL + getPurchaseRedeemHistoryEPL + getAPYHistoryEPL + getTickersEPL + getTickerEPL + getPremiumHistoryEPL getIndexTickersEPL getOrderBookEPL - getCandlestickEPL + getOrderBookLiteEPL + getCandlesticksEPL getTradesRequestEPL get24HTotalVolumeEPL getOracleEPL @@ -331,6 +244,19 @@ const ( getIndexComponentsEPL getBlockTickersEPL getBlockTradesEPL + placeSpreadOrderEPL + cancelSpreadOrderEPL + cancelAllSpreadOrderEPL + amendSpreadOrderEPL + getSpreadOrderDetailsEPL + getSpreadOrderTradesEPL + getSpreadsEPL + getSpreadOrderbookEPL + getSpreadTickerEPL + getSpreadPublicTradesEPL + cancelAllSpreadOrdersAfterEPL + getActiveSpreadOrdersEPL + getSpreadOrders7DaysEPL getInstrumentsEPL getDeliveryExerciseHistoryEPL getOpenInterestEPL @@ -349,6 +275,8 @@ const ( getUnderlyingEPL getInsuranceFundEPL unitConvertEPL + optionTickBandsEPL + getIndexTickerEPL getSupportCoinEPL getTakerVolumeEPL getMarginLendingRatioEPL @@ -360,192 +288,377 @@ const ( getTakerFlowEPL getEventStatusEPL getCandlestickHistoryEPL - getIndexCandlestickEPL + getIndexCandlesticksEPL + getIndexCandlesticksHistoryEPL + getMarkPriceCandlesticksHistoryEPL + getEconomicCalendarEPL + getEstimatedDeliveryPriceEPL + + getAffilateInviteesDetailEPL + getUserAffiliateRebateInformationEPL + + placeLendingOrderEPL + amendLendingOrderEPL + lendingOrderListEPL + lendingSubOrderListEPL + lendingPublicOfferEPL + lendingAPYHistoryEPL + lendingVolumeEPL + + rubikGetContractOpenInterestHistoryEPL + rubikContractTakerVolumeEPL + rubikTopTradersContractLongShortRatioEPL + + getAccountInstrumentsEPL + getAnnouncementsEPL + getAnnouncementTypeEPL + + getDepositOrderDetailEPL + getDepositOrderHistoryEPL + getWithdrawalOrderDetailEPL + getFiatWithdrawalOrderHistoryEPL + cancelWithdrawalOrderEPL + createWithdrawalOrderEPL + getWithdrawalPaymentMethodsEPL + getFiatDepositPaymentMethodsEPL ) // GetRateLimit returns a RateLimit instance, which implements the request.Limiter interface. func GetRateLimit() request.RateLimitDefinitions { return request.RateLimitDefinitions{ // Trade Endpoints - placeOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, placeOrderRate, 1), - placeMultipleOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, placeMultipleOrdersRate, 1), - cancelOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelOrderRate, 1), - cancelMultipleOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelMultipleOrdersRate, 1), - amendOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, amendOrderRate, 1), - amendMultipleOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, amendMultipleOrdersRate, 1), - closePositionEPL: request.NewRateLimitWithWeight(twoSecondsInterval, closePositionsRate, 1), - getOrderDetEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOrderDetails, 1), - getOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOrderListRate, 1), - getOrderHistory7DaysEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOrderHistory7DaysRate, 1), - getOrderHistory3MonthsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOrderHistory3MonthsRate, 1), - getTransactionDetail3DaysEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTransactionDetail3DaysRate, 1), - getTransactionDetail3MonthsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTransactionDetail3MonthsRate, 1), - placeAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, placeAlgoOrderRate, 1), - cancelAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelAlgoOrderRate, 1), - cancelAdvanceAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelAdvanceAlgoOrderRate, 1), - getAlgoOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getAlgoOrderListRate, 1), - getAlgoOrderHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getAlgoOrderHistoryRate, 1), - getEasyConvertCurrencyListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getEasyConvertCurrencyListRate, 1), - placeEasyConvertEPL: request.NewRateLimitWithWeight(twoSecondsInterval, placeEasyConvert, 1), - getEasyConvertHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getEasyConvertHistory, 1), - getOneClickRepayHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOneClickRepayHistory, 1), - oneClickRepayCurrencyListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, oneClickRepayCurrencyList, 1), - tradeOneClickRepayEPL: request.NewRateLimitWithWeight(twoSecondsInterval, tradeOneClickRepay, 1), + placeOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 60, 1), + placeMultipleOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 4, 1), + cancelOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 60, 1), + cancelMultipleOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 300, 1), + amendOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 60, 1), + amendMultipleOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 4, 1), + closePositionEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getOrderDetEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 60, 1), + getOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 60, 1), + getOrderHistory7DaysEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 40, 1), + getOrderHistory3MonthsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getTransactionDetail3DaysEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 60, 1), + getTransactionDetail3MonthsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + setTransactionDetail2YearIntervalEPL: request.NewRateLimitWithWeight(time.Hour*24, 5, 1), + getTransactionDetailLast2YearsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + cancelAllAfterCountdownEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + getTradeAccountRateLimitEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + orderPreCheckEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + placeAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + cancelAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + amendAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + cancelAdvanceAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getAlgoOrderDetailEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getAlgoOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getAlgoOrderHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getEasyConvertCurrencyListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + placeEasyConvertEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + getEasyConvertHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + getOneClickRepayHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + oneClickRepayCurrencyListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + tradeOneClickRepayEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + massCancemMMPOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), // Block Trading endpoints - getCounterpartiesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getCounterpartiesRate, 1), - createRfqEPL: request.NewRateLimitWithWeight(twoSecondsInterval, createRfqRate, 1), - cancelRfqEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelRfqRate, 1), - cancelMultipleRfqEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelMultipleRfqRate, 1), - cancelAllRfqsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelAllRfqsRate, 1), - executeQuoteEPL: request.NewRateLimitWithWeight(threeSecondsInterval, executeQuoteRate, 1), - setQuoteProductsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, setQuoteProducts, 1), - restMMPStatusEPL: request.NewRateLimitWithWeight(twoSecondsInterval, restMMPStatus, 1), - createQuoteEPL: request.NewRateLimitWithWeight(twoSecondsInterval, createQuoteRate, 1), - cancelQuoteEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelQuoteRate, 1), - cancelMultipleQuotesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelMultipleQuotesRate, 1), - cancelAllQuotesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, cancelAllQuotes, 1), - getRfqsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getRfqsRate, 1), - getQuotesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getQuotesRate, 1), - getTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTradesRate, 1), - getTradesHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTradesHistoryRate, 1), - getPublicTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getPublicTradesRate, 1), - // Funding - getCurrenciesEPL: request.NewRateLimitWithWeight(oneSecondInterval, getCurrenciesRate, 1), - getBalanceEPL: request.NewRateLimitWithWeight(oneSecondInterval, getBalanceRate, 1), - getAccountAssetValuationEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getAccountAssetValuationRate, 1), - fundsTransferEPL: request.NewRateLimitWithWeight(oneSecondInterval, fundsTransferRate, 1), - getFundsTransferStateEPL: request.NewRateLimitWithWeight(oneSecondInterval, getFundsTransferStateRate, 1), - assetBillsDetailsEPL: request.NewRateLimitWithWeight(oneSecondInterval, assetBillsDetailsRate, 1), - lightningDepositsEPL: request.NewRateLimitWithWeight(oneSecondInterval, lightningDepositsRate, 1), - getDepositAddressEPL: request.NewRateLimitWithWeight(oneSecondInterval, getDepositAddressRate, 1), - getDepositHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, getDepositHistoryRate, 1), - withdrawalEPL: request.NewRateLimitWithWeight(oneSecondInterval, withdrawalRate, 1), - lightningWithdrawalsEPL: request.NewRateLimitWithWeight(oneSecondInterval, lightningWithdrawalsRate, 1), - cancelWithdrawalEPL: request.NewRateLimitWithWeight(oneSecondInterval, cancelWithdrawalRate, 1), - getWithdrawalHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, getWithdrawalHistoryRate, 1), - smallAssetsConvertEPL: request.NewRateLimitWithWeight(oneSecondInterval, smallAssetsConvertRate, 1), - getSavingBalanceEPL: request.NewRateLimitWithWeight(oneSecondInterval, getSavingBalanceRate, 1), - savingsPurchaseRedemptionEPL: request.NewRateLimitWithWeight(oneSecondInterval, savingsPurchaseRedemptionRate, 1), - setLendingRateEPL: request.NewRateLimitWithWeight(oneSecondInterval, setLendingRateRate, 1), - getLendingHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, getLendingHistoryRate, 1), - getPublicBorrowInfoEPL: request.NewRateLimitWithWeight(oneSecondInterval, getPublicBorrowInfoRate, 1), - getPublicBorrowHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, getPublicBorrowHistoryRate, 1), + getCounterpartiesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + createRFQEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + cancelRFQEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + cancelMultipleRFQEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + cancelAllRFQsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + executeQuoteEPL: request.NewRateLimitWithWeight(threeSecondsInterval, 2, 1), + getQuoteProductsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setQuoteProductsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + resetMMPStatusEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + resetRFQMMPEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setMMPEPL: request.NewRateLimitWithWeight(tenSecondsInterval, 2, 1), + getMMPConfigEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + createQuoteEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 50, 1), + cancelQuoteEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 50, 1), + cancelMultipleQuotesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + cancelAllQuotesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getRFQsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getQuotesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getTradesHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + optionInstrumentTradeFamilyEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + optionTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getPublicTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + // Funding + getCurrenciesEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getBalanceEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getNonTradableAssetsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getAccountAssetValuationEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + fundsTransferEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + getFundsTransferStateEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + assetBillsDetailsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + lightningDepositsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + getDepositAddressEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getDepositHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + withdrawalEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + lightningWithdrawalsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + cancelWithdrawalEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getWithdrawalHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getDepositWithdrawalStatusEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + smallAssetsConvertEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + getPublicExchangeListEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getSavingBalanceEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + savingsPurchaseRedemptionEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + setLendingRateEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getLendingHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getPublicBorrowInfoEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getPublicBorrowHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), // Convert - getConvertCurrenciesEPL: request.NewRateLimitWithWeight(oneSecondInterval, getConvertCurrenciesRate, 1), - getConvertCurrencyPairEPL: request.NewRateLimitWithWeight(oneSecondInterval, getConvertCurrencyPairRate, 1), - estimateQuoteEPL: request.NewRateLimitWithWeight(oneSecondInterval, estimateQuoteRate, 1), - convertTradeEPL: request.NewRateLimitWithWeight(oneSecondInterval, convertTradeRate, 1), - getConvertHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, getConvertHistoryRate, 1), - + getMonthlyStatementEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + applyForMonthlyStatementEPL: request.NewRateLimitWithWeight(time.Hour*24*30, 20, 1), + getConvertCurrenciesEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getConvertCurrencyPairEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + estimateQuoteEPL: request.NewRateLimitWithWeight(oneSecondInterval, 10, 1), + convertTradeEPL: request.NewRateLimitWithWeight(oneSecondInterval, 10, 1), + getConvertHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), // Account - getAccountBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getAccountBalanceRate, 1), - getPositionsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getPositionsRate, 1), - getPositionsHistoryEPL: request.NewRateLimitWithWeight(tenSecondsInterval, getPositionsHistoryRate, 1), - getAccountAndPositionRiskEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getAccountAndPositionRiskRate, 1), - getBillsDetailsEPL: request.NewRateLimitWithWeight(oneSecondInterval, getBillsDetailsRate, 1), - getAccountConfigurationEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getAccountConfigurationRate, 1), - setPositionModeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, setPositionModeRate, 1), - setLeverageEPL: request.NewRateLimitWithWeight(twoSecondsInterval, setLeverageRate, 1), - getMaximumBuyOrSellAmountEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getMaximumBuyOrSellAmountRate, 1), - getMaximumAvailableTradableAmountEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getMaximumAvailableTradableAmountRate, 1), - increaseOrDecreaseMarginEPL: request.NewRateLimitWithWeight(twoSecondsInterval, increaseOrDecreaseMarginRate, 1), - getLeverageEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getLeverageRate, 1), - getTheMaximumLoanOfInstrumentEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTheMaximumLoanOfInstrumentRate, 1), - getFeeRatesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getFeeRatesRate, 1), - getInterestAccruedDataEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getInterestAccruedDataRate, 1), - getInterestRateEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getInterestRateRate, 1), - setGreeksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, setGreeksRate, 1), - isolatedMarginTradingSettingsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, isolatedMarginTradingSettingsRate, 1), - getMaximumWithdrawalsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getMaximumWithdrawalsRate, 1), - getAccountRiskStateEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getAccountRiskStateRate, 1), - vipLoansBorrowAnsRepayEPL: request.NewRateLimitWithWeight(oneSecondInterval, vipLoansBorrowAndRepayRate, 1), - getBorrowAnsRepayHistoryHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getBorrowAnsRepayHistoryHistoryRate, 1), - getBorrowInterestAndLimitEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getBorrowInterestAndLimitRate, 1), - positionBuilderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, positionBuilderRate, 1), - getGreeksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getGreeksRate, 1), - getPMLimitationEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getPMLimitation, 1), + getAccountBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getPositionsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getPositionsHistoryEPL: request.NewRateLimitWithWeight(tenSecondsInterval, 1, 1), + getAccountAndPositionRiskEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getBillsDetailsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 5, 1), + getBillsDetailArchiveEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + billHistoryArchiveEPL: request.NewRateLimitWithWeight(time.Hour*24, 12, 1), + getBillHistoryArchiveEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getAccountConfigurationEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setPositionModeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setLeverageEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getMaximumBuyOrSellAmountEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getMaximumAvailableTradableAmountEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + increaseOrDecreaseMarginEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getLeverageEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getLeverateEstimatedInfoEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getTheMaximumLoanOfInstrumentEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getFeeRatesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getInterestAccruedDataEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getInterestRateEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setGreeksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + isolatedMarginTradingSettingsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getMaximumWithdrawalsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getAccountRiskStateEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + manualBorrowAndRepayEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getBorrowAndRepayHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + vipLoansBorrowAnsRepayEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getBorrowAnsRepayHistoryHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getVIPInterestAccruedDataEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getVIPInterestDeductedDataEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getVIPLoanOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getVIPLoanOrderDetailEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getBorrowInterestAndLimitEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getFixedLoanBorrowLimitEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getFixedLoanBorrowQuoteEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + placeFixedLoanBorrowingOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + amendFixedLaonBorrowingOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + manualRenewFixedLoanBorrowingOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + repayFixedLoanBorrowingOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + convertFixedLoanToMarketLoanEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + reduceLiabilitiesForFixedLoanEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + getFixedLoanBorrowOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + manualBorrowOrRepayEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + setAutoRepayEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getBorrowRepayHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + newPositionBuilderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + setRiskOffsetAmountEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + positionBuilderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getGreeksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getPMLimitationEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + setRiskOffsetLimiterEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + activateOptionEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setAutoLoanEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setAccountLevelEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), // Sub Account Endpoints - viewSubaccountListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, viewSubaccountListRate, 1), - resetSubAccountAPIKeyEPL: request.NewRateLimitWithWeight(oneSecondInterval, resetSubAccountAPIKey, 1), - getSubaccountTradingBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getSubaccountTradingBalanceRate, 1), - getSubaccountFundingBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getSubaccountFundingBalanceRate, 1), - historyOfSubaccountTransferEPL: request.NewRateLimitWithWeight(oneSecondInterval, historyOfSubaccountTransferRate, 1), - masterAccountsManageTransfersBetweenSubaccountEPL: request.NewRateLimitWithWeight(oneSecondInterval, masterAccountsManageTransfersBetweenSubaccountRate, 1), - setPermissionOfTransferOutEPL: request.NewRateLimitWithWeight(oneSecondInterval, setPermissionOfTransferOutRate, 1), - getCustodyTradingSubaccountListEPL: request.NewRateLimitWithWeight(oneSecondInterval, getCustodyTradingSubaccountListRate, 1), + viewSubaccountListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + resetSubAccountAPIKeyEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + getSubaccountTradingBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getSubaccountFundingBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getSubAccountMaxWithdrawalEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + historyOfSubaccountTransferEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + managedSubAccountTransferEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + masterAccountsManageTransfersBetweenSubaccountEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + setPermissionOfTransferOutEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + getCustodyTradingSubaccountListEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), + setSubAccountVIPLoanAllocationEPL: request.NewRateLimitWithWeight(oneSecondInterval, 5, 1), + getSubAccountBorrowInterestAndLimitEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), // Grid Trading Endpoints - gridTradingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, gridTradingRate, 1), - amendGridAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, amendGridAlgoOrderRate, 1), - stopGridAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, stopGridAlgoOrderRate, 1), - getGridAlgoOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getGridAlgoOrderListRate, 1), - getGridAlgoOrderHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getGridAlgoOrderHistoryRate, 1), - getGridAlgoOrderDetailsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getGridAlgoOrderDetailsRate, 1), - getGridAlgoSubOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getGridAlgoSubOrdersRate, 1), - getGridAlgoOrderPositionsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getGridAlgoOrderPositionsRate, 1), - spotGridWithdrawIncomeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, spotGridWithdrawIncomeRate, 1), - computeMarginBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, computeMarginBalance, 1), - adjustMarginBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, adjustMarginBalance, 1), - getGridAIParameterEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getGridAIParameter, 1), + + gridTradingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + amendGridAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + stopGridAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + closePositionForForContractGridEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + cancelClosePositionOrderForContractGridEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + instantTriggerGridAlgoOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getGridAlgoOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getGridAlgoOrderHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getGridAlgoOrderDetailsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getGridAlgoSubOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getGridAlgoOrderPositionsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + spotGridWithdrawIncomeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + computeMarginBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + adjustMarginBalanceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getGridAIParameterEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + computeMinInvestmentEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + rsiBackTestingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + + // Signal Bot Trading + signalBotOrderDetailsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + signalBotOrderPositionsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + signalBotSubOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + signalBotEventHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + + // Recurring Buy Order + placeRecurringBuyOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + amendRecurringBuyOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + stopRecurringBuyOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getRecurringBuyOrderListEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getRecurringBuyOrderHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getRecurringBuyOrderDetailEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getRecurringBuySubOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getExistingLeadingPositionsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getLeadingPositionHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + placeLeadingStopOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + closeLeadingPositionEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getLeadingInstrumentsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getProfitSharingLimitEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getTotalProfitSharingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setFirstCopySettingsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + amendFirstCopySettingsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + stopCopyingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getCopySettingsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getMultipleLeveragesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + setBatchLeverageEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getMyLeadTradersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getLeadTraderRanksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getLeadTraderWeeklyPNLEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getLeadTraderDailyPNLEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getLeadTraderStatsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getLeadTraderCurrencyPreferencesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getTraderCurrentLeadPositionsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getLeadTraderLeadPositionHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), // Earn - getOfferEPL: request.NewRateLimitWithWeight(oneSecondInterval, getOffer, 1), - purchaseEPL: request.NewRateLimitWithWeight(oneSecondInterval, purchase, 1), - redeemEPL: request.NewRateLimitWithWeight(oneSecondInterval, redeem, 1), - cancelPurchaseOrRedemptionEPL: request.NewRateLimitWithWeight(oneSecondInterval, cancelPurchaseOrRedemption, 1), - getEarnActiveOrdersEPL: request.NewRateLimitWithWeight(oneSecondInterval, getEarnActiveOrders, 1), - getFundingOrderHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, getFundingOrderHistory, 1), + getOfferEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + purchaseEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + redeemEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + cancelPurchaseOrRedemptionEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + getEarnActiveOrdersEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + getFundingOrderHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + getProductInfoEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + + // ETH Staking + purchaseETHStakingEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + redeemETHStakingEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + getBETHBalanceEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getPurchaseRedeemHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), + getAPYHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 6, 1), // Market Data - getTickersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTickersRate, 1), - getIndexTickersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getIndexTickersRate, 1), - getOrderBookEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOrderBookRate, 1), - getCandlestickEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getCandlesticksRate, 1), - getCandlestickHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getCandlesticksHistoryRate, 1), - getIndexCandlestickEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getIndexCandlesticksRate, 1), - getTradesRequestEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTradesRequestRate, 1), - get24HTotalVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, get24HTotalVolumeRate, 1), - getOracleEPL: request.NewRateLimitWithWeight(fiveSecondsInterval, getOracleRate, 1), - getExchangeRateRequestEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getExchangeRateRequestRate, 1), - getIndexComponentsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getIndexComponentsRate, 1), - getBlockTickersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getBlockTickersRate, 1), - getBlockTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getBlockTradesRate, 1), + getTickersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getTickerEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getPremiumHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getIndexTickersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getOrderBookEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 40, 1), + getOrderBookLiteEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 6, 1), + getCandlesticksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 40, 1), + getCandlestickHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getIndexCandlesticksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getIndexCandlesticksHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getMarkPriceCandlesticksHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getEconomicCalendarEPL: request.NewRateLimitWithWeight(oneSecondInterval, 5, 1), + // getIndexCandlesticksEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getEstimatedDeliveryPriceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getTradesRequestEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 100, 1), + get24HTotalVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getOracleEPL: request.NewRateLimitWithWeight(fiveSecondsInterval, 1, 1), + getExchangeRateRequestEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + getIndexComponentsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getBlockTickersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getBlockTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + + // Spread Orders rate limiters + placeSpreadOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + cancelSpreadOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + cancelAllSpreadOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + amendSpreadOrderEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getSpreadOrderDetailsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getActiveSpreadOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getSpreadOrders7DaysEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getSpreadOrderTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getSpreadsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getSpreadOrderbookEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getSpreadTickerEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getSpreadPublicTradesEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + cancelAllSpreadOrdersAfterEPL: request.NewRateLimitWithWeight(oneSecondInterval, 1, 1), // Public Data Endpoints - getInstrumentsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getInstrumentsRate, 1), - getDeliveryExerciseHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getDeliveryExerciseHistoryRate, 1), - getOpenInterestEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOpenInterestRate, 1), - getFundingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getFundingRate, 1), - getFundingRateHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getFundingRateHistoryRate, 1), - getLimitPriceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getLimitPriceRate, 1), - getOptionMarketDateEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOptionMarketDateRate, 1), - getEstimatedDeliveryExercisePriceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getEstimatedDeliveryExercisePriceRate, 1), - getDiscountRateAndInterestFreeQuotaEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getDiscountRateAndInterestFreeQuotaRate, 1), - getSystemTimeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getSystemTimeRate, 1), - getLiquidationOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getLiquidationOrdersRate, 1), - getMarkPriceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getMarkPriceRate, 1), - getPositionTiersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getPositionTiersRate, 1), - getInterestRateAndLoanQuotaEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getInterestRateAndLoanQuotaRate, 1), - getInterestRateAndLoanQuoteForVIPLoansEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getInterestRateAndLoanQuoteForVIPLoansRate, 1), - getUnderlyingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getUnderlyingRate, 1), - getInsuranceFundEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getInsuranceFundRate, 1), - unitConvertEPL: request.NewRateLimitWithWeight(twoSecondsInterval, unitConvertRate, 1), + getInstrumentsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getDeliveryExerciseHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 40, 1), + getOpenInterestEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getFundingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getFundingRateHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getLimitPriceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getOptionMarketDateEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getEstimatedDeliveryExercisePriceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getDiscountRateAndInterestFreeQuotaEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getSystemTimeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getLiquidationOrdersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 40, 1), // Missing from documentation + getMarkPriceEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getPositionTiersEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + getInterestRateAndLoanQuotaEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getInterestRateAndLoanQuoteForVIPLoansEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 2, 1), + getUnderlyingEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getInsuranceFundEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + unitConvertEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + optionTickBandsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getIndexTickerEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), // Trading Data Endpoints - getSupportCoinEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getSupportCoinRate, 1), - getTakerVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTakerVolumeRate, 1), - getMarginLendingRatioEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getMarginLendingRatioRate, 1), - getLongShortRatioEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getLongShortRatioRate, 1), - getContractsOpenInterestAndVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getContractsOpenInterestAndVolumeRate, 1), - getOptionsOpenInterestAndVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOptionsOpenInterestAndVolumeRate, 1), - getPutCallRatioEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getPutCallRatioRate, 1), - getOpenInterestAndVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getOpenInterestAndVolumeRate, 1), - getTakerFlowEPL: request.NewRateLimitWithWeight(twoSecondsInterval, getTakerFlowRate, 1), + + getSupportCoinEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getTakerVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getMarginLendingRatioEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getLongShortRatioEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getContractsOpenInterestAndVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getOptionsOpenInterestAndVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getPutCallRatioEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getOpenInterestAndVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getTakerFlowEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), // Status Endpoints - getEventStatusEPL: request.NewRateLimitWithWeight(fiveSecondsInterval, getEventStatusRate, 1), + + getEventStatusEPL: request.NewRateLimitWithWeight(fiveSecondsInterval, 1, 1), + getAffilateInviteesDetailEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getUserAffiliateRebateInformationEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + + placeLendingOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + amendLendingOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 2, 1), + lendingOrderListEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + lendingSubOrderListEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + lendingPublicOfferEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + lendingAPYHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + lendingVolumeEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + + rubikGetContractOpenInterestHistoryEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 10, 1), + rubikContractTakerVolumeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + rubikTopTradersContractLongShortRatioEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + + getAccountInstrumentsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 20, 1), + getAnnouncementsEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 5, 1), + getAnnouncementTypeEPL: request.NewRateLimitWithWeight(twoSecondsInterval, 1, 1), + getDepositOrderDetailEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + getDepositOrderHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + getWithdrawalOrderDetailEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + getFiatWithdrawalOrderHistoryEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + cancelWithdrawalOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + createWithdrawalOrderEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + getWithdrawalPaymentMethodsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), + getFiatDepositPaymentMethodsEPL: request.NewRateLimitWithWeight(oneSecondInterval, 3, 1), } } diff --git a/exchanges/okx/ratelimiter_test.go b/exchanges/okx/ratelimiter_test.go new file mode 100644 index 00000000000..984eecdeca3 --- /dev/null +++ b/exchanges/okx/ratelimiter_test.go @@ -0,0 +1,283 @@ +package okx + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +func TestRateLimit_LimitStatic(t *testing.T) { + t.Parallel() + testTable := map[string]request.EndpointLimit{ + "placeOrder": placeOrderEPL, + "placeMultipleOrders": placeMultipleOrdersEPL, + "cancelOrder": cancelOrderEPL, + "cancelMultipleOrders": cancelMultipleOrdersEPL, + "amendOrder": amendOrderEPL, + "amendMultipleOrders": amendMultipleOrdersEPL, + "closePosition": closePositionEPL, + "getOrderDet": getOrderDetEPL, + "getOrderList": getOrderListEPL, + "getOrderHistory7Days": getOrderHistory7DaysEPL, + "getOrderHistory3Months": getOrderHistory3MonthsEPL, + "getTransactionDetail3Days": getTransactionDetail3DaysEPL, + "getTransactionDetail3Months": getTransactionDetail3MonthsEPL, + "setTransactionDetail2YearInterval": setTransactionDetail2YearIntervalEPL, + "getTransactionDetailLast2Years": getTransactionDetailLast2YearsEPL, + "cancelAllAfterCountdown": cancelAllAfterCountdownEPL, + "placeAlgoOrder": placeAlgoOrderEPL, + "cancelAlgoOrder": cancelAlgoOrderEPL, + "amendAlgoOrder": amendAlgoOrderEPL, + "cancelAdvanceAlgoOrder": cancelAdvanceAlgoOrderEPL, + "getAlgoOrderDetail": getAlgoOrderDetailEPL, + "getAlgoOrderList": getAlgoOrderListEPL, + "getAlgoOrderHistory": getAlgoOrderHistoryEPL, + "getEasyConvertCurrencyList": getEasyConvertCurrencyListEPL, + "placeEasyConvert": placeEasyConvertEPL, + "getEasyConvertHistory": getEasyConvertHistoryEPL, + "getOneClickRepayHistory": getOneClickRepayHistoryEPL, + "oneClickRepayCurrencyList": oneClickRepayCurrencyListEPL, + "tradeOneClickRepay": tradeOneClickRepayEPL, + "massCancemMMPOrder": massCancemMMPOrderEPL, + "getCounterparties": getCounterpartiesEPL, + "createRFQ": createRFQEPL, + "cancelRFQ": cancelRFQEPL, + "cancelMultipleRFQ": cancelMultipleRFQEPL, + "cancelAllRFQs": cancelAllRFQsEPL, + "executeQuote": executeQuoteEPL, + "getQuoteProducts": getQuoteProductsEPL, + "setQuoteProducts": setQuoteProductsEPL, + "resetRFQMMP": resetRFQMMPEPL, + "setMMP": setMMPEPL, + "getMMPConfig": getMMPConfigEPL, + "createQuote": createQuoteEPL, + "cancelQuote": cancelQuoteEPL, + "cancelMultipleQuotes": cancelMultipleQuotesEPL, + "cancelAllQuotes": cancelAllQuotesEPL, + "getRFQs": getRFQsEPL, + "getQuotes": getQuotesEPL, + "getTrades": getTradesEPL, + "getTradesHistory": getTradesHistoryEPL, + "optionInstrumentTradeFamily": optionInstrumentTradeFamilyEPL, + "optionTrades": optionTradesEPL, + "getPublicTrades": getPublicTradesEPL, + "getCurrencies": getCurrenciesEPL, + "getBalance": getBalanceEPL, + "getNonTradableAssets": getNonTradableAssetsEPL, + "getAccountAssetValuation": getAccountAssetValuationEPL, + "fundsTransfer": fundsTransferEPL, + "getFundsTransferState": getFundsTransferStateEPL, + "assetBillsDetails": assetBillsDetailsEPL, + "lightningDeposits": lightningDepositsEPL, + "getDepositAddress": getDepositAddressEPL, + "getDepositHistory": getDepositHistoryEPL, + "withdrawal": withdrawalEPL, + "lightningWithdrawals": lightningWithdrawalsEPL, + "cancelWithdrawal": cancelWithdrawalEPL, + "getWithdrawalHistory": getWithdrawalHistoryEPL, + "getDepositWithdrawalStatus": getDepositWithdrawalStatusEPL, + "smallAssetsConvert": smallAssetsConvertEPL, + "getPublicExchangeList": getPublicExchangeListEPL, + "getSavingBalance": getSavingBalanceEPL, + "savingsPurchaseRedemption": savingsPurchaseRedemptionEPL, + "setLendingRate": setLendingRateEPL, + "getLendingHistory": getLendingHistoryEPL, + "getPublicBorrowInfo": getPublicBorrowInfoEPL, + "getPublicBorrowHistory": getPublicBorrowHistoryEPL, + "getConvertCurrencies": getConvertCurrenciesEPL, + "getConvertCurrencyPair": getConvertCurrencyPairEPL, + "estimateQuote": estimateQuoteEPL, + "convertTrade": convertTradeEPL, + "getConvertHistory": getConvertHistoryEPL, + "getAccountBalance": getAccountBalanceEPL, + "getPositions": getPositionsEPL, + "getPositionsHistory": getPositionsHistoryEPL, + "getAccountAndPositionRisk": getAccountAndPositionRiskEPL, + "getBillsDetails": getBillsDetailsEPL, + "getBillsDetailArchive": getBillsDetailArchiveEPL, + "getAccountConfiguration": getAccountConfigurationEPL, + "setPositionMode": setPositionModeEPL, + "setLeverage": setLeverageEPL, + "getMaximumBuyOrSellAmount": getMaximumBuyOrSellAmountEPL, + "getMaximumAvailableTradableAmount": getMaximumAvailableTradableAmountEPL, + "increaseOrDecreaseMargin": increaseOrDecreaseMarginEPL, + "getLeverage": getLeverageEPL, + "getLeverateEstimatedInfo": getLeverateEstimatedInfoEPL, + "getTheMaximumLoanOfInstrument": getTheMaximumLoanOfInstrumentEPL, + "getFeeRates": getFeeRatesEPL, + "getInterestAccruedData": getInterestAccruedDataEPL, + "getInterestRate": getInterestRateEPL, + "setGreeks": setGreeksEPL, + "isolatedMarginTradingSettings": isolatedMarginTradingSettingsEPL, + "getMaximumWithdrawals": getMaximumWithdrawalsEPL, + "getAccountRiskState": getAccountRiskStateEPL, + "manualBorrowAndRepay": manualBorrowAndRepayEPL, + "getBorrowAndRepayHistory": getBorrowAndRepayHistoryEPL, + "vipLoansBorrowAnsRepay": vipLoansBorrowAnsRepayEPL, + "getBorrowAnsRepayHistoryHistory": getBorrowAnsRepayHistoryHistoryEPL, + "getVIPInterestAccruedData": getVIPInterestAccruedDataEPL, + "getVIPInterestDeductedData": getVIPInterestDeductedDataEPL, + "getVIPLoanOrderList": getVIPLoanOrderListEPL, + "getVIPLoanOrderDetail": getVIPLoanOrderDetailEPL, + "getBorrowInterestAndLimit": getBorrowInterestAndLimitEPL, + "positionBuilder": positionBuilderEPL, + "getGreeks": getGreeksEPL, + "getPMLimitation": getPMLimitationEPL, + "setRiskOffsetLimiter": setRiskOffsetLimiterEPL, + "activateOption": activateOptionEPL, + "setAutoLoan": setAutoLoanEPL, + "setAccountLevel": setAccountLevelEPL, + "resetMMPStatus": resetMMPStatusEPL, + "viewSubaccountList": viewSubaccountListEPL, + "resetSubAccountAPIKey": resetSubAccountAPIKeyEPL, + "getSubaccountTradingBalance": getSubaccountTradingBalanceEPL, + "getSubaccountFundingBalance": getSubaccountFundingBalanceEPL, + "getSubAccountMaxWithdrawal": getSubAccountMaxWithdrawalEPL, + "historyOfSubaccountTransfer": historyOfSubaccountTransferEPL, + "managedSubAccountTransfer": managedSubAccountTransferEPL, + "masterAccountsManageTransfersBetweenSubaccount": masterAccountsManageTransfersBetweenSubaccountEPL, + "setPermissionOfTransferOut": setPermissionOfTransferOutEPL, + "getCustodyTradingSubaccountList": getCustodyTradingSubaccountListEPL, + "setSubAccountVIPLoanAllocation": setSubAccountVIPLoanAllocationEPL, + "getSubAccountBorrowInterestAndLimit": getSubAccountBorrowInterestAndLimitEPL, + "gridTrading": gridTradingEPL, + "amendGridAlgoOrder": amendGridAlgoOrderEPL, + "stopGridAlgoOrder": stopGridAlgoOrderEPL, + "closePositionForForContractGrid": closePositionForForContractGridEPL, + "cancelClosePositionOrderForContractGrid": cancelClosePositionOrderForContractGridEPL, + "instantTriggerGridAlgoOrder": instantTriggerGridAlgoOrderEPL, + "getGridAlgoOrderList": getGridAlgoOrderListEPL, + "getGridAlgoOrderHistory": getGridAlgoOrderHistoryEPL, + "getGridAlgoOrderDetails": getGridAlgoOrderDetailsEPL, + "getGridAlgoSubOrders": getGridAlgoSubOrdersEPL, + "getGridAlgoOrderPositions": getGridAlgoOrderPositionsEPL, + "spotGridWithdrawIncome": spotGridWithdrawIncomeEPL, + "computeMarginBalance": computeMarginBalanceEPL, + "adjustMarginBalance": adjustMarginBalanceEPL, + "getGridAIParameter": getGridAIParameterEPL, + "computeMinInvestment": computeMinInvestmentEPL, + "rsiBackTesting": rsiBackTestingEPL, + "signalBotOrderDetails": signalBotOrderDetailsEPL, + "signalBotOrderPositions": signalBotOrderPositionsEPL, + "signalBotSubOrders": signalBotSubOrdersEPL, + "signalBotEventHistory": signalBotEventHistoryEPL, + "placeRecurringBuyOrder": placeRecurringBuyOrderEPL, + "amendRecurringBuyOrder": amendRecurringBuyOrderEPL, + "stopRecurringBuyOrder": stopRecurringBuyOrderEPL, + "getRecurringBuyOrderList": getRecurringBuyOrderListEPL, + "getRecurringBuyOrderHistory": getRecurringBuyOrderHistoryEPL, + "getRecurringBuyOrderDetail": getRecurringBuyOrderDetailEPL, + "getRecurringBuySubOrders": getRecurringBuySubOrdersEPL, + "getExistingLeadingPositions": getExistingLeadingPositionsEPL, + "getLeadingPositionHistory": getLeadingPositionHistoryEPL, + "placeLeadingStopOrder": placeLeadingStopOrderEPL, + "closeLeadingPosition": closeLeadingPositionEPL, + "getLeadingInstruments": getLeadingInstrumentsEPL, + "getProfitSharingLimit": getProfitSharingLimitEPL, + "getTotalProfitSharing": getTotalProfitSharingEPL, + "setFirstCopySettings": setFirstCopySettingsEPL, + "amendFirstCopySettings": amendFirstCopySettingsEPL, + "stopCopying": stopCopyingEPL, + "getCopySettings": getCopySettingsEPL, + "getMultipleLeverages": getMultipleLeveragesEPL, + "setBatchLeverage": setBatchLeverageEPL, + "getMyLeadTraders": getMyLeadTradersEPL, + "getLeadTraderRanks": getLeadTraderRanksEPL, + "getLeadTraderWeeklyPNL": getLeadTraderWeeklyPNLEPL, + "getLeadTraderDailyPNL": getLeadTraderDailyPNLEPL, + "getLeadTraderStats": getLeadTraderStatsEPL, + "getLeadTraderCurrencyPreferences": getLeadTraderCurrencyPreferencesEPL, + "getTraderCurrentLeadPositions": getTraderCurrentLeadPositionsEPL, + "getLeadTraderLeadPositionHistory": getLeadTraderLeadPositionHistoryEPL, + "getOffer": getOfferEPL, + "purchase": purchaseEPL, + "redeem": redeemEPL, + "cancelPurchaseOrRedemption": cancelPurchaseOrRedemptionEPL, + "getEarnActiveOrders": getEarnActiveOrdersEPL, + "getFundingOrderHistory": getFundingOrderHistoryEPL, + "purchaseETHStaking": purchaseETHStakingEPL, + "redeemETHStaking": redeemETHStakingEPL, + "getBETHBalance": getBETHBalanceEPL, + "getPurchaseRedeemHistory": getPurchaseRedeemHistoryEPL, + "getAPYHistory": getAPYHistoryEPL, + "getTickers": getTickersEPL, + "getTicker": getTickerEPL, + "getIndexTickers": getIndexTickersEPL, + "getOrderBook": getOrderBookEPL, + "getOrderBookLite": getOrderBookLiteEPL, + "getCandlesticks": getCandlesticksEPL, + "getTradesRequest": getTradesRequestEPL, + "get24HTotalVolume": get24HTotalVolumeEPL, + "getOracle": getOracleEPL, + "getExchangeRateRequest": getExchangeRateRequestEPL, + "getIndexComponents": getIndexComponentsEPL, + "getBlockTickers": getBlockTickersEPL, + "getBlockTrades": getBlockTradesEPL, + "placeSpreadOrder": placeSpreadOrderEPL, + "cancelSpreadOrder": cancelSpreadOrderEPL, + "cancelAllSpreadOrder": cancelAllSpreadOrderEPL, + "amendSpreadOrder": amendSpreadOrderEPL, + "getSpreadOrderDetails": getSpreadOrderDetailsEPL, + "getSpreadOrderTrades": getSpreadOrderTradesEPL, + "getSpreads": getSpreadsEPL, + "getSpreadOrderbook": getSpreadOrderbookEPL, + "getSpreadTicker": getSpreadTickerEPL, + "getSpreadPublicTrades": getSpreadPublicTradesEPL, + "getActiveSpreadOrders": getActiveSpreadOrdersEPL, + "getSpreadOrders7Days": getSpreadOrders7DaysEPL, + "getInstruments": getInstrumentsEPL, + "getDeliveryExerciseHistory": getDeliveryExerciseHistoryEPL, + "getOpenInterest": getOpenInterestEPL, + "getFunding": getFundingEPL, + "getFundingRateHistory": getFundingRateHistoryEPL, + "getLimitPrice": getLimitPriceEPL, + "getOptionMarketDate": getOptionMarketDateEPL, + "getEstimatedDeliveryExercisePrice": getEstimatedDeliveryExercisePriceEPL, + "getDiscountRateAndInterestFreeQuota": getDiscountRateAndInterestFreeQuotaEPL, + "getSystemTime": getSystemTimeEPL, + "getLiquidationOrders": getLiquidationOrdersEPL, + "getMarkPrice": getMarkPriceEPL, + "getPositionTiers": getPositionTiersEPL, + "getInterestRateAndLoanQuota": getInterestRateAndLoanQuotaEPL, + "getInterestRateAndLoanQuoteForVIPLoans": getInterestRateAndLoanQuoteForVIPLoansEPL, + "getUnderlying": getUnderlyingEPL, + "getInsuranceFund": getInsuranceFundEPL, + "unitConvert": unitConvertEPL, + "optionTickBands": optionTickBandsEPL, + "getIndexTicker": getIndexTickerEPL, + "getSupportCoin": getSupportCoinEPL, + "getTakerVolume": getTakerVolumeEPL, + "getMarginLendingRatio": getMarginLendingRatioEPL, + "getLongShortRatio": getLongShortRatioEPL, + "getContractsOpenInterestAndVolume": getContractsOpenInterestAndVolumeEPL, + "getOptionsOpenInterestAndVolume": getOptionsOpenInterestAndVolumeEPL, + "getPutCallRatio": getPutCallRatioEPL, + "getOpenInterestAndVolume": getOpenInterestAndVolumeEPL, + "getTakerFlow": getTakerFlowEPL, + "getEventStatus": getEventStatusEPL, + "getCandlestickHistory": getCandlestickHistoryEPL, + "getIndexCandlesticks": getIndexCandlesticksEPL, + "getIndexCandlesticksHistory": getIndexCandlesticksHistoryEPL, + "getMarkPriceCandlesticksHistory": getMarkPriceCandlesticksHistoryEPL, + "getEconomicCalendar": getEconomicCalendarEPL, + "getEstimatedDeliveryPrice": getEstimatedDeliveryPriceEPL, + "getAffilateInviteesDetail": getAffilateInviteesDetailEPL, + "getUserAffilateRebateInformation": getUserAffiliateRebateInformationEPL, + } + + rl, err := request.New("RateLimit_Static", http.DefaultClient, request.WithLimiter(GetRateLimit())) + require.NoError(t, err) + + for name, tt := range testTable { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if err := rl.InitiateRateLimit(context.Background(), tt); err != nil { + t.Fatalf("error applying rate limit: %v", err) + } + }) + } +} diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index b01d62aeba9..59a62429afd 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -949,7 +949,22 @@ func TestStringToOrderType(t *testing.T) { {"tRiGgEr", Trigger, nil}, {"conDitiOnal", ConditionalStop, nil}, {"oCo", OCO, nil}, + {"mMp", MarketMakerProtection, nil}, + {"Mmp_And_Post_oNly", MarketMakerProtectionAndPostOnly, nil}, + {"tWaP", TWAP, nil}, + {"TWAP", TWAP, nil}, {"woahMan", UnknownType, errUnrecognisedOrderType}, + {"chase", Chase, nil}, + {"MOVE_ORDER_STOP", TrailingStop, nil}, + {"mOVe_OrdeR_StoP", TrailingStop, nil}, + {"optimal_limit_IoC", OptimalLimitIOC, nil}, + {"Stop_market", StopMarket, nil}, + {"liquidation", Liquidation, nil}, + {"LiQuidation", Liquidation, nil}, + {"take_profit", TakeProfit, nil}, + {"Take ProfIt", TakeProfit, nil}, + {"TAKE PROFIT MARkEt", TakeProfitMarket, nil}, + {"TAKE_PROFIT_MARkEt", TakeProfitMarket, nil}, } for i := range cases { testData := &cases[i] @@ -2151,3 +2166,29 @@ func TestGetTradeAmount(t *testing.T) { s.Side = Sell require.Equal(t, baseAmount, s.GetTradeAmount(protocol.TradingRequirements{SpotMarketOrderAmountSellBaseOnly: true})) } + +func TestStringToTrackingMode(t *testing.T) { + t.Parallel() + inputs := map[string]TrackingMode{ + "diStance": Distance, + "distance": Distance, + "Percentage": Percentage, + "percentage": Percentage, + "": UnknownTrackingMode, + } + for k, v := range inputs { + assert.Equal(t, v, StringToTrackingMode(k)) + } +} + +func TestTrackingModeString(t *testing.T) { + t.Parallel() + inputs := map[TrackingMode]string{ + Distance: "distance", + Percentage: "percentage", + UnknownTrackingMode: "", + } + for k, v := range inputs { + require.Equal(t, v, k.String()) + } +} diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index 75193c1ccab..52b080a594d 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -31,6 +31,8 @@ var ( // ErrNoRates is returned when no margin rates are returned when they are expected ErrNoRates = errors.New("no rates") ErrCannotLiquidate = errors.New("cannot liquidate position") + + ErrUnknownTrackingMode = errors.New("unknown tracking mode") ) // Submit contains all properties of an order that may be required @@ -92,6 +94,11 @@ type Submit struct { // Iceberg specifies whether or not only visible portions of orders are shown in iceberg orders Iceberg bool + + // TrackingMode specifies the way trailing stop and chase orders follow the market price or ask/bid prices. + // See: https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order + TrackingMode TrackingMode + TrackingValue float64 } // SubmitResponse is what is returned after submitting an order to an exchange @@ -129,6 +136,16 @@ type SubmitResponse struct { MarginType margin.Type } +// TrackingMode defines how the stop price follows the market price. +type TrackingMode uint8 + +// Defined package tracking modes +const ( + UnknownTrackingMode TrackingMode = iota + Distance // Distance fixed amount away from the market price + Percentage // Percentage fixed percentage away from the market price +) + // Modify contains all properties of an order // that may be updated after it has been created // Each exchange has their own requirements, so not all fields @@ -359,8 +376,12 @@ const ( Liquidation Trigger OptimalLimitIOC - OCO // One-cancels-the-other order - ConditionalStop // One-way stop order + OCO // One-cancels-the-other order + ConditionalStop // One-way stop order + MarketMakerProtection // market-maker-protection used with portfolio margin mode. See https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order + MarketMakerProtectionAndPostOnly // market-maker-protection and post-only mode. Used in Okx exchange orders. + TWAP // time-weighted average price. + Chase // chase order. See https://www.okx.com/docs-v5/en/#order-book-trading-algo-trading-post-place-algo-order ) // Side enforces a standard for order sides across the code base diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index ad09ba5dfc8..2eae0b848d1 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -705,6 +705,14 @@ func (t Type) String() string { return "STOP" case ConditionalStop: return "CONDITIONAL" + case MarketMakerProtection: + return "MMP" + case MarketMakerProtectionAndPostOnly: + return "MMP_AND_POST_ONLY" + case TWAP: + return "TWAP" + case Chase: + return "CHASE" case StopLimit: return "STOP LIMIT" case StopMarket: @@ -1137,7 +1145,7 @@ func StringToOrderType(oType string) (Type, error) { return StopLimit, nil case StopMarket.String(), "STOP_MARKET": return StopMarket, nil - case TrailingStop.String(), "TRAILING STOP", "EXCHANGE TRAILING STOP": + case TrailingStop.String(), "TRAILING STOP", "EXCHANGE TRAILING STOP", "MOVE_ORDER_STOP": return TrailingStop, nil case FillOrKill.String(), "EXCHANGE FOK": return FillOrKill, nil @@ -1155,6 +1163,20 @@ func StringToOrderType(oType string) (Type, error) { return OCO, nil case ConditionalStop.String(): return ConditionalStop, nil + case MarketMakerProtection.String(): + return MarketMakerProtection, nil + case MarketMakerProtectionAndPostOnly.String(): + return MarketMakerProtectionAndPostOnly, nil + case TWAP.String(): + return TWAP, nil + case Chase.String(): + return Chase, nil + case TakeProfitMarket.String(), "TAKE_PROFIT_MARKET": + return TakeProfitMarket, nil + case TakeProfit.String(), "TAKE_PROFIT": + return TakeProfit, nil + case Liquidation.String(): + return Liquidation, nil default: return UnknownType, fmt.Errorf("'%v' %w", oType, errUnrecognisedOrderType) } @@ -1368,3 +1390,28 @@ func (t PriceType) StringToPriceType(priceType string) (PriceType, error) { return UnknownPriceType, ErrUnknownPriceType } } + +// String implements the stringer interface +func (t TrackingMode) String() string { + switch t { + case Distance: + return "distance" + case Percentage: + return "percentage" + default: + return "" + } +} + +// StringToTrackingMode converts TrackingMode instance from string +func StringToTrackingMode(mode string) TrackingMode { + mode = strings.ToLower(mode) + switch mode { + case "distance": + return Distance + case "percentage": + return Percentage + default: + return UnknownTrackingMode + } +} diff --git a/testdata/configtest.json b/testdata/configtest.json index 04f87dc7cc6..2d4ad2fd189 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -2214,7 +2214,8 @@ "margin", "option", "perpetualswap", - "spot" + "spot", + "spread" ], "pairs": { "futures": { @@ -2241,6 +2242,11 @@ "assetEnabled": true, "enabled": "BTC-USDT,ETH-USDT,OKB-USDT", "available": "OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,BTC-USDT,ETH-USDT,OKB-USDT,OKT-USDT,LTC-USDT,DOT-USDT,DOGE-USDT,LUNC-USDT,ETHW-USDT,LUNA-USDT,FIL-USDT,XRP-USDT,CITY-USDT,MENGO-USDT,ARG-USDT,POR-USDT,1INCH-USDT,AAVE-USDT,ABT-USDT,ACA-USDT,ADA-USDT,AERGO-USDT,AGLD-USDT,AKITA-USDT,ALCX-USDT,ALGO-USDT,ALPHA-USDT,ANC-USDT,ANT-USDT,ANW-USDT,APE-USDT,APIX-USDT,API3-USDT,APM-USDT,AR-USDT,ARK-USDT,AST-USDT,ASTR-USDT,ATOM-USDT,AUCTION-USDT,AVAX-USDT,AXS-USDT,AZY-USDT,BABYDOGE-USDT,BADGER-USDT,BAL-USDT,BAND-USDT,BAT-USDT,BCD-USDT,BCH-USDT,BETH-USDT,BHP-USDT,BICO-USDT,BLOK-USDT,BNT-USDT,BORING-USDT,BORA-USDT,BRWL-USDT,BSV-USDT,BTG-USDT,BTM-USDT,BTT-USDT,BZZ-USDT,CELO-USDT,CEL-USDT,CELR-USDT,CELT-USDT,CFG-USDT,CFX-USDT,CGS-USDT,CHAT-USDT,CHE-USDT,CHZ-USDT,CLV-USDT,CMT-USDT,CNTM-USDT,COMP-USDT,CONV-USDT,COVER-USDT,CQT-USDT,CRO-USDT,CRV-USDT,CSPR-USDT,CTC-USDT,CTXC-USDT,CVC-USDT,CVP-USDT,CVT-USDT,CVX-USDT,DAI-USDT,DAO-USDT,DASH-USDT,DCR-USDT,DEP-USDT,DEVT-USDT,DGB-USDT,DHT-USDT,DIA-USDT,DMD-USDT,DNA-USDT,DOME-USDT,DORA-USDT,DOSE-USDT,DYDX-USDT,EC-USDT,EDEN-USDT,EFI-USDT,EGLD-USDT,EGT-USDT,ELF-USDT,ELON-USDT,ELT-USDT,EM-USDT,ENJ-USDT,ENS-USDT,EOS-USDT,ERN-USDT,ETC-USDT,EURT-USDT,FAIR-USDT,FAME-USDT,FITFI-USDT,FLM-USDT,FLOW-USDT,FODL-USDT,FORTH-USDT,FRONT-USDT,FSN-USDT,FTM-USDT,GALA-USDT,GALFT-USDT,GARI-USDT,GAS-USDT,GF-USDT,GHST-USDT,GLM-USDT,GLMR-USDT,GM-USDT,GMT-USDT,GODS-USDT,GOG-USDT,GRT-USDT,GTO-USDT,GUSD-USDT,HBAR-USDT,HC-USDT,HDAO-USDT,HEGIC-USDT,HYC-USDT,ICP-USDT,ICX-USDT,ILV-USDT,IMX-USDT,INT-USDT,INX-USDT,IOST-USDT,IOTA-USDT,IQ-USDT,JFI-USDT,JOE-USDT,JST-USDT,KAN-USDT,KAR-USDT,KCASH-USDT,KDA-USDT,KINE-USDT,KISHU-USDT,KLAY-USDT,KNC-USDT,KOL-USDT,KONO-USDT,KP3R-USDT,KSM-USDT,LAMB-USDT,LAT-USDT,LBA-USDT,LDN-USDT,LDO-USDT,LEASH-USDT,LEO-USDT,LET-USDT,LINK-USDT,LING-USDT,LITH-USDT,LON-USDT,LOON-USDT,LOOKS-USDT,LPT-USDT,LRC-USDT,LSK-USDT,MAGIC-USDT,MANA-USDT,MASK-USDT,MATIC-USDT,MCO-USDT,MDA-USDT,MDT-USDT,MEME-USDT,METIS-USDT,MILO-USDT,MINA-USDT,MIR-USDT,MITH-USDT,MKR-USDT,MLN-USDT,MOF-USDT,MON-USDT,MOVR-USDT,MOVEZ-USDT,MXC-USDT,MXT-USDT,NAS-USDT,NEAR-USDT,NEO-USDT,NFT-USDT,NMR-USDT,NULS-USDT,NYM-USDT,OM-USDT,OMG-USDT,OMI-USDT,ONE-USDT,ONT-USDT,OP-USDT,ORBS-USDT,ORB-USDT,ORS-USDT,OXT-USDT,PAY-USDT,PCI-USDT,PEOPLE-USDT,PERP-USDT,PHA-USDT,PICKLE-USDT,PIT-USDT,PLG-USDT,PNK-USDT,POLS-USDT,POLYDOGE-USDT,PPT-USDT,PRQ-USDT,PST-USDT,PSTAKE-USDT,QOM-USDT,QTUM-USDT,RACA-USDT,RAY-USDT,REN-USDT,REP-USDT,REVV-USDT,RFUEL-USDT,RIO-USDT,RNT-USDT,ROAD-USDT,RON-USDT,RSR-USDT,RSS3-USDT,RVN-USDT,SAITAMA-USDT,SAMO-USDT,SAND-USDT,SC-USDT,SD-USDT,SFG-USDT,SHIB-USDT,SIS-USDT,SKEB-USDT,SKL-USDT,SLP-USDT,SNT-USDT,SNX-USDT,SOC-USDT,SOL-USDT,SOS-USDT,SPELL-USDT,SRM-USDT,STARL-USDT,STC-USDT,STORJ-USDT,STRK-USDT,STX-USDT,SUN-USDT,SUSHI-USDT,SWEAT-USDT,SWFTC-USDT,SWRV-USDT,T-USDT,TAI-USDT,TAKI-USDT,TCT-USDT,THETA-USDT,THG-USDT,TON-USDT,TOPC-USDT,TORN-USDT,TOWN-USDT,TRADE-USDT,TRA-USDT,TRB-USDT,TRUE-USDT,TRX-USDT,TUP-USDT,TUSD-USDT,UMA-USDT,UMEE-USDT,UNI-USDT,USDC-USDT,USDP-USDT,USTC-USDT,UTK-USDT,VALUE-USDT,VELO-USDT,VRA-USDT,VSYS-USDT,WAVES-USDT,WAXP-USDT,WBTC-USDT,WEMIX-USDT,WGRT-USDT,WING-USDT,WIN-USDT,WNCG-USDT,WNXM-USDT,WOO-USDT,WSB-USDT,WXT-USDT,XAUT-USDT,XCH-USDT,XEC-USDT,XEM-USDT,XETA-USDT,XLM-USDT,XMR-USDT,XNO-USDT,XPR-USDT,XTZ-USDT,YEE-USDT,YFI-USDT,YFII-USDT,YGG-USDT,YOU-USDT,YOYO-USDT,ZBC-USDT,ZEC-USDT,ZEN-USDT,ZIL-USDT,ZKS-USDT,ZRX-USDT,ZYRO-USDT,BTC-USDC,ETH-USDC,ETH-BTC,OKB-USDC,OKT-USDC,LTC-USDC,DOT-USDC,DOGE-USDC,LUNC-USDC,ETHW-USDC,LUNA-USDC,FIL-USDC,XRP-USDC,1INCH-USDC,AAVE-USDC,ADA-USDC,AGLD-USDC,ALGO-USDC,ANC-USDC,ANT-USDC,APE-USDC,API3-USDC,AR-USDC,ASTR-USDC,ATOM-USDC,AVAX-USDC,AXS-USDC,AZY-USDC,BABYDOGE-USDC,BAT-USDC,BCH-USDC,BICO-USDC,BSV-USDC,CEL-USDC,CELO-USDC,CELT-USDC,CHZ-USDC,COMP-USDC,CRO-USDC,CRV-USDC,CSPR-USDC,DASH-USDC,DEP-USDC,DOME-USDC,DYDX-USDC,EGLD-USDC,ELT-USDC,ENS-USDC,EOS-USDC,ETC-USDC,FITFI-USDC,FLM-USDC,FLOW-USDC,FTM-USDC,GALA-USDC,GALFT-USDC,GARI-USDC,GLMR-USDC,GMT-USDC,GODS-USDC,GRT-USDC,HBAR-USDC,ICP-USDC,IMX-USDC,IOST-USDC,JST-USDC,KISHU-USDC,KLAY-USDC,KNC-USDC,KSM-USDC,LINK-USDC,LOOKS-USDC,LRC-USDC,MANA-USDC,MASK-USDC,MATIC-USDC,MINA-USDC,MKR-USDC,MOF-USDC,MOVEZ-USDC,MXC-USDC,NEAR-USDC,NFT-USDC,NMR-USDC,NYM-USDC,OMG-USDC,OP-USDC,PEOPLE-USDC,PERP-USDC,RACA-USDC,RSR-USDC,SAITAMA-USDC,SAND-USDC,SHIB-USDC,SLP-USDC,SNX-USDC,SOC-USDC,SOL-USDC,SOS-USDC,SRM-USDC,STARL-USDC,STC-USDC,STORJ-USDC,STX-USDC,SUSHI-USDC,SWFTC-USDC,THETA-USDC,TON-USDC,TORN-USDC,TRB-USDC,TRX-USDC,UNI-USDC,USDP-USDC,USTC-USDC,VRA-USDC,WAVES-USDC,XCH-USDC,XEM-USDC,XLM-USDC,XMR-USDC,XNO-USDC,XTZ-USDC,YFI-USDC,YFII-USDC,YGG-USDC,ZEC-USDC,ZIL-USDC,BTC-DAI,ETH-DAI,BTC-USDK,ETH-USDK,USDT-USDK,OKB-BTC,OKT-BTC,LTC-BTC,DOT-BTC,DOGE-BTC,FIL-BTC,XRP-BTC,AAVE-BTC,ADA-BTC,ALGO-BTC,ALPHA-BTC,ANT-BTC,ATOM-BTC,AVAX-BTC,BADGER-BTC,BAT-BTC,BCD-BTC,BCH-BTC,BNT-BTC,BSV-BTC,BTG-BTC,BTM-BTC,CELO-BTC,CELT-BTC,CHZ-BTC,COMP-BTC,CQT-BTC,CRO-BTC,CRV-BTC,CTC-BTC,CVC-BTC,DASH-BTC,DCR-BTC,DGB-BTC,EGLD-BTC,ELF-BTC,ENJ-BTC,EOS-BTC,ETC-BTC,FLOW-BTC,GAS-BTC,GRT-BTC,GTO-BTC,HBAR-BTC,HC-BTC,ICP-BTC,ICX-BTC,INT-BTC,IOST-BTC,IOTA-BTC,KLAY-BTC,KNC-BTC,KSM-BTC,LINK-BTC,LRC-BTC,LSK-BTC,MANA-BTC,MITH-BTC,MKR-BTC,NAS-BTC,NEAR-BTC,NEO-BTC,NULS-BTC,OMG-BTC,ONT-BTC,PST-BTC,QTUM-BTC,REN-BTC,RSR-BTC,RVN-BTC,SC-BTC,SNT-BTC,SOL-BTC,SRM-BTC,STX-BTC,SWFTC-BTC,THETA-BTC,TRUE-BTC,TRX-BTC,UNI-BTC,VSYS-BTC,WAVES-BTC,WBTC-BTC,WXT-BTC,XCH-BTC,XEM-BTC,XLM-BTC,XMR-BTC,XTZ-BTC,YFI-BTC,YOU-BTC,ZEC-BTC,ZEN-BTC,ZIL-BTC,ZRX-BTC,OKB-ETH,OKT-ETH,LTC-ETH,DOT-ETH,DOGE-ETH,FIL-ETH,XRP-ETH,AAVE-ETH,ADA-ETH,API3-ETH,ATOM-ETH,AVAX-ETH,BETH-ETH,CRV-ETH,DASH-ETH,EOS-ETH,ETC-ETH,FLOW-ETH,GAS-ETH,GHST-ETH,HEGIC-ETH,INT-ETH,IOST-ETH,KSM-ETH,LINK-ETH,MANA-ETH,MKR-ETH,NEAR-ETH,NEO-ETH,NULS-ETH,OM-ETH,QTUM-ETH,SNX-ETH,SOL-ETH,SUSHI-ETH,SWFTC-ETH,TRX-ETH,UNI-ETH,WBTC-ETH,XLM-ETH,XMR-ETH,YFI-ETH,ZEC-ETH,LTC-OKB,XRP-OKB,ETC-OKB,OKDOT1-DOT,OKDOT2-DOT,BTC-EURT,ETH-EURT" + }, + "spread": { + "assetEnabled": true, + "enabled": "BTC-USDT-SWAP_BTC-USDT-250328,ETH-USDT-SWAP_ETH-USD-SWAP,ETH-USDT_ETH-USD-250627", + "available": "BTC-USDT-SWAP_BTC-USDT-250328,ETH-USDT-SWAP_ETH-USD-SWAP,ETH-USDT_ETH-USD-250627,BTC-USD-241220_BTC-USD-250328,BTC-USDT-SWAP_BTC-USDT-250228,BTC-USDT_BTC-USDT-250131,BTC-USD-SWAP_BTC-USD-250328,BTC-USDT-241220_BTC-USDT-250627,ETH-USDT-241220_ETH-USDT-250328,BTC-USD-250228_BTC-USD-250627,ETH-USDT-SWAP_ETH-USDT-250328,BTC-USD-241220_BTC-USD-250228,ETH-USDT-250328_ETH-USDT-250627,DOGE-USDT_DOGE-USDT-SWAP,ETH-USD-SWAP_ETH-USD-241227,BTC-USDT_BTC-USD-241227,BTC-USDT-SWAP_BTC-USDT-250131,ETH-USDT-241220_ETH-USDT-250627,ETH-USD-SWAP_ETH-USD-241220,BTC-USD-SWAP_BTC-USD-250228,ETH-USD-241227_ETH-USD-250228,BTC-USDT_BTC-USDT-250328,LTC-USDT_LTC-USDT-SWAP,ETH-USDT_ETH-USD-250328,ETH-USD-250328_ETH-USD-250627,ETH-USD-241227_ETH-USD-250328,BTC-USD-SWAP_BTC-USD-250131,BTC-USDT_BTC-USDT-250228,ETH-USD-250228_ETH-USD-250328,SOL-USDT_SOL-USDT-SWAP,ETH-USDT_ETH-USD-SWAP,BTC-USD-241220_BTC-USD-241227,ETH-USD-250131_ETH-USD-250328,ETH-USD-SWAP_ETH-USD-250131,BTC-USDT-241227_BTC-USDT-250131,ETH-USDT_ETH-USDT-250627,ETH-USDT-SWAP_ETH-USDT-250627,BTC-USDT_BTC-USDT-SWAP,BTC-USDT-SWAP_BTC-USDT-250627,BTC-USDT_BTC-USD-SWAP,BTC-USDT-SWAP_BTC-USD-SWAP,ETH-USDT-241220_ETH-USDT-241227,ETH-USD-241220_ETH-USD-250131,ETH-USD-SWAP_ETH-USD-250228,ETH-USD-241227_ETH-USD-250131,BTC-USDT_BTC-USD-250627,BTC-USD-241220_BTC-USD-250131,ETH-USD-241220_ETH-USD-241227,BTC-USD-SWAP_BTC-USD-250627,BTC-USD-241227_BTC-USD-250131,ETH-USD-SWAP_ETH-USD-250328,ETH-USD-250228_ETH-USD-250627,BTC-USDT-241227_BTC-USDT-250228,BTC-USDT_BTC-USD-250328,ETH-USD-250131_ETH-USD-250627,BTC-USDT_BTC-USDT-250627,BTC-USDT-241227_BTC-USDT-250328,BTC-USD-241227_BTC-USD-250328,BTC-USDT-250131_BTC-USDT-250328,BCH-USDT_BCH-USDT-SWAP,BTC-USDT-250228_BTC-USDT-250328,BTC-USD-250328_BTC-USD-250627,BTC-USDT-241220_BTC-USDT-241227,BTC-USD-241227_BTC-USD-250228,ETH-USDT_ETH-USDT-250328,BTC-USDT-250131_BTC-USDT-250228,BTC-USD-250131_BTC-USD-250627,ETH-USD-SWAP_ETH-USD-250627,ETH-USD-241220_ETH-USD-250228,BTC-USD-SWAP_BTC-USD-241227,BTC-USD-SWAP_BTC-USD-241220,BTC-USD-250131_BTC-USD-250328,BTC-USDT-241227_BTC-USDT-250627,ETH-USD-250131_ETH-USD-250228,ETH-USD-241220_ETH-USD-250328,ETH-USDT_ETH-USDT-241227,ETH-USDT-SWAP_ETH-USDT-241220,BTC-USDT-241220_BTC-USDT-250328,BTC-USD-250228_BTC-USD-250328,ETH-USDT-SWAP_ETH-USDT-241227,BTC-USDT-SWAP_BTC-USDT-241220,ETH-USDT_ETH-USDT-241220,ETH-USDT-241227_ETH-USDT-250627,BTC-USD-241227_BTC-USD-250627,BTC-USDT-241220_BTC-USDT-250228,BTC-USDT-250131_BTC-USDT-250627,BTC-USD-241220_BTC-USD-250627,BTC-USDT-SWAP_BTC-USDT-241227,BTC-USD-250131_BTC-USD-250228,BTC-USDT-250328_BTC-USDT-250627,ETH-USD-241220_ETH-USD-250627,XRP-USDT_XRP-USDT-SWAP,ETH-USD-241227_ETH-USD-250627,BTC-USDT_BTC-USDT-241227,BTC-USDT-250228_BTC-USDT-250627,BTC-USDT_BTC-USDT-241220,ETH-USDT_ETH-USD-241227,BTC-USDT-241220_BTC-USDT-250131,ETH-USDT_ETH-USDT-SWAP,ETH-USDT-241227_ETH-USDT-25032" } } },