@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,56 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from '../core/exchange'
|
|
2
|
+
import type { LogicalOrder } from '../core/logicalOrders'
|
|
3
|
+
|
|
4
|
+
export class ExitExecutor {
|
|
5
|
+
async execute(ex: ExchangeAdapter, o: LogicalOrder, price: number): Promise<void> {
|
|
6
|
+
if (o.market === 'spot') {
|
|
7
|
+
await this.exitSpot(ex, o, price)
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
if (o.market === 'futures') {
|
|
11
|
+
await this.exitFutures(ex, o)
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
throw new Error(`Unknown market scope: ${(o as any).market}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private async exitSpot(ex: ExchangeAdapter, o: LogicalOrder, price: number) {
|
|
18
|
+
const side = o.side ?? 'sell'
|
|
19
|
+
const qty = o.qty ?? 0
|
|
20
|
+
if (qty <= 0) {
|
|
21
|
+
console.warn('[ExitExecutor] spot exit skipped: qty<=0', { id: o.id, symbol: o.symbol })
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const bal = await ex.fetchBalance()
|
|
26
|
+
const base = o.symbol.split('/')[0]
|
|
27
|
+
const free = bal[base]?.free ?? 0
|
|
28
|
+
const rawQty = Math.min(qty, free * 0.995)
|
|
29
|
+
|
|
30
|
+
if (rawQty <= 0) {
|
|
31
|
+
console.warn('[ExitExecutor] spot exit skipped: no free base', { id: o.id, symbol: o.symbol, base, free, qty })
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (side === 'sell') {
|
|
36
|
+
await ex.marketSellSpot(o.symbol, rawQty)
|
|
37
|
+
} else {
|
|
38
|
+
const quoteCost = price * rawQty
|
|
39
|
+
await ex.marketBuySpot(o.symbol, quoteCost)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async exitFutures(ex: ExchangeAdapter, o: LogicalOrder) {
|
|
44
|
+
const positionSide = o.positionSide
|
|
45
|
+
if (!positionSide) {
|
|
46
|
+
throw new Error(`[ExitExecutor] futures order missing positionSide: ${o.id}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const exitMode = o.exitMode ?? 'all'
|
|
50
|
+
await ex.closeFutures({
|
|
51
|
+
symbol: o.symbol,
|
|
52
|
+
side: positionSide,
|
|
53
|
+
closeMode: exitMode,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LogicalOrder } from '../core/logicalOrders'
|
|
2
|
+
|
|
3
|
+
export interface OrderStore {
|
|
4
|
+
set(o: LogicalOrder): void
|
|
5
|
+
delete(id: string): void
|
|
6
|
+
values(): LogicalOrder[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class InMemoryOrderStore implements OrderStore {
|
|
10
|
+
private orders = new Map<string, LogicalOrder>()
|
|
11
|
+
|
|
12
|
+
set(o: LogicalOrder) {
|
|
13
|
+
this.orders.set(o.id, o)
|
|
14
|
+
}
|
|
15
|
+
delete(id: string) {
|
|
16
|
+
this.orders.delete(id)
|
|
17
|
+
}
|
|
18
|
+
values(): LogicalOrder[] {
|
|
19
|
+
return [...this.orders.values()]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from '../core/exchange'
|
|
2
|
+
import type { LogicalOrder, TakeProfitOrder, StopLossOrder, TrailingStopOrder } from '../core/logicalOrders'
|
|
3
|
+
import type { OrderStore } from './OrderStore'
|
|
4
|
+
import { ExitExecutor } from './ExitExecutor'
|
|
5
|
+
|
|
6
|
+
export class RiskEngine {
|
|
7
|
+
private executor = new ExitExecutor()
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private store: OrderStore,
|
|
11
|
+
private exchanges: Record<string, ExchangeAdapter>,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
place(o: LogicalOrder) {
|
|
15
|
+
this.store.set(o)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
cancel(id: string) {
|
|
19
|
+
this.store.delete(id)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
list(): LogicalOrder[] {
|
|
23
|
+
return this.store.values()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async tick(): Promise<void> {
|
|
27
|
+
const grouped: Record<string, Record<string, LogicalOrder[]>> = {}
|
|
28
|
+
|
|
29
|
+
for (const o of this.store.values()) {
|
|
30
|
+
if (!grouped[o.exchangeId]) grouped[o.exchangeId] = {}
|
|
31
|
+
if (!grouped[o.exchangeId][o.symbol]) grouped[o.exchangeId][o.symbol] = []
|
|
32
|
+
grouped[o.exchangeId][o.symbol].push(o)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const [exId, bySymbol] of Object.entries(grouped)) {
|
|
36
|
+
const ex = this.exchanges[exId]
|
|
37
|
+
if (!ex) continue
|
|
38
|
+
|
|
39
|
+
for (const [symbol, orders] of Object.entries(bySymbol)) {
|
|
40
|
+
await this.processForSymbol(ex, symbol, orders)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async processForSymbol(ex: ExchangeAdapter, symbol: string, orders: LogicalOrder[]) {
|
|
46
|
+
const ticker = await ex.fetchTicker(symbol)
|
|
47
|
+
const price = ticker.last
|
|
48
|
+
if (!price || price <= 0) return
|
|
49
|
+
|
|
50
|
+
for (const o of orders) {
|
|
51
|
+
if (o.kind === 'take-profit') {
|
|
52
|
+
const tp = o as TakeProfitOrder
|
|
53
|
+
// MVP: TP условие только для "sell exit" логики (и spot, и futures)
|
|
54
|
+
if (price >= tp.triggerPrice) {
|
|
55
|
+
await this.executeAndClear(ex, o, price)
|
|
56
|
+
}
|
|
57
|
+
} else if (o.kind === 'stop-loss') {
|
|
58
|
+
const sl = o as StopLossOrder
|
|
59
|
+
if (price <= sl.triggerPrice) {
|
|
60
|
+
await this.executeAndClear(ex, o, price)
|
|
61
|
+
}
|
|
62
|
+
} else if (o.kind === 'trailing-stop') {
|
|
63
|
+
const ts = o as TrailingStopOrder
|
|
64
|
+
|
|
65
|
+
if (typeof ts.activationPrice === 'number' && ts.activationPrice > 0 && price < ts.activationPrice) {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!ts.highWatermark || price > ts.highWatermark) {
|
|
70
|
+
ts.highWatermark = price
|
|
71
|
+
this.store.set(ts)
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const drop = (ts.highWatermark - price) / ts.highWatermark
|
|
76
|
+
if (drop >= ts.callbackRate) {
|
|
77
|
+
await this.executeAndClear(ex, o, price)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async executeAndClear(ex: ExchangeAdapter, o: LogicalOrder, price: number) {
|
|
84
|
+
// 1) удалить триггернувшийся
|
|
85
|
+
this.store.delete(o.id)
|
|
86
|
+
|
|
87
|
+
// 2) удалить “похожие” exits по symbol/exchange (особенно важно для spot sell exits)
|
|
88
|
+
for (const ord of this.store.values()) {
|
|
89
|
+
if (ord.exchangeId === o.exchangeId && ord.symbol === o.symbol && ord.market === o.market) {
|
|
90
|
+
// если это один и тот же "exit stack" — удаляем
|
|
91
|
+
// (можно ужесточить фильтры позже: по positionSide / side)
|
|
92
|
+
if (ord.kind === 'take-profit' || ord.kind === 'stop-loss' || ord.kind === 'trailing-stop') {
|
|
93
|
+
this.store.delete(ord.id)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3) исполнить выход
|
|
99
|
+
await this.executor.execute(ex, o, price)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RiskEngine } from './RiskEngine'
|
|
2
|
+
|
|
3
|
+
export class RiskScheduler {
|
|
4
|
+
private timer: NodeJS.Timeout | null = null
|
|
5
|
+
|
|
6
|
+
constructor(
|
|
7
|
+
private engine: RiskEngine,
|
|
8
|
+
private pollIntervalMs: number,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
start() {
|
|
12
|
+
if (this.timer) return
|
|
13
|
+
this.timer = setInterval(() => {
|
|
14
|
+
this.engine.tick().catch((e) => console.error('[RiskScheduler] tick error', e))
|
|
15
|
+
}, this.pollIntervalMs)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
stop() {
|
|
19
|
+
if (!this.timer) return
|
|
20
|
+
clearInterval(this.timer)
|
|
21
|
+
this.timer = null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from '../core/exchange'
|
|
2
|
+
|
|
3
|
+
export class ExchangeRegistry {
|
|
4
|
+
private adapters: Record<string, ExchangeAdapter> = {}
|
|
5
|
+
|
|
6
|
+
register(ex: ExchangeAdapter) {
|
|
7
|
+
this.adapters[ex.id] = ex
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get(id: string): ExchangeAdapter | undefined {
|
|
11
|
+
return this.adapters[id]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
require(id: string): ExchangeAdapter {
|
|
15
|
+
const ex = this.adapters[id]
|
|
16
|
+
if (!ex) throw new Error(`Exchange ${id} is not configured`)
|
|
17
|
+
return ex
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
list(): ExchangeAdapter[] {
|
|
21
|
+
return Object.values(this.adapters)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
asRecord(): Record<string, ExchangeAdapter> {
|
|
25
|
+
return this.adapters
|
|
26
|
+
}
|
|
27
|
+
}
|
package/examples/main.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
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/src/core/RiskManager.ts
DELETED
|
@@ -1,205 +0,0 @@
|
|
|
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
|
-
}
|
package/src/core/results.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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
|
-
}
|