@metaflux/fluxaction 0.1.3 → 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/dist/core/RiskManager.js +9 -3
- package/dist/core/exchange.js +2 -1
- package/dist/core/logicalOrders.js +2 -1
- package/dist/core/results.js +2 -1
- package/dist/exchanges/GateSpotAdapter.js +29 -15
- package/dist/index.js +17 -9
- package/package.json +5 -4
- 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/tsconfig.json +3 -4
- 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
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
export type LogicalOrderKind = 'take-profit' | 'stop-loss' | 'trailing-stop'
|
|
2
|
+
export type MarketScope = 'spot' | 'futures'
|
|
3
|
+
export type FuturesPositionSide = 'long' | 'short'
|
|
4
|
+
|
|
5
|
+
export type ExitMode = 'all' // MVP: закрываем всю позицию по symbol
|
|
2
6
|
|
|
3
7
|
export interface LogicalOrderBase {
|
|
4
8
|
id: string
|
|
5
9
|
exchangeId: string
|
|
10
|
+
market: MarketScope
|
|
6
11
|
symbol: string
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
|
|
13
|
+
kind: LogicalOrderKind
|
|
14
|
+
|
|
15
|
+
// trigger side semantics:
|
|
16
|
+
// - spot: side 'sell' обычно означает выход из long spot позиции
|
|
17
|
+
// - futures: мы используем positionSide + exitMode
|
|
18
|
+
side?: 'buy' | 'sell'
|
|
19
|
+
|
|
20
|
+
// spot
|
|
21
|
+
qty?: number // base qty
|
|
22
|
+
|
|
23
|
+
// futures
|
|
24
|
+
positionSide?: FuturesPositionSide
|
|
25
|
+
exitMode?: ExitMode
|
|
9
26
|
}
|
|
10
27
|
|
|
11
28
|
export interface TakeProfitOrder extends LogicalOrderBase {
|
|
@@ -20,12 +37,9 @@ export interface StopLossOrder extends LogicalOrderBase {
|
|
|
20
37
|
|
|
21
38
|
export interface TrailingStopOrder extends LogicalOrderBase {
|
|
22
39
|
kind: 'trailing-stop'
|
|
23
|
-
callbackRate: number
|
|
40
|
+
callbackRate: number
|
|
24
41
|
activationPrice?: number
|
|
25
42
|
highWatermark?: number
|
|
26
43
|
}
|
|
27
44
|
|
|
28
|
-
export type LogicalOrder =
|
|
29
|
-
| TakeProfitOrder
|
|
30
|
-
| StopLossOrder
|
|
31
|
-
| TrailingStopOrder
|
|
45
|
+
export type LogicalOrder = TakeProfitOrder | StopLossOrder | TrailingStopOrder
|
|
@@ -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
|
+
}
|