@metaflux/fluxaction 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/RiskManager.d.ts +22 -0
- package/dist/core/RiskManager.js +179 -0
- package/dist/core/exchange.d.ts +76 -0
- package/dist/core/exchange.js +1 -0
- package/dist/core/logicalOrders.d.ts +23 -0
- package/dist/core/logicalOrders.js +1 -0
- package/dist/core/results.d.ts +18 -0
- package/dist/core/results.js +1 -0
- package/dist/exchanges/GateSpotAdapter.d.ts +33 -0
- package/dist/exchanges/GateSpotAdapter.js +146 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +70 -0
- package/examples/main.ts +77 -0
- package/package.json +34 -0
- package/src/core/RiskManager.ts +205 -0
- package/src/core/exchange.ts +87 -0
- package/src/core/logicalOrders.ts +31 -0
- package/src/core/results.ts +20 -0
- package/src/exchanges/GateSpotAdapter.ts +193 -0
- package/src/index.ts +138 -0
- package/tsconfig.json +15 -0
package/examples/main.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
import { FluxAction } from '../src'
|
|
3
|
+
import type { TrailingStopOrder, TakeProfitOrder, StopLossOrder } from '../src/core/logicalOrders'
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const flux = new FluxAction({
|
|
7
|
+
gate: {
|
|
8
|
+
apiKey: process.env.GATE_API_KEY || '',
|
|
9
|
+
secret: process.env.GATE_SECRET_KEY || '',
|
|
10
|
+
},
|
|
11
|
+
risk: { pollIntervalMs: 500 },
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
await flux.init()
|
|
15
|
+
|
|
16
|
+
const symbol = 'BTC/USDT'
|
|
17
|
+
const exchangeId = 'gate-spot'
|
|
18
|
+
const quoteCost = 10
|
|
19
|
+
|
|
20
|
+
console.log('[test] openSpotPosition', { exchangeId, symbol, quoteCost })
|
|
21
|
+
const { orderId, filledBase, avgPrice } = await flux.openSpotPosition({
|
|
22
|
+
exchangeId,
|
|
23
|
+
symbol,
|
|
24
|
+
quoteCost,
|
|
25
|
+
})
|
|
26
|
+
console.log('[test] opened', { orderId, filledBase, avgPrice })
|
|
27
|
+
|
|
28
|
+
const sellQty = filledBase * 0.9
|
|
29
|
+
const tpPrice = avgPrice * 1.04
|
|
30
|
+
const slPrice = avgPrice * 0.9
|
|
31
|
+
|
|
32
|
+
const baseId = `${exchangeId}:${symbol}:${Date.now()}`
|
|
33
|
+
|
|
34
|
+
const tp: TakeProfitOrder = {
|
|
35
|
+
id: baseId + ':tp',
|
|
36
|
+
exchangeId,
|
|
37
|
+
symbol,
|
|
38
|
+
side: 'sell',
|
|
39
|
+
qty: sellQty,
|
|
40
|
+
kind: 'take-profit',
|
|
41
|
+
triggerPrice: tpPrice,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ts: TrailingStopOrder = {
|
|
45
|
+
id: baseId + ':ts',
|
|
46
|
+
exchangeId,
|
|
47
|
+
symbol,
|
|
48
|
+
side: 'sell',
|
|
49
|
+
qty: sellQty,
|
|
50
|
+
kind: 'trailing-stop',
|
|
51
|
+
callbackRate: 0.04,
|
|
52
|
+
activationPrice: avgPrice,
|
|
53
|
+
highWatermark: avgPrice,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const sl: StopLossOrder = {
|
|
57
|
+
id: baseId + ':sl',
|
|
58
|
+
exchangeId,
|
|
59
|
+
symbol,
|
|
60
|
+
side: 'sell',
|
|
61
|
+
qty: sellQty,
|
|
62
|
+
kind: 'stop-loss',
|
|
63
|
+
triggerPrice: slPrice,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await flux.placeTakeProfit(tp)
|
|
67
|
+
await flux.placeTrailingStop(ts)
|
|
68
|
+
await flux.placeStopLoss(sl)
|
|
69
|
+
|
|
70
|
+
console.log('[test] TP, SL & trailing registered', { sellQty, tpPrice, slPrice })
|
|
71
|
+
console.log('[test] waiting RiskManager to act... (keep process running)')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
main().catch((e) => {
|
|
75
|
+
console.error('[test] error', e)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@metaflux/fluxaction",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Programmatic spot trading actions (TP/SL/trailing) for crypto exchanges",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"start": "ts-node examples/main.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"trading-bot",
|
|
17
|
+
"crypto",
|
|
18
|
+
"ccxt",
|
|
19
|
+
"risk-management",
|
|
20
|
+
"take-profit",
|
|
21
|
+
"stop-loss",
|
|
22
|
+
"trailing-stop"
|
|
23
|
+
],
|
|
24
|
+
"author": "wancareri",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"ccxt": "^4.4.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"ts-node": "^10.9.2",
|
|
32
|
+
"typescript": "^5.6.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from './exchange'
|
|
2
|
+
import type {
|
|
3
|
+
LogicalOrder,
|
|
4
|
+
TakeProfitOrder,
|
|
5
|
+
StopLossOrder,
|
|
6
|
+
TrailingStopOrder,
|
|
7
|
+
} from './logicalOrders'
|
|
8
|
+
|
|
9
|
+
export interface RiskManagerConfig {
|
|
10
|
+
exchanges: Record<string, ExchangeAdapter>
|
|
11
|
+
pollIntervalMs?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class RiskManager {
|
|
15
|
+
private cfg: RiskManagerConfig
|
|
16
|
+
private orders = new Map<string, LogicalOrder>()
|
|
17
|
+
private started = false
|
|
18
|
+
|
|
19
|
+
constructor(cfg: RiskManagerConfig) {
|
|
20
|
+
this.cfg = cfg
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
registerExchange(ex: ExchangeAdapter) {
|
|
24
|
+
this.cfg.exchanges[ex.id] = ex
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async placeTakeProfit(o: TakeProfitOrder) {
|
|
28
|
+
this.orders.set(o.id, o)
|
|
29
|
+
const ex = this.cfg.exchanges[o.exchangeId]
|
|
30
|
+
if (!ex) throw new Error(`no exchange ${o.exchangeId}`)
|
|
31
|
+
if (ex.capabilities.spot.nativeTakeProfit) {
|
|
32
|
+
// TODO
|
|
33
|
+
} else {
|
|
34
|
+
this.startPolling()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async placeStopLoss(o: StopLossOrder) {
|
|
39
|
+
this.orders.set(o.id, o)
|
|
40
|
+
const ex = this.cfg.exchanges[o.exchangeId]
|
|
41
|
+
if (!ex) throw new Error(`no exchange ${o.exchangeId}`)
|
|
42
|
+
if (ex.capabilities.spot.nativeStopLoss) {
|
|
43
|
+
// TODO
|
|
44
|
+
} else {
|
|
45
|
+
this.startPolling()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async placeTrailingStop(o: TrailingStopOrder) {
|
|
50
|
+
this.orders.set(o.id, o)
|
|
51
|
+
const ex = this.cfg.exchanges[o.exchangeId]
|
|
52
|
+
if (!ex) throw new Error(`no exchange ${o.exchangeId}`)
|
|
53
|
+
if (ex.capabilities.spot.nativeTrailingStop) {
|
|
54
|
+
// TODO
|
|
55
|
+
} else {
|
|
56
|
+
this.startPolling()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async cancel(id: string) {
|
|
61
|
+
this.orders.delete(id)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getActiveOrders(): LogicalOrder[] {
|
|
65
|
+
return [...this.orders.values()]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private startPolling() {
|
|
69
|
+
if (this.started) return
|
|
70
|
+
this.started = true
|
|
71
|
+
const interval = this.cfg.pollIntervalMs ?? 3_000
|
|
72
|
+
setInterval(() => {
|
|
73
|
+
this.tick().catch((e) => {
|
|
74
|
+
console.error('[RiskManager] tick error', e)
|
|
75
|
+
})
|
|
76
|
+
}, interval)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async tick() {
|
|
80
|
+
const grouped: Record<string, Record<string, LogicalOrder[]>> = {}
|
|
81
|
+
for (const o of this.orders.values()) {
|
|
82
|
+
if (!grouped[o.exchangeId]) grouped[o.exchangeId] = {}
|
|
83
|
+
if (!grouped[o.exchangeId][o.symbol]) grouped[o.exchangeId][o.symbol] = []
|
|
84
|
+
grouped[o.exchangeId][o.symbol].push(o)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const [exId, bySymbol] of Object.entries(grouped)) {
|
|
88
|
+
const ex = this.cfg.exchanges[exId]
|
|
89
|
+
if (!ex) continue
|
|
90
|
+
for (const [symbol, orders] of Object.entries(bySymbol)) {
|
|
91
|
+
await this.processForSymbol(ex, symbol, orders)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async processForSymbol(
|
|
97
|
+
ex: ExchangeAdapter,
|
|
98
|
+
symbol: string,
|
|
99
|
+
orders: LogicalOrder[],
|
|
100
|
+
) {
|
|
101
|
+
const ticker = await ex.fetchTicker(symbol)
|
|
102
|
+
const price = ticker.last
|
|
103
|
+
if (!price || price <= 0) return
|
|
104
|
+
|
|
105
|
+
for (const o of orders) {
|
|
106
|
+
if (o.kind === 'take-profit') {
|
|
107
|
+
const tp = o as TakeProfitOrder
|
|
108
|
+
if (o.side === 'sell' && price >= tp.triggerPrice) {
|
|
109
|
+
console.log('[RiskManager] TP condition hit', {
|
|
110
|
+
id: o.id,
|
|
111
|
+
price,
|
|
112
|
+
triggerPrice: tp.triggerPrice,
|
|
113
|
+
})
|
|
114
|
+
await this.executeAndClear(ex, o, price)
|
|
115
|
+
}
|
|
116
|
+
} else if (o.kind === 'stop-loss') {
|
|
117
|
+
const sl = o as StopLossOrder
|
|
118
|
+
if (o.side === 'sell' && price <= sl.triggerPrice) {
|
|
119
|
+
console.log('[RiskManager] SL condition hit', {
|
|
120
|
+
id: o.id,
|
|
121
|
+
price,
|
|
122
|
+
triggerPrice: sl.triggerPrice,
|
|
123
|
+
})
|
|
124
|
+
await this.executeAndClear(ex, o, price)
|
|
125
|
+
}
|
|
126
|
+
} else if (o.kind === 'trailing-stop') {
|
|
127
|
+
const ts = o as TrailingStopOrder
|
|
128
|
+
if (!ts.highWatermark || price > ts.highWatermark) {
|
|
129
|
+
ts.highWatermark = price
|
|
130
|
+
this.orders.set(o.id, ts)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
const drop = (ts.highWatermark - price) / ts.highWatermark
|
|
134
|
+
|
|
135
|
+
if (drop >= ts.callbackRate) {
|
|
136
|
+
console.log('[RiskManager] TRAILING condition hit', {
|
|
137
|
+
id: o.id,
|
|
138
|
+
price,
|
|
139
|
+
highWatermark: ts.highWatermark,
|
|
140
|
+
drop,
|
|
141
|
+
})
|
|
142
|
+
await this.executeAndClear(ex, o, price)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async executeAndClear(
|
|
149
|
+
ex: ExchangeAdapter,
|
|
150
|
+
o: LogicalOrder,
|
|
151
|
+
price: number,
|
|
152
|
+
) {
|
|
153
|
+
console.log('[RiskManager] EXIT', {
|
|
154
|
+
exchangeId: ex.id,
|
|
155
|
+
symbol: o.symbol,
|
|
156
|
+
kind: o.kind,
|
|
157
|
+
side: o.side,
|
|
158
|
+
qty: o.qty,
|
|
159
|
+
price,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
this.orders.delete(o.id)
|
|
163
|
+
|
|
164
|
+
if (o.side === 'sell') {
|
|
165
|
+
for (const [id, ord] of this.orders.entries()) {
|
|
166
|
+
if (ord.exchangeId === o.exchangeId && ord.symbol === o.symbol && ord.side === 'sell') {
|
|
167
|
+
this.orders.delete(id)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const bal = await ex.fetchBalance()
|
|
173
|
+
const base = o.symbol.split('/')[0]
|
|
174
|
+
const free = bal[base]?.free ?? 0
|
|
175
|
+
const rawQty = Math.min(o.qty, free * 0.995)
|
|
176
|
+
|
|
177
|
+
if (rawQty <= 0) {
|
|
178
|
+
console.warn('[RiskManager] skip exit: qty <= 0', { symbol: o.symbol, base, free, requested: o.qty })
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (o.side === 'sell') {
|
|
183
|
+
try {
|
|
184
|
+
await ex.marketSellSpot(o.symbol, rawQty)
|
|
185
|
+
} catch (e: any) {
|
|
186
|
+
const msg = String(e.message || '')
|
|
187
|
+
if (msg.includes('too small') && msg.includes('Your order size')) {
|
|
188
|
+
console.warn('[RiskManager] sell failed: below exchange min size', {
|
|
189
|
+
symbol: o.symbol,
|
|
190
|
+
base,
|
|
191
|
+
free,
|
|
192
|
+
requested: o.qty,
|
|
193
|
+
rawQty,
|
|
194
|
+
error: msg,
|
|
195
|
+
})
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
throw e
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const quoteCost = price * rawQty
|
|
202
|
+
await ex.marketBuySpot(o.symbol, quoteCost)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type MarketType = 'spot' | 'futures'
|
|
2
|
+
export type Side = 'buy' | 'sell'
|
|
3
|
+
export type PositionSide = 'long' | 'short'
|
|
4
|
+
|
|
5
|
+
export interface MarketInfo {
|
|
6
|
+
symbol: string
|
|
7
|
+
base: string
|
|
8
|
+
quote: string
|
|
9
|
+
type: MarketType
|
|
10
|
+
precisionAmount?: number
|
|
11
|
+
precisionPrice?: number
|
|
12
|
+
minCost?: number
|
|
13
|
+
minAmount?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OrderInfo {
|
|
17
|
+
id: string
|
|
18
|
+
symbol: string
|
|
19
|
+
side: Side
|
|
20
|
+
type: 'market' | 'limit'
|
|
21
|
+
price?: number
|
|
22
|
+
amount: number
|
|
23
|
+
status: string
|
|
24
|
+
raw: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PositionInfo {
|
|
28
|
+
symbol: string
|
|
29
|
+
side: PositionSide
|
|
30
|
+
size: number
|
|
31
|
+
entryPrice: number
|
|
32
|
+
raw: any
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ExchangeCapabilities {
|
|
36
|
+
spot: {
|
|
37
|
+
nativeTakeProfit: boolean
|
|
38
|
+
nativeStopLoss: boolean
|
|
39
|
+
nativeTrailingStop: boolean
|
|
40
|
+
}
|
|
41
|
+
futures: {
|
|
42
|
+
nativeTakeProfit: boolean
|
|
43
|
+
nativeStopLoss: boolean
|
|
44
|
+
nativeTrailingStop: boolean
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ExchangeAdapter {
|
|
49
|
+
readonly id: string
|
|
50
|
+
readonly label: string
|
|
51
|
+
readonly capabilities: ExchangeCapabilities
|
|
52
|
+
|
|
53
|
+
loadMarkets(): Promise<void>
|
|
54
|
+
|
|
55
|
+
hasSpotMarket(symbol: string): boolean
|
|
56
|
+
hasFuturesMarket(symbol: string): boolean
|
|
57
|
+
getMarket(symbol: string): MarketInfo | undefined
|
|
58
|
+
|
|
59
|
+
fetchTicker(symbol: string): Promise<{ last: number }>
|
|
60
|
+
fetchBalance(): Promise<Record<string, { free: number; used: number; total: number }>>
|
|
61
|
+
fetchOpenOrders(symbol?: string): Promise<OrderInfo[]>
|
|
62
|
+
cancelOrder(symbol: string, orderId: string): Promise<void>
|
|
63
|
+
|
|
64
|
+
marketBuySpot(symbol: string, quoteCost: number): Promise<OrderInfo>
|
|
65
|
+
marketSellSpot(symbol: string, baseAmount: number): Promise<OrderInfo>
|
|
66
|
+
limitOrderSpot(
|
|
67
|
+
symbol: string,
|
|
68
|
+
side: Side,
|
|
69
|
+
baseAmount: number,
|
|
70
|
+
price: number,
|
|
71
|
+
): Promise<OrderInfo>
|
|
72
|
+
|
|
73
|
+
openFutures(params: {
|
|
74
|
+
symbol: string
|
|
75
|
+
side: PositionSide
|
|
76
|
+
costUSDT: number
|
|
77
|
+
leverage?: number
|
|
78
|
+
}): Promise<OrderInfo>
|
|
79
|
+
|
|
80
|
+
closeFutures(params: {
|
|
81
|
+
symbol: string
|
|
82
|
+
side: PositionSide
|
|
83
|
+
costUSDT?: number
|
|
84
|
+
}): Promise<OrderInfo>
|
|
85
|
+
|
|
86
|
+
fetchPositions?(symbol?: string): Promise<PositionInfo[]>
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type LogicalOrderKind = 'take-profit' | 'stop-loss' | 'trailing-stop'
|
|
2
|
+
|
|
3
|
+
export interface LogicalOrderBase {
|
|
4
|
+
id: string
|
|
5
|
+
exchangeId: string
|
|
6
|
+
symbol: string
|
|
7
|
+
side: 'buy' | 'sell'
|
|
8
|
+
qty: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TakeProfitOrder extends LogicalOrderBase {
|
|
12
|
+
kind: 'take-profit'
|
|
13
|
+
triggerPrice: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StopLossOrder extends LogicalOrderBase {
|
|
17
|
+
kind: 'stop-loss'
|
|
18
|
+
triggerPrice: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TrailingStopOrder extends LogicalOrderBase {
|
|
22
|
+
kind: 'trailing-stop'
|
|
23
|
+
callbackRate: number
|
|
24
|
+
activationPrice?: number
|
|
25
|
+
highWatermark?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type LogicalOrder =
|
|
29
|
+
| TakeProfitOrder
|
|
30
|
+
| StopLossOrder
|
|
31
|
+
| TrailingStopOrder
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SpotListingExecutionStep {
|
|
2
|
+
at: number
|
|
3
|
+
level: 'info' | 'warn' | 'error'
|
|
4
|
+
message: string
|
|
5
|
+
context?: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface SpotListingExecutionLog {
|
|
9
|
+
exchangeId: string
|
|
10
|
+
symbol: string
|
|
11
|
+
steps: SpotListingExecutionStep[]
|
|
12
|
+
error?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SpotListingResult {
|
|
16
|
+
ticker: string
|
|
17
|
+
normalizedSymbol: string
|
|
18
|
+
perExchangeBudget: number
|
|
19
|
+
executions: SpotListingExecutionLog[]
|
|
20
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import ccxt, { type gateio as GateCcxt } from 'ccxt'
|
|
2
|
+
import {
|
|
3
|
+
type ExchangeAdapter,
|
|
4
|
+
type ExchangeCapabilities,
|
|
5
|
+
type MarketInfo,
|
|
6
|
+
type OrderInfo,
|
|
7
|
+
type PositionInfo,
|
|
8
|
+
} from '../core/exchange'
|
|
9
|
+
|
|
10
|
+
export interface GateSpotAdapterConfig {
|
|
11
|
+
id: string
|
|
12
|
+
label?: string
|
|
13
|
+
apiKey: string
|
|
14
|
+
secret: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class GateSpotAdapter implements ExchangeAdapter {
|
|
18
|
+
readonly id: string
|
|
19
|
+
readonly label: string
|
|
20
|
+
readonly capabilities: ExchangeCapabilities
|
|
21
|
+
private ex: GateCcxt
|
|
22
|
+
|
|
23
|
+
constructor(cfg: GateSpotAdapterConfig) {
|
|
24
|
+
this.id = cfg.id
|
|
25
|
+
this.label = cfg.label ?? 'gate-spot'
|
|
26
|
+
|
|
27
|
+
this.ex = new ccxt.gateio({
|
|
28
|
+
apiKey: cfg.apiKey,
|
|
29
|
+
secret: cfg.secret,
|
|
30
|
+
enableRateLimit: true,
|
|
31
|
+
options: {
|
|
32
|
+
defaultType: 'spot',
|
|
33
|
+
createMarketBuyOrderRequiresPrice: false,
|
|
34
|
+
},
|
|
35
|
+
}) as GateCcxt
|
|
36
|
+
|
|
37
|
+
this.capabilities = {
|
|
38
|
+
spot: {
|
|
39
|
+
nativeTakeProfit: false,
|
|
40
|
+
nativeStopLoss: false,
|
|
41
|
+
nativeTrailingStop: false,
|
|
42
|
+
},
|
|
43
|
+
futures: {
|
|
44
|
+
nativeTakeProfit: false,
|
|
45
|
+
nativeStopLoss: false,
|
|
46
|
+
nativeTrailingStop: false,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async loadMarkets(): Promise<void> {
|
|
52
|
+
await this.ex.loadMarkets()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
hasSpotMarket(symbol: string): boolean {
|
|
56
|
+
const m = this.ex.markets?.[symbol]
|
|
57
|
+
return !!m && m.type === 'spot'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
hasFuturesMarket(): boolean {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getMarket(symbol: string): MarketInfo | undefined {
|
|
65
|
+
const m = this.ex.markets?.[symbol]
|
|
66
|
+
if (!m) return undefined
|
|
67
|
+
return {
|
|
68
|
+
symbol: m.symbol,
|
|
69
|
+
base: m.base,
|
|
70
|
+
quote: m.quote,
|
|
71
|
+
type: m.type as any,
|
|
72
|
+
precisionAmount: m.precision?.amount,
|
|
73
|
+
precisionPrice: m.precision?.price,
|
|
74
|
+
minCost: m.limits?.cost?.min,
|
|
75
|
+
minAmount: m.limits?.amount?.min,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private roundAmount(symbol: string, amount: number): number {
|
|
80
|
+
const m = this.ex.markets?.[symbol]
|
|
81
|
+
if (!m) return amount
|
|
82
|
+
return Number(this.ex.amountToPrecision(symbol, amount))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private roundPrice(symbol: string, price: number): number {
|
|
86
|
+
const m = this.ex.markets?.[symbol]
|
|
87
|
+
if (!m) return price
|
|
88
|
+
return Number(this.ex.priceToPrecision(symbol, price))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private mapOrder(symbol: string, o: any): OrderInfo {
|
|
92
|
+
const price = o.price ?? o.average ?? o.avgPrice
|
|
93
|
+
|
|
94
|
+
let baseAmount: number
|
|
95
|
+
if (price && o.cost) {
|
|
96
|
+
baseAmount = o.cost / price
|
|
97
|
+
} else if (price && o.amount) {
|
|
98
|
+
baseAmount = o.amount / price
|
|
99
|
+
} else if (typeof o.amount === 'number' && o.amount > 0) {
|
|
100
|
+
baseAmount = o.amount
|
|
101
|
+
} else {
|
|
102
|
+
baseAmount = 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: o.id,
|
|
107
|
+
symbol,
|
|
108
|
+
side: o.side,
|
|
109
|
+
type: o.type,
|
|
110
|
+
price,
|
|
111
|
+
amount: baseAmount,
|
|
112
|
+
status: o.status,
|
|
113
|
+
raw: o,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async fetchTicker(symbol: string): Promise<{ last: number }> {
|
|
118
|
+
const t = await this.ex.fetchTicker(symbol)
|
|
119
|
+
return { last: t.last ?? 0 }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async fetchBalance() {
|
|
123
|
+
const bal = await this.ex.fetchBalance()
|
|
124
|
+
return bal as any
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async fetchOpenOrders(symbol?: string): Promise<OrderInfo[]> {
|
|
128
|
+
const orders = await this.ex.fetchOpenOrders(symbol)
|
|
129
|
+
return orders.map((o: any) => this.mapOrder(o.symbol, o))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async cancelOrder(symbol: string, orderId: string): Promise<void> {
|
|
133
|
+
await this.ex.cancelOrder(orderId, symbol)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async marketBuySpot(symbol: string, quoteCost: number): Promise<OrderInfo> {
|
|
137
|
+
const ticker = await this.fetchTicker(symbol)
|
|
138
|
+
if (!ticker.last || ticker.last <= 0) {
|
|
139
|
+
throw new Error(`gate-spot invalid price for ${symbol}: ${ticker.last}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const m = this.getMarket(symbol)
|
|
143
|
+
if (m?.minCost && quoteCost < m.minCost) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`gate-spot buy violates minCost: ${quoteCost} < ${m.minCost} ${m.quote}`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const order = await this.ex.createMarketBuyOrderWithCost(symbol, quoteCost, {
|
|
150
|
+
createMarketBuyOrderRequiresPrice: false,
|
|
151
|
+
})
|
|
152
|
+
return this.mapOrder(symbol, order)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async marketSellSpot(symbol: string, baseAmount: number): Promise<OrderInfo> {
|
|
156
|
+
const amount = this.roundAmount(symbol, baseAmount)
|
|
157
|
+
if (!amount || amount <= 0) {
|
|
158
|
+
throw new Error(`gate-spot sell amount too small for ${symbol}: ${amount}`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const order = await this.ex.createOrder(symbol, 'market', 'sell', amount)
|
|
162
|
+
return this.mapOrder(symbol, order)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async limitOrderSpot(
|
|
166
|
+
symbol: string,
|
|
167
|
+
side: 'buy' | 'sell',
|
|
168
|
+
baseAmount: number,
|
|
169
|
+
price: number,
|
|
170
|
+
): Promise<OrderInfo> {
|
|
171
|
+
const amount = this.roundAmount(symbol, baseAmount)
|
|
172
|
+
const p = this.roundPrice(symbol, price)
|
|
173
|
+
|
|
174
|
+
if (!amount || amount <= 0) {
|
|
175
|
+
throw new Error(`gate-spot limit amount too small for ${symbol}: ${amount}`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const order = await this.ex.createOrder(symbol, 'limit', side, amount, p)
|
|
179
|
+
return this.mapOrder(symbol, order)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async openFutures(): Promise<OrderInfo> {
|
|
183
|
+
throw new Error('gate-spot: futures not supported')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async closeFutures(): Promise<OrderInfo> {
|
|
187
|
+
throw new Error('gate-spot: futures not supported')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async fetchPositions(): Promise<PositionInfo[]> {
|
|
191
|
+
return []
|
|
192
|
+
}
|
|
193
|
+
}
|