@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
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'
|
|
@@ -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/tsconfig.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"module": "
|
|
3
|
+
"target": "ES2019",
|
|
4
|
+
"module": "CommonJS",
|
|
5
5
|
"moduleResolution": "Node",
|
|
6
6
|
"declaration": true,
|
|
7
7
|
"outDir": "dist",
|
|
8
8
|
"rootDir": "src",
|
|
9
|
-
"strict": true,
|
|
10
9
|
"esModuleInterop": true,
|
|
11
|
-
"
|
|
10
|
+
"strict": true,
|
|
12
11
|
"skipLibCheck": true
|
|
13
12
|
},
|
|
14
13
|
"include": ["src"]
|
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
|
-
}
|