@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/src/index.ts CHANGED
@@ -1,138 +1,33 @@
1
- import type { ExchangeAdapter } from './core/exchange'
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
- } from './exchanges/GateSpotAdapter'
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": "ES2020",
4
- "module": "ES2020",
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
- "resolveJsonModule": true,
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
- })
@@ -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
- }