@metaflux/fluxaction 0.1.5 → 0.1.6
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/package.json +5 -3
- package/scripts/futures.ts +80 -0
- package/scripts/spot.ts +84 -0
- package/src/FluxAction.ts +150 -0
- package/src/clients/GateClient.ts +52 -0
- package/src/core/exchange.ts +8 -0
- package/src/core/logicalOrders.ts +21 -7
- package/src/core/orders.ts +7 -0
- package/src/exchanges/gate/futures/GateFuturesAdapter.ts +301 -0
- package/src/exchanges/gate/index.ts +2 -0
- package/src/exchanges/gate/spot/GateSpotAdapter.ts +208 -0
- package/src/exchanges/gate/spot/gate.mapOrder.ts +22 -0
- package/src/index.ts +21 -126
- package/src/risk/ExitExecutor.ts +56 -0
- package/src/risk/OrderStore.ts +21 -0
- package/src/risk/RiskEngine.ts +101 -0
- package/src/risk/RiskScheduler.ts +23 -0
- package/src/services/ExchangeRegistry.ts +27 -0
- package/examples/main.ts +0 -77
- package/src/core/RiskManager.ts +0 -205
- package/src/core/results.ts +0 -20
- package/src/exchanges/GateSpotAdapter.ts +0 -193
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import ccxt, { type gateio as GateCcxt } from 'ccxt'
|
|
2
|
+
import type {
|
|
3
|
+
ExchangeAdapter,
|
|
4
|
+
ExchangeCapabilities,
|
|
5
|
+
MarketInfo,
|
|
6
|
+
OrderInfo,
|
|
7
|
+
PositionInfo,
|
|
8
|
+
PositionSide,
|
|
9
|
+
} from '../../../core/exchange'
|
|
10
|
+
import { mapCcxtOrder } from '../spot/gate.mapOrder'
|
|
11
|
+
|
|
12
|
+
export interface GateFuturesAdapterConfig {
|
|
13
|
+
id: string
|
|
14
|
+
label?: string
|
|
15
|
+
apiKey: string
|
|
16
|
+
secret: string
|
|
17
|
+
settle?: 'usdt' | 'btc'
|
|
18
|
+
defaultLeverage?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class GateFuturesAdapter implements ExchangeAdapter {
|
|
22
|
+
readonly id: string
|
|
23
|
+
readonly label: string
|
|
24
|
+
readonly capabilities: ExchangeCapabilities
|
|
25
|
+
|
|
26
|
+
private ex: GateCcxt
|
|
27
|
+
private settle: 'usdt' | 'btc'
|
|
28
|
+
private defaultLeverage?: number
|
|
29
|
+
|
|
30
|
+
constructor(cfg: GateFuturesAdapterConfig) {
|
|
31
|
+
this.id = cfg.id
|
|
32
|
+
this.label = cfg.label ?? 'gate-futures'
|
|
33
|
+
this.settle = cfg.settle ?? 'usdt'
|
|
34
|
+
this.defaultLeverage = cfg.defaultLeverage
|
|
35
|
+
|
|
36
|
+
this.ex = new ccxt.gateio({
|
|
37
|
+
apiKey: cfg.apiKey,
|
|
38
|
+
secret: cfg.secret,
|
|
39
|
+
enableRateLimit: true,
|
|
40
|
+
options: { defaultType: 'swap' },
|
|
41
|
+
}) as GateCcxt
|
|
42
|
+
|
|
43
|
+
this.capabilities = {
|
|
44
|
+
spot: { nativeTakeProfit: false, nativeStopLoss: false, nativeTrailingStop: false },
|
|
45
|
+
futures: { nativeTakeProfit: false, nativeStopLoss: false, nativeTrailingStop: false },
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async loadMarkets(): Promise<void> {
|
|
50
|
+
await this.ex.loadMarkets()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
hasSpotMarket(_symbol: string): boolean {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
hasFuturesMarket(symbol: string): boolean {
|
|
58
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
59
|
+
return !!m && (m.type === 'swap' || m.swap === true)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getMarket(symbol: string): MarketInfo | undefined {
|
|
63
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
64
|
+
if (!m) return undefined
|
|
65
|
+
return {
|
|
66
|
+
symbol: m.symbol,
|
|
67
|
+
base: m.base,
|
|
68
|
+
quote: m.quote,
|
|
69
|
+
type: 'futures',
|
|
70
|
+
precisionAmount: m.precision?.amount,
|
|
71
|
+
precisionPrice: m.precision?.price,
|
|
72
|
+
minCost: m.limits?.cost?.min,
|
|
73
|
+
minAmount: m.limits?.amount?.min,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private roundAmount(symbol: string, amount: number): number {
|
|
78
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
79
|
+
if (!m) return amount
|
|
80
|
+
return Number((this.ex as any).amountToPrecision(symbol, amount))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private roundPrice(symbol: string, price: number): number {
|
|
84
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
85
|
+
if (!m) return price
|
|
86
|
+
return Number((this.ex as any).priceToPrecision(symbol, price))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async fetchTicker(symbol: string): Promise<{ last: number }> {
|
|
90
|
+
const t: any = await (this.ex as any).fetchTicker(symbol)
|
|
91
|
+
return { last: Number(t?.last ?? 0) }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async fetchBalance(): Promise<Record<string, { free: number; used: number; total: number }>> {
|
|
95
|
+
const bal: any = await (this.ex as any).fetchBalance()
|
|
96
|
+
return bal as any
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async fetchOpenOrders(symbol?: string): Promise<OrderInfo[]> {
|
|
100
|
+
const orders: any[] = await (this.ex as any).fetchOpenOrders(symbol)
|
|
101
|
+
return orders.map((o: any) => mapCcxtOrder(o.symbol, o))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async cancelOrder(symbol: string, orderId: string): Promise<void> {
|
|
105
|
+
await (this.ex as any).cancelOrder(orderId, symbol)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ===== helpers =====
|
|
109
|
+
private getMarketRaw(symbol: string): any {
|
|
110
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
111
|
+
if (!m) throw new Error(`gate-futures: unknown market ${symbol}`)
|
|
112
|
+
return m
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private getContractSize(symbol: string): number {
|
|
116
|
+
const m = this.getMarketRaw(symbol)
|
|
117
|
+
const cs = Number(m.contractSize ?? 1)
|
|
118
|
+
return cs > 0 ? cs : 1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Конвертирует USDT стоимость в дробное количество контрактов
|
|
123
|
+
* Gate API поддерживает дробные размеры через X-Gate-Size-Decimal: 1
|
|
124
|
+
*/
|
|
125
|
+
private costUSDTToContracts(symbol: string, costUSDT: number, price: number): number {
|
|
126
|
+
const contractSize = this.getContractSize(symbol)
|
|
127
|
+
const notionalPerContract = price * contractSize
|
|
128
|
+
|
|
129
|
+
// Дробное количество контрактов (может быть 0.17, 1.71 и т.д.)
|
|
130
|
+
const contractsFloat = costUSDT / notionalPerContract
|
|
131
|
+
|
|
132
|
+
if (contractsFloat < 0.0001) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`gate-futures: costUSDT too small. ` +
|
|
135
|
+
`Minimum notional per contract: ${notionalPerContract.toFixed(2)} USDT, got ${costUSDT}`,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return contractsFloat
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async quoteToBaseQty(params: { symbol: string; quoteCost: number; price?: number }): Promise<number> {
|
|
143
|
+
const { symbol, quoteCost } = params
|
|
144
|
+
if (!quoteCost || quoteCost <= 0) throw new Error(`gate-futures quoteCost must be > 0, got ${quoteCost}`)
|
|
145
|
+
|
|
146
|
+
const p = params.price ?? (await this.fetchTicker(symbol)).last
|
|
147
|
+
if (!p || p <= 0) throw new Error(`gate-futures invalid price for ${symbol}: ${p}`)
|
|
148
|
+
|
|
149
|
+
// Возвращаем дробное количество контрактов
|
|
150
|
+
const contracts = this.costUSDTToContracts(symbol, quoteCost, p)
|
|
151
|
+
return contracts
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async baseQtyToQuote(params: { symbol: string; baseQty: number; price?: number }): Promise<number> {
|
|
155
|
+
// baseQty здесь = contracts (может быть дробное)
|
|
156
|
+
if (!params.baseQty || params.baseQty <= 0) return 0
|
|
157
|
+
|
|
158
|
+
const p = params.price ?? (await this.fetchTicker(params.symbol)).last
|
|
159
|
+
if (!p || p <= 0) throw new Error(`gate-futures invalid price for ${params.symbol}: ${p}`)
|
|
160
|
+
|
|
161
|
+
const contractSize = this.getContractSize(params.symbol)
|
|
162
|
+
// contracts * contractSize * price = notional in USDT
|
|
163
|
+
return params.baseQty * contractSize * p
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ===== spot methods not supported =====
|
|
167
|
+
async marketBuySpot(): Promise<OrderInfo> {
|
|
168
|
+
throw new Error('gate-futures: spot not supported')
|
|
169
|
+
}
|
|
170
|
+
async marketSellSpot(): Promise<OrderInfo> {
|
|
171
|
+
throw new Error('gate-futures: spot not supported')
|
|
172
|
+
}
|
|
173
|
+
async limitOrderSpot(): Promise<OrderInfo> {
|
|
174
|
+
throw new Error('gate-futures: spot not supported')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ===== futures =====
|
|
178
|
+
async openFutures(params: {
|
|
179
|
+
symbol: string
|
|
180
|
+
side: PositionSide
|
|
181
|
+
costUSDT: number
|
|
182
|
+
leverage?: number
|
|
183
|
+
price?: number | null
|
|
184
|
+
}): Promise<OrderInfo> {
|
|
185
|
+
const symbol = params.symbol
|
|
186
|
+
|
|
187
|
+
// Set leverage
|
|
188
|
+
const leverage = params.leverage ?? this.defaultLeverage
|
|
189
|
+
if (leverage) {
|
|
190
|
+
try {
|
|
191
|
+
await (this.ex as any).setLeverage(leverage, symbol)
|
|
192
|
+
} catch {
|
|
193
|
+
// ignore best-effort
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Конвертируем costUSDT -> contracts (дробное)
|
|
198
|
+
const last = (await this.fetchTicker(symbol)).last
|
|
199
|
+
const contracts = await this.quoteToBaseQty({ symbol, quoteCost: params.costUSDT, price: last })
|
|
200
|
+
const actualCostUSDT = await this.baseQtyToQuote({ symbol, baseQty: contracts, price: last })
|
|
201
|
+
|
|
202
|
+
console.log(
|
|
203
|
+
`[${this.id}] openFutures: ` +
|
|
204
|
+
`requested ${params.costUSDT} USDT -> ` +
|
|
205
|
+
`calculated ${contracts.toFixed(8)} contracts -> ` +
|
|
206
|
+
`notional ${actualCostUSDT.toFixed(2)} USDT`,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const side = params.side === 'long' ? 'buy' : 'sell'
|
|
210
|
+
const type: 'market' | 'limit' = typeof params.price === 'number' ? 'limit' : 'market'
|
|
211
|
+
const price = typeof params.price === 'number' ? this.roundPrice(symbol, params.price) : undefined
|
|
212
|
+
|
|
213
|
+
// DEBUG: выводим то, что передаём в createOrder
|
|
214
|
+
console.log(`[${this.id}] createOrder args: symbol=${symbol}, type=${type}, side=${side}, amount=${contracts}, price=${price}`)
|
|
215
|
+
|
|
216
|
+
// Создаём ордер с дробным количеством контрактов
|
|
217
|
+
const order: any = await (this.ex as any).createOrder(symbol, type, side, contracts, price, {
|
|
218
|
+
settle: this.settle,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
console.log(`[${this.id}] createOrder response: id=${order.id}, filled=${order.filled}, cost=${order.cost}, status=${order.status}`)
|
|
222
|
+
|
|
223
|
+
// fetchOrder чтобы получить точные filled values
|
|
224
|
+
const filledOrder: any = await (this.ex as any).fetchOrder(order.id, symbol).catch(() => order)
|
|
225
|
+
|
|
226
|
+
console.log(
|
|
227
|
+
`[${this.id}] fetchOrder response: id=${filledOrder.id}, filled=${filledOrder.filled}, cost=${filledOrder.cost}, average=${filledOrder.average}`,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return mapCcxtOrder(symbol, filledOrder)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async closeFutures(params: {
|
|
234
|
+
symbol: string
|
|
235
|
+
side: PositionSide
|
|
236
|
+
closeMode: 'all' | 'cost'
|
|
237
|
+
costUSDT?: number
|
|
238
|
+
}): Promise<OrderInfo> {
|
|
239
|
+
const symbol = params.symbol
|
|
240
|
+
const closeSide = params.side === 'long' ? 'sell' : 'buy'
|
|
241
|
+
|
|
242
|
+
if (params.closeMode === 'cost') {
|
|
243
|
+
if (!params.costUSDT) throw new Error('gate-futures closeMode=cost requires costUSDT')
|
|
244
|
+
|
|
245
|
+
const last = (await this.fetchTicker(symbol)).last
|
|
246
|
+
const contracts = await this.quoteToBaseQty({
|
|
247
|
+
symbol,
|
|
248
|
+
quoteCost: params.costUSDT,
|
|
249
|
+
price: last,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const order: any = await (this.ex as any).createOrder(symbol, 'market', closeSide, contracts, undefined, {
|
|
253
|
+
settle: this.settle,
|
|
254
|
+
reduceOnly: true,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const filledOrder: any = await (this.ex as any).fetchOrder(order.id, symbol).catch(() => order)
|
|
258
|
+
return mapCcxtOrder(symbol, filledOrder)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// closeMode === 'all': close entire position
|
|
262
|
+
const positions: any[] = await (this.ex as any).fetchPositions?.([symbol]).catch(() => []) ?? []
|
|
263
|
+
const p0: any = positions.find((p) => p?.symbol === symbol) ?? positions[0]
|
|
264
|
+
const rawSize = Math.abs(Number(p0?.contracts ?? p0?.size ?? p0?.positionAmt ?? 0))
|
|
265
|
+
|
|
266
|
+
if (!rawSize || rawSize <= 0) {
|
|
267
|
+
throw new Error(`gate-futures closeMode=all: no open position for ${symbol}`)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const order: any = await (this.ex as any).createOrder(symbol, 'market', closeSide, rawSize, undefined, {
|
|
271
|
+
settle: this.settle,
|
|
272
|
+
reduceOnly: true,
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const filledOrder: any = await (this.ex as any).fetchOrder(order.id, symbol).catch(() => order)
|
|
276
|
+
return mapCcxtOrder(symbol, filledOrder)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async fetchPositions(symbol?: string): Promise<PositionInfo[]> {
|
|
280
|
+
const positions: any[] = await (this.ex as any).fetchPositions?.(symbol ? [symbol] : undefined).catch(() => []) ?? []
|
|
281
|
+
if (!Array.isArray(positions)) return []
|
|
282
|
+
|
|
283
|
+
return positions.map((p: any) => {
|
|
284
|
+
const rawSize = Number(p?.contracts ?? p?.size ?? p?.positionAmt ?? 0)
|
|
285
|
+
const side: PositionSide =
|
|
286
|
+
p?.side === 'long' || p?.side === 'short'
|
|
287
|
+
? p.side
|
|
288
|
+
: rawSize >= 0
|
|
289
|
+
? 'long'
|
|
290
|
+
: 'short'
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
symbol: p?.symbol ?? symbol ?? '',
|
|
294
|
+
side,
|
|
295
|
+
size: Math.abs(rawSize),
|
|
296
|
+
entryPrice: Number(p?.entryPrice ?? p?.entry ?? 0),
|
|
297
|
+
raw: p,
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import ccxt, { type gateio as GateCcxt } from 'ccxt'
|
|
2
|
+
import type {
|
|
3
|
+
ExchangeAdapter,
|
|
4
|
+
ExchangeCapabilities,
|
|
5
|
+
MarketInfo,
|
|
6
|
+
OrderInfo,
|
|
7
|
+
PositionInfo,
|
|
8
|
+
} from '../../../core/exchange'
|
|
9
|
+
import { mapCcxtOrder } from './gate.mapOrder'
|
|
10
|
+
|
|
11
|
+
export interface GateSpotAdapterConfig {
|
|
12
|
+
id: string
|
|
13
|
+
label?: string
|
|
14
|
+
apiKey: string
|
|
15
|
+
secret: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class GateSpotAdapter implements ExchangeAdapter {
|
|
19
|
+
readonly id: string
|
|
20
|
+
readonly label: string
|
|
21
|
+
readonly capabilities: ExchangeCapabilities
|
|
22
|
+
private ex: GateCcxt
|
|
23
|
+
|
|
24
|
+
constructor(cfg: GateSpotAdapterConfig) {
|
|
25
|
+
this.id = cfg.id
|
|
26
|
+
this.label = cfg.label ?? 'gate-spot'
|
|
27
|
+
|
|
28
|
+
this.ex = new ccxt.gateio({
|
|
29
|
+
apiKey: cfg.apiKey,
|
|
30
|
+
secret: cfg.secret,
|
|
31
|
+
enableRateLimit: true,
|
|
32
|
+
options: {
|
|
33
|
+
defaultType: 'spot',
|
|
34
|
+
createMarketBuyOrderRequiresPrice: false,
|
|
35
|
+
},
|
|
36
|
+
}) as GateCcxt
|
|
37
|
+
|
|
38
|
+
this.capabilities = {
|
|
39
|
+
spot: { nativeTakeProfit: false, nativeStopLoss: false, nativeTrailingStop: false },
|
|
40
|
+
futures: { nativeTakeProfit: false, nativeStopLoss: false, nativeTrailingStop: false },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async loadMarkets(): Promise<void> {
|
|
45
|
+
await this.ex.loadMarkets()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
hasSpotMarket(symbol: string): boolean {
|
|
49
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
50
|
+
return !!m && m.type === 'spot'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
hasFuturesMarket(_symbol: string): boolean {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getMarket(symbol: string): MarketInfo | undefined {
|
|
58
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
59
|
+
if (!m) return undefined
|
|
60
|
+
return {
|
|
61
|
+
symbol: m.symbol,
|
|
62
|
+
base: m.base,
|
|
63
|
+
quote: m.quote,
|
|
64
|
+
type: 'spot',
|
|
65
|
+
precisionAmount: m.precision?.amount,
|
|
66
|
+
precisionPrice: m.precision?.price,
|
|
67
|
+
minCost: m.limits?.cost?.min,
|
|
68
|
+
minAmount: m.limits?.amount?.min,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private roundAmount(symbol: string, amount: number): number {
|
|
73
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
74
|
+
if (!m) return amount
|
|
75
|
+
return Number((this.ex as any).amountToPrecision(symbol, amount))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private roundPrice(symbol: string, price: number): number {
|
|
79
|
+
const m: any = (this.ex as any).markets?.[symbol]
|
|
80
|
+
if (!m) return price
|
|
81
|
+
return Number((this.ex as any).priceToPrecision(symbol, price))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async fetchTicker(symbol: string): Promise<{ last: number }> {
|
|
85
|
+
const t: any = await (this.ex as any).fetchTicker(symbol)
|
|
86
|
+
return { last: Number(t?.last ?? 0) }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async fetchBalance(): Promise<Record<string, { free: number; used: number; total: number }>> {
|
|
90
|
+
const bal: any = await (this.ex as any).fetchBalance()
|
|
91
|
+
return bal as any
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async fetchOpenOrders(symbol?: string): Promise<OrderInfo[]> {
|
|
95
|
+
const orders: any[] = await (this.ex as any).fetchOpenOrders(symbol)
|
|
96
|
+
return orders.map((o: any) => mapCcxtOrder(o.symbol, o))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async cancelOrder(symbol: string, orderId: string): Promise<void> {
|
|
100
|
+
await (this.ex as any).cancelOrder(orderId, symbol)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async quoteToBaseQty(params: { symbol: string; quoteCost: number; price?: number }): Promise<number> {
|
|
104
|
+
const { symbol, quoteCost } = params
|
|
105
|
+
if (!quoteCost || quoteCost <= 0) throw new Error(`gate-spot quoteCost must be > 0, got ${quoteCost}`)
|
|
106
|
+
|
|
107
|
+
const p = params.price ?? (await this.fetchTicker(symbol)).last
|
|
108
|
+
if (!p || p <= 0) throw new Error(`gate-spot invalid price for ${symbol}: ${p}`)
|
|
109
|
+
|
|
110
|
+
const m = this.getMarket(symbol)
|
|
111
|
+
|
|
112
|
+
// 1) первичное значение и округление вниз (как обычно делает ccxt)
|
|
113
|
+
let baseQty = this.roundAmount(symbol, quoteCost / p)
|
|
114
|
+
|
|
115
|
+
// 2) minAmount
|
|
116
|
+
if (m?.minAmount && baseQty < m.minAmount) {
|
|
117
|
+
baseQty = this.roundAmount(symbol, m.minAmount)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3) minCost (ключевой фикс): если из-за округления ниже minCost — увеличиваем на один шаг
|
|
121
|
+
if (m?.minCost) {
|
|
122
|
+
const cost = baseQty * p
|
|
123
|
+
if (cost + 1e-12 < m.minCost) {
|
|
124
|
+
// вычисляем 1 шаг по precision.amount
|
|
125
|
+
const prec = m.precisionAmount
|
|
126
|
+
if (typeof prec === 'number') {
|
|
127
|
+
const step = Math.pow(10, -prec)
|
|
128
|
+
baseQty = this.roundAmount(symbol, baseQty + step)
|
|
129
|
+
} else {
|
|
130
|
+
// fallback: небольшой буфер по quoteCost
|
|
131
|
+
baseQty = this.roundAmount(symbol, (quoteCost * 1.002) / p)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (baseQty * p + 1e-12 < m.minCost) {
|
|
135
|
+
throw new Error(`gate-spot violates minCost: ${baseQty * p} < ${m.minCost} ${m.quote}`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return baseQty
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async baseQtyToQuote(params: { symbol: string; baseQty: number; price?: number }): Promise<number> {
|
|
143
|
+
const qty = this.roundAmount(params.symbol, params.baseQty)
|
|
144
|
+
if (!qty || qty <= 0) return 0
|
|
145
|
+
const p = params.price ?? (await this.fetchTicker(params.symbol)).last
|
|
146
|
+
if (!p || p <= 0) throw new Error(`gate-spot invalid price for ${params.symbol}: ${p}`)
|
|
147
|
+
return qty * p
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// spot trading
|
|
151
|
+
async marketBuySpot(symbol: string, quoteCost: number): Promise<OrderInfo> {
|
|
152
|
+
const m = this.getMarket(symbol)
|
|
153
|
+
if (m?.minCost && quoteCost < m.minCost) {
|
|
154
|
+
throw new Error(`gate-spot buy violates minCost: ${quoteCost} < ${m.minCost} ${m.quote}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const order: any = await (this.ex as any).createMarketBuyOrderWithCost(symbol, quoteCost, {
|
|
158
|
+
createMarketBuyOrderRequiresPrice: false,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return mapCcxtOrder(symbol, order)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async marketSellSpot(symbol: string, baseAmount: number): Promise<OrderInfo> {
|
|
165
|
+
const amount = this.roundAmount(symbol, baseAmount)
|
|
166
|
+
if (!amount || amount <= 0) throw new Error(`gate-spot sell amount too small: ${amount}`)
|
|
167
|
+
|
|
168
|
+
const m = this.getMarket(symbol)
|
|
169
|
+
if (m?.minAmount && amount < m.minAmount) {
|
|
170
|
+
throw new Error(`gate-spot sell violates minAmount: ${amount} < ${m.minAmount} ${m.base}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const order: any = await (this.ex as any).createOrder(symbol, 'market', 'sell', amount)
|
|
174
|
+
return mapCcxtOrder(symbol, order)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async limitOrderSpot(symbol: string, side: 'buy' | 'sell', baseAmount: number, price: number): Promise<OrderInfo> {
|
|
178
|
+
const amount = this.roundAmount(symbol, baseAmount)
|
|
179
|
+
const p = this.roundPrice(symbol, price)
|
|
180
|
+
|
|
181
|
+
if (!amount || amount <= 0) throw new Error(`gate-spot limit amount too small: ${amount}`)
|
|
182
|
+
if (!p || p <= 0) throw new Error(`gate-spot limit price invalid: ${p}`)
|
|
183
|
+
|
|
184
|
+
const m = this.getMarket(symbol)
|
|
185
|
+
if (m?.minAmount && amount < m.minAmount) {
|
|
186
|
+
throw new Error(`gate-spot limit violates minAmount: ${amount} < ${m.minAmount} ${m.base}`)
|
|
187
|
+
}
|
|
188
|
+
if (m?.minCost && amount * p < m.minCost) {
|
|
189
|
+
throw new Error(`gate-spot limit violates minCost: ${amount * p} < ${m.minCost} ${m.quote}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const order: any = await (this.ex as any).createOrder(symbol, 'limit', side, amount, p)
|
|
193
|
+
return mapCcxtOrder(symbol, order)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// futures not supported
|
|
197
|
+
async openFutures(): Promise<OrderInfo> {
|
|
198
|
+
throw new Error('gate-spot: futures not supported')
|
|
199
|
+
}
|
|
200
|
+
async closeFutures(): Promise<OrderInfo> {
|
|
201
|
+
throw new Error('gate-spot: futures not supported')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// positions for spot: empty (НЕ используем ccxt Position typing)
|
|
205
|
+
async fetchPositions(): Promise<PositionInfo[]> {
|
|
206
|
+
return []
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OrderInfo } from '../../../core/exchange'
|
|
2
|
+
|
|
3
|
+
export function mapCcxtOrder(symbol: string, o: any): OrderInfo {
|
|
4
|
+
const price = o.price ?? o.average ?? o.avgPrice
|
|
5
|
+
|
|
6
|
+
// ВАЖНО: в ccxt amount обычно base, filled тоже base, cost = quote.
|
|
7
|
+
const amount =
|
|
8
|
+
typeof o.filled === 'number' && o.filled > 0 ? o.filled :
|
|
9
|
+
typeof o.amount === 'number' && o.amount > 0 ? o.amount :
|
|
10
|
+
0
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
id: String(o.id),
|
|
14
|
+
symbol,
|
|
15
|
+
side: o.side,
|
|
16
|
+
type: o.type,
|
|
17
|
+
price,
|
|
18
|
+
amount,
|
|
19
|
+
status: o.status,
|
|
20
|
+
raw: o,
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,138 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
import { RiskManager } from './core/RiskManager'
|
|
3
|
-
import {
|
|
4
|
-
type TakeProfitOrder,
|
|
5
|
-
type StopLossOrder,
|
|
6
|
-
type TrailingStopOrder,
|
|
7
|
-
} from './core/logicalOrders'
|
|
8
|
-
import { GateSpotAdapter } from './exchanges/GateSpotAdapter'
|
|
9
|
-
|
|
10
|
-
export interface FluxActionConfig {
|
|
11
|
-
okx?: {
|
|
12
|
-
apiKey: string
|
|
13
|
-
secret: string
|
|
14
|
-
passphrase: string
|
|
15
|
-
isDemo?: boolean
|
|
16
|
-
}
|
|
17
|
-
gate?: {
|
|
18
|
-
apiKey: string
|
|
19
|
-
secret: string
|
|
20
|
-
}
|
|
21
|
-
risk?: {
|
|
22
|
-
pollIntervalMs?: number
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class FluxAction {
|
|
27
|
-
private adapters: Record<string, ExchangeAdapter> = {}
|
|
28
|
-
private risk: RiskManager
|
|
29
|
-
|
|
30
|
-
constructor(cfg: FluxActionConfig) {
|
|
31
|
-
if (cfg.gate) {
|
|
32
|
-
const gate = new GateSpotAdapter({
|
|
33
|
-
id: 'gate-spot',
|
|
34
|
-
apiKey: cfg.gate.apiKey,
|
|
35
|
-
secret: cfg.gate.secret,
|
|
36
|
-
})
|
|
37
|
-
this.adapters[gate.id] = gate
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
this.risk = new RiskManager({
|
|
41
|
-
exchanges: this.adapters,
|
|
42
|
-
pollIntervalMs: cfg.risk?.pollIntervalMs,
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async init(): Promise<void> {
|
|
47
|
-
await Promise.all(Object.values(this.adapters).map((a) => a.loadMarkets()))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
getExchange(id: string): ExchangeAdapter | undefined {
|
|
51
|
-
return this.adapters[id]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async openSpotPosition(params: {
|
|
55
|
-
exchangeId: string
|
|
56
|
-
symbol: string
|
|
57
|
-
quoteCost: number
|
|
58
|
-
}): Promise<{ orderId: string; filledBase: number; avgPrice: number }> {
|
|
59
|
-
const ex = this.requireExchange(params.exchangeId)
|
|
60
|
-
const order = await ex.marketBuySpot(params.symbol, params.quoteCost)
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
orderId: order.id,
|
|
64
|
-
filledBase: order.amount,
|
|
65
|
-
avgPrice: order.price ?? 0,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async closeSpotMarket(params: {
|
|
70
|
-
exchangeId: string
|
|
71
|
-
symbol: string
|
|
72
|
-
baseQty: number
|
|
73
|
-
}): Promise<{ orderId: string }> {
|
|
74
|
-
const ex = this.requireExchange(params.exchangeId)
|
|
75
|
-
const order = await ex.marketSellSpot(params.symbol, params.baseQty)
|
|
76
|
-
return { orderId: order.id }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async placeSpotLimit(params: {
|
|
80
|
-
exchangeId: string
|
|
81
|
-
symbol: string
|
|
82
|
-
side: 'buy' | 'sell'
|
|
83
|
-
baseQty: number
|
|
84
|
-
price: number
|
|
85
|
-
}): Promise<{ orderId: string }> {
|
|
86
|
-
const ex = this.requireExchange(params.exchangeId)
|
|
87
|
-
const order = await ex.limitOrderSpot(
|
|
88
|
-
params.symbol,
|
|
89
|
-
params.side,
|
|
90
|
-
params.baseQty,
|
|
91
|
-
params.price,
|
|
92
|
-
)
|
|
93
|
-
return { orderId: order.id }
|
|
94
|
-
}
|
|
1
|
+
export { FluxAction, type FluxActionConfig } from './FluxAction'
|
|
95
2
|
|
|
96
|
-
async placeTakeProfit(o: TakeProfitOrder): Promise<{ id: string }> {
|
|
97
|
-
await this.risk.placeTakeProfit(o)
|
|
98
|
-
return { id: o.id }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async placeStopLoss(o: StopLossOrder): Promise<{ id: string }> {
|
|
102
|
-
await this.risk.placeStopLoss(o)
|
|
103
|
-
return { id: o.id }
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async placeTrailingStop(o: TrailingStopOrder): Promise<{ id: string }> {
|
|
107
|
-
await this.risk.placeTrailingStop(o)
|
|
108
|
-
return { id: o.id }
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async cancelLogical(id: string): Promise<void> {
|
|
112
|
-
await this.risk.cancel(id)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
getActiveLogicalOrders() {
|
|
116
|
-
return this.risk.getActiveOrders()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private requireExchange(id: string): ExchangeAdapter {
|
|
120
|
-
const ex = this.adapters[id]
|
|
121
|
-
if (!ex) throw new Error(`Exchange ${id} is not configured`)
|
|
122
|
-
return ex
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export { RiskManager, type RiskManagerConfig } from './core/RiskManager'
|
|
127
3
|
export type {
|
|
128
4
|
ExchangeAdapter,
|
|
129
5
|
ExchangeCapabilities,
|
|
130
6
|
MarketInfo,
|
|
131
7
|
OrderInfo,
|
|
132
8
|
PositionInfo,
|
|
9
|
+
PositionSide,
|
|
10
|
+
MarketType,
|
|
11
|
+
Side,
|
|
133
12
|
} from './core/exchange'
|
|
134
13
|
|
|
14
|
+
export type { OrderKind, MarketOrLimit, SpotSide } from './core/orders'
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
LogicalOrder,
|
|
18
|
+
TakeProfitOrder,
|
|
19
|
+
StopLossOrder,
|
|
20
|
+
TrailingStopOrder,
|
|
21
|
+
LogicalOrderKind,
|
|
22
|
+
MarketScope,
|
|
23
|
+
FuturesPositionSide,
|
|
24
|
+
ExitMode,
|
|
25
|
+
} from './core/logicalOrders'
|
|
26
|
+
|
|
27
|
+
export { GateClient } from './clients/GateClient'
|
|
135
28
|
export {
|
|
136
29
|
GateSpotAdapter,
|
|
137
30
|
type GateSpotAdapterConfig,
|
|
138
|
-
|
|
31
|
+
GateFuturesAdapter,
|
|
32
|
+
type GateFuturesAdapterConfig,
|
|
33
|
+
} from './exchanges/gate'
|