@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaflux/fluxaction",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Programmatic spot trading actions (TP/SL/trailing) for crypto exchanges",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "scripts": {
11
11
  "build": "tsc -p tsconfig.json",
12
- "start": "ts-node examples/main.ts"
12
+ "test:spot": "ts-node scripts/spot.ts",
13
+ "test:futures": "ts-node scripts/futures.ts"
13
14
  },
14
15
  "keywords": [
15
16
  "trading-bot",
@@ -23,7 +24,8 @@
23
24
  "author": "wancareri",
24
25
  "license": "MIT",
25
26
  "dependencies": {
26
- "ccxt": "^4.4.0"
27
+ "ccxt": "^4.4.0",
28
+ "dotenv": "^17.2.3"
27
29
  },
28
30
  "devDependencies": {
29
31
  "@types/node": "^22.0.0",
@@ -0,0 +1,80 @@
1
+ try { require('dotenv').config() } catch {}
2
+
3
+ import { FluxAction } from '../src'
4
+ import type { TakeProfitOrder, StopLossOrder, TrailingStopOrder } from '../src/core/logicalOrders'
5
+
6
+ async function main() {
7
+ const apiKey = process.env.GATE_API_KEY || ''
8
+ const secret = process.env.GATE_SECRET_KEY || ''
9
+ if (!apiKey || !secret) throw new Error('Missing GATE_API_KEY / GATE_SECRET_KEY')
10
+
11
+ const flux = new FluxAction({
12
+ gate: { apiKey, secret },
13
+ risk: { pollIntervalMs: 300 },
14
+ })
15
+
16
+ await flux.init()
17
+
18
+ const exchangeId = 'gate-futures'
19
+ const symbol = 'BTC/USDT:USDT'
20
+ const costUSDT = 15
21
+
22
+ const open = await flux.openFuturesPosition({
23
+ exchangeId,
24
+ symbol,
25
+ side: 'short',
26
+ costUSDT,
27
+ leverage: 3,
28
+ price: null, // market
29
+ })
30
+ console.log('[futures] opened', open)
31
+
32
+ const baseId = `${exchangeId}:${symbol}:${Date.now()}`
33
+
34
+ const tp: TakeProfitOrder = {
35
+ id: baseId + ':tp',
36
+ exchangeId,
37
+ market: 'futures',
38
+ symbol,
39
+ kind: 'take-profit',
40
+ triggerPrice: 10_000_000,
41
+ positionSide: 'long',
42
+ exitMode: 'all',
43
+ }
44
+
45
+ const sl: StopLossOrder = {
46
+ id: baseId + ':sl',
47
+ exchangeId,
48
+ market: 'futures',
49
+ symbol,
50
+ kind: 'stop-loss',
51
+ triggerPrice: 1,
52
+ positionSide: 'long',
53
+ exitMode: 'all',
54
+ }
55
+
56
+ const ts: TrailingStopOrder = {
57
+ id: baseId + ':ts',
58
+ exchangeId,
59
+ market: 'futures',
60
+ symbol,
61
+ kind: 'trailing-stop',
62
+ callbackRate: 0.03,
63
+ activationPrice: undefined,
64
+ highWatermark: undefined,
65
+ positionSide: 'long',
66
+ exitMode: 'all',
67
+ }
68
+
69
+ await flux.placeTakeProfit(tp)
70
+ await flux.placeStopLoss(sl)
71
+ await flux.placeTrailingStop(ts)
72
+
73
+ console.log('[futures] exits registered', flux.getActiveLogicalOrders().map((o) => o.id))
74
+ while (true) await new Promise((r) => setTimeout(r, 60_000))
75
+ }
76
+
77
+ main().catch((e) => {
78
+ console.error(e)
79
+ process.exit(1)
80
+ })
@@ -0,0 +1,84 @@
1
+ try { require('dotenv').config() } catch {}
2
+
3
+ import { FluxAction } from '../src'
4
+ import type { TakeProfitOrder, StopLossOrder, TrailingStopOrder } from '../src/core/logicalOrders'
5
+
6
+ async function main() {
7
+ const apiKey = process.env.GATE_API_KEY || ''
8
+ const secret = process.env.GATE_SECRET_KEY || ''
9
+ if (!apiKey || !secret) throw new Error('Missing GATE_API_KEY / GATE_SECRET_KEY')
10
+
11
+ const flux = new FluxAction({
12
+ gate: { apiKey, secret },
13
+ risk: { pollIntervalMs: 300 },
14
+ })
15
+
16
+ await flux.init()
17
+
18
+ const exchangeId = 'gate-spot'
19
+ const symbol = 'DOGE/USDT'
20
+ const quoteCost = 15
21
+
22
+ const open = await flux.openSpotPosition({ exchangeId, symbol, quoteCost })
23
+ console.log('[spot] opened', open)
24
+
25
+ const { baseQty } = await flux.quoteToBaseQty({ exchangeId, symbol, quoteCost: 3, price: open.avgPrice || undefined })
26
+ const limitOrder = await flux.placeSpotLimit({
27
+ exchangeId,
28
+ symbol,
29
+ side: 'sell',
30
+ baseQty,
31
+ price: (open.avgPrice || 0) * 1.1,
32
+ })
33
+ console.log('[spot] limit sell placed', limitOrder)
34
+
35
+ const qty = open.filledBase * 0.7
36
+ const baseId = `${exchangeId}:${symbol}:${Date.now()}`
37
+
38
+ const tp: TakeProfitOrder = {
39
+ id: baseId + ':tp',
40
+ exchangeId,
41
+ market: 'spot',
42
+ symbol,
43
+ side: 'sell',
44
+ qty,
45
+ kind: 'take-profit',
46
+ triggerPrice: open.avgPrice * 1.02,
47
+ }
48
+
49
+ const sl: StopLossOrder = {
50
+ id: baseId + ':sl',
51
+ exchangeId,
52
+ market: 'spot',
53
+ symbol,
54
+ side: 'sell',
55
+ qty,
56
+ kind: 'stop-loss',
57
+ triggerPrice: open.avgPrice * 0.95,
58
+ }
59
+
60
+ const ts: TrailingStopOrder = {
61
+ id: baseId + ':ts',
62
+ exchangeId,
63
+ market: 'spot',
64
+ symbol,
65
+ side: 'sell',
66
+ qty,
67
+ kind: 'trailing-stop',
68
+ callbackRate: 0.02,
69
+ activationPrice: open.avgPrice,
70
+ highWatermark: open.avgPrice,
71
+ }
72
+
73
+ await flux.placeTakeProfit(tp)
74
+ await flux.placeStopLoss(sl)
75
+ await flux.placeTrailingStop(ts)
76
+
77
+ console.log('[spot] exits registered', flux.getActiveLogicalOrders().map((o) => o.id))
78
+ while (true) await new Promise((r) => setTimeout(r, 60_000))
79
+ }
80
+
81
+ main().catch((e) => {
82
+ console.error(e)
83
+ process.exit(1)
84
+ })
@@ -0,0 +1,150 @@
1
+ import type { ExchangeAdapter, PositionSide } from './core/exchange'
2
+ import { ExchangeRegistry } from './services/ExchangeRegistry'
3
+ import { InMemoryOrderStore } from './risk/OrderStore'
4
+ import { RiskEngine } from './risk/RiskEngine'
5
+ import { RiskScheduler } from './risk/RiskScheduler'
6
+ import type {
7
+ TakeProfitOrder,
8
+ StopLossOrder,
9
+ TrailingStopOrder,
10
+ LogicalOrder,
11
+ } from './core/logicalOrders'
12
+ import { GateSpotAdapter, GateFuturesAdapter } from './exchanges/gate'
13
+ import { GateClient } from './clients/GateClient'
14
+
15
+ export interface FluxActionConfig {
16
+ gate?: { apiKey: string; secret: string }
17
+ gateFutures?: { enabled?: boolean; settle?: 'usdt' | 'btc'; defaultLeverage?: number }
18
+ risk?: { pollIntervalMs?: number; autoStart?: boolean }
19
+ }
20
+
21
+ export class FluxAction {
22
+ private registry = new ExchangeRegistry()
23
+ private riskEngine: RiskEngine
24
+ private riskScheduler: RiskScheduler
25
+
26
+ readonly gate: GateClient
27
+
28
+ constructor(cfg: FluxActionConfig) {
29
+ if (cfg.gate) {
30
+ this.registry.register(new GateSpotAdapter({
31
+ id: 'gate-spot',
32
+ apiKey: cfg.gate.apiKey,
33
+ secret: cfg.gate.secret,
34
+ }))
35
+
36
+ const wantFutures = cfg.gateFutures?.enabled ?? true
37
+ if (wantFutures) {
38
+ this.registry.register(new GateFuturesAdapter({
39
+ id: 'gate-futures',
40
+ apiKey: cfg.gate.apiKey,
41
+ secret: cfg.gate.secret,
42
+ settle: cfg.gateFutures?.settle ?? 'usdt',
43
+ defaultLeverage: cfg.gateFutures?.defaultLeverage,
44
+ }))
45
+ }
46
+ }
47
+
48
+ const store = new InMemoryOrderStore()
49
+ this.riskEngine = new RiskEngine(store, this.registry.asRecord())
50
+ this.riskScheduler = new RiskScheduler(this.riskEngine, cfg.risk?.pollIntervalMs ?? 3000)
51
+ if (cfg.risk?.autoStart) this.riskScheduler.start()
52
+
53
+ this.gate = new GateClient(this, { spot: 'gate-spot', futures: 'gate-futures' })
54
+ }
55
+
56
+ async init(): Promise<void> {
57
+ await Promise.all(this.registry.list().map((a) => a.loadMarkets()))
58
+ }
59
+
60
+ getExchange(id: string): ExchangeAdapter | undefined {
61
+ return this.registry.get(id)
62
+ }
63
+
64
+ // --------- converters ----------
65
+ async quoteToBaseQty(params: { exchangeId: string; symbol: string; quoteCost: number; price?: number }) {
66
+ const ex = this.registry.require(params.exchangeId)
67
+ const baseQty = await ex.quoteToBaseQty({ symbol: params.symbol, quoteCost: params.quoteCost, price: params.price })
68
+ return { baseQty }
69
+ }
70
+
71
+ async baseQtyToQuote(params: { exchangeId: string; symbol: string; baseQty: number; price?: number }) {
72
+ const ex = this.registry.require(params.exchangeId)
73
+ const quoteCost = await ex.baseQtyToQuote({ symbol: params.symbol, baseQty: params.baseQty, price: params.price })
74
+ return { quoteCost }
75
+ }
76
+
77
+ // --------- universal spot ----------
78
+ async openSpotPosition(params: { exchangeId: string; symbol: string; quoteCost: number }) {
79
+ const ex = this.registry.require(params.exchangeId)
80
+ const order = await ex.marketBuySpot(params.symbol, params.quoteCost)
81
+ return { orderId: order.id, filledBase: order.amount, avgPrice: order.price ?? 0 }
82
+ }
83
+
84
+ async closeSpotMarket(params: { exchangeId: string; symbol: string; baseQty: number }) {
85
+ const ex = this.registry.require(params.exchangeId)
86
+ const order = await ex.marketSellSpot(params.symbol, params.baseQty)
87
+ return { orderId: order.id }
88
+ }
89
+
90
+ async placeSpotLimit(params: { exchangeId: string; symbol: string; side: 'buy'|'sell'; baseQty: number; price: number }) {
91
+ const ex = this.registry.require(params.exchangeId)
92
+ const order = await ex.limitOrderSpot(params.symbol, params.side, params.baseQty, params.price)
93
+ return { orderId: order.id }
94
+ }
95
+
96
+ // --------- universal futures ----------
97
+ async openFuturesPosition(params: {
98
+ exchangeId: string
99
+ symbol: string
100
+ side: PositionSide
101
+ costUSDT: number
102
+ leverage?: number
103
+ price?: number | null // null/undefined => market
104
+ }) {
105
+ const ex = this.registry.require(params.exchangeId)
106
+ const order = await ex.openFutures({
107
+ symbol: params.symbol,
108
+ side: params.side,
109
+ costUSDT: params.costUSDT,
110
+ leverage: params.leverage,
111
+ price: params.price,
112
+ })
113
+ return { orderId: order.id }
114
+ }
115
+
116
+ async closeFuturesPosition(params: {
117
+ exchangeId: string
118
+ symbol: string
119
+ side: PositionSide
120
+ closeMode: 'all'|'cost'
121
+ costUSDT?: number
122
+ }) {
123
+ const ex = this.registry.require(params.exchangeId)
124
+ const order = await ex.closeFutures({
125
+ symbol: params.symbol,
126
+ side: params.side,
127
+ closeMode: params.closeMode,
128
+ costUSDT: params.costUSDT,
129
+ })
130
+ return { orderId: order.id }
131
+ }
132
+
133
+ // --------- risk ----------
134
+ async placeTakeProfit(o: TakeProfitOrder) { this.placeLogical(o); return { id: o.id } }
135
+ async placeStopLoss(o: StopLossOrder) { this.placeLogical(o); return { id: o.id } }
136
+ async placeTrailingStop(o: TrailingStopOrder) { this.placeLogical(o); return { id: o.id } }
137
+
138
+ async cancelLogical(id: string) { this.riskEngine.cancel(id) }
139
+ getActiveLogicalOrders(): LogicalOrder[] { return this.riskEngine.list() }
140
+
141
+ startRiskPolling() { this.riskScheduler.start() }
142
+ stopRiskPolling() { this.riskScheduler.stop() }
143
+
144
+ private placeLogical(o: LogicalOrder) {
145
+ const ex = this.registry.get(o.exchangeId)
146
+ if (!ex) throw new Error(`no exchange ${o.exchangeId}`)
147
+ this.riskEngine.place(o)
148
+ this.riskScheduler.start()
149
+ }
150
+ }
@@ -0,0 +1,52 @@
1
+ import type { PositionSide } from '../core/exchange'
2
+ import type { FluxAction } from '../FluxAction'
3
+
4
+ export class GateClient {
5
+ constructor(
6
+ private flux: FluxAction,
7
+ private ids: { spot: string; futures: string },
8
+ ) {}
9
+
10
+ // converters
11
+ quoteToBaseQty(params: { symbol: string; quoteCost: number; price?: number }) {
12
+ return this.flux.quoteToBaseQty({ exchangeId: this.ids.spot, ...params })
13
+ }
14
+
15
+ baseQtyToQuote(params: { symbol: string; baseQty: number; price?: number }) {
16
+ return this.flux.baseQtyToQuote({ exchangeId: this.ids.spot, ...params })
17
+ }
18
+
19
+ // spot
20
+ openSpotPosition(params: { symbol: string; quoteCost: number }) {
21
+ return this.flux.openSpotPosition({ exchangeId: this.ids.spot, ...params })
22
+ }
23
+
24
+ closeSpotMarket(params: { symbol: string; baseQty: number }) {
25
+ return this.flux.closeSpotMarket({ exchangeId: this.ids.spot, ...params })
26
+ }
27
+
28
+ placeSpotLimit(params: { symbol: string; side: 'buy' | 'sell'; baseQty: number; price: number }) {
29
+ return this.flux.placeSpotLimit({ exchangeId: this.ids.spot, ...params })
30
+ }
31
+
32
+ // futures
33
+ openFuturesPosition(params: {
34
+ symbol: string
35
+ side: PositionSide
36
+ costUSDT: number
37
+ leverage?: number
38
+ price?: number | null // null/undefined => market
39
+ }) {
40
+ return this.flux.openFuturesPosition({ exchangeId: this.ids.futures, ...params })
41
+ }
42
+
43
+ closeFuturesPosition(params: { symbol: string; side: PositionSide; closeMode?: 'all' | 'cost'; costUSDT?: number }) {
44
+ return this.flux.closeFuturesPosition({
45
+ exchangeId: this.ids.futures,
46
+ symbol: params.symbol,
47
+ side: params.side,
48
+ closeMode: params.closeMode ?? 'all',
49
+ costUSDT: params.costUSDT,
50
+ })
51
+ }
52
+ }
@@ -61,6 +61,11 @@ export interface ExchangeAdapter {
61
61
  fetchOpenOrders(symbol?: string): Promise<OrderInfo[]>
62
62
  cancelOrder(symbol: string, orderId: string): Promise<void>
63
63
 
64
+ // helpers
65
+ quoteToBaseQty(params: { symbol: string; quoteCost: number; price?: number }): Promise<number>
66
+ baseQtyToQuote(params: { symbol: string; baseQty: number; price?: number }): Promise<number>
67
+
68
+ // spot
64
69
  marketBuySpot(symbol: string, quoteCost: number): Promise<OrderInfo>
65
70
  marketSellSpot(symbol: string, baseAmount: number): Promise<OrderInfo>
66
71
  limitOrderSpot(
@@ -70,16 +75,19 @@ export interface ExchangeAdapter {
70
75
  price: number,
71
76
  ): Promise<OrderInfo>
72
77
 
78
+ // futures
73
79
  openFutures(params: {
74
80
  symbol: string
75
81
  side: PositionSide
76
82
  costUSDT: number
77
83
  leverage?: number
84
+ price?: number | null
78
85
  }): Promise<OrderInfo>
79
86
 
80
87
  closeFutures(params: {
81
88
  symbol: string
82
89
  side: PositionSide
90
+ closeMode: 'all' | 'cost'
83
91
  costUSDT?: number
84
92
  }): Promise<OrderInfo>
85
93
 
@@ -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
- side: 'buy' | 'sell'
8
- qty: number
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,7 @@
1
+ export type OrderKind = 'market' | 'limit'
2
+
3
+ export type MarketOrLimit =
4
+ | { kind: 'market' }
5
+ | { kind: 'limit'; price: number }
6
+
7
+ export type SpotSide = 'buy' | 'sell'