@metaflux/fluxaction 0.1.3

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.
@@ -0,0 +1,77 @@
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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@metaflux/fluxaction",
3
+ "version": "0.1.3",
4
+ "description": "Programmatic spot trading actions (TP/SL/trailing) for crypto exchanges",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc -p tsconfig.json",
13
+ "start": "ts-node examples/main.ts"
14
+ },
15
+ "keywords": [
16
+ "trading-bot",
17
+ "crypto",
18
+ "ccxt",
19
+ "risk-management",
20
+ "take-profit",
21
+ "stop-loss",
22
+ "trailing-stop"
23
+ ],
24
+ "author": "wancareri",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "ccxt": "^4.4.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "ts-node": "^10.9.2",
32
+ "typescript": "^5.6.0"
33
+ }
34
+ }
@@ -0,0 +1,205 @@
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
+ }
@@ -0,0 +1,87 @@
1
+ export type MarketType = 'spot' | 'futures'
2
+ export type Side = 'buy' | 'sell'
3
+ export type PositionSide = 'long' | 'short'
4
+
5
+ export interface MarketInfo {
6
+ symbol: string
7
+ base: string
8
+ quote: string
9
+ type: MarketType
10
+ precisionAmount?: number
11
+ precisionPrice?: number
12
+ minCost?: number
13
+ minAmount?: number
14
+ }
15
+
16
+ export interface OrderInfo {
17
+ id: string
18
+ symbol: string
19
+ side: Side
20
+ type: 'market' | 'limit'
21
+ price?: number
22
+ amount: number
23
+ status: string
24
+ raw: any
25
+ }
26
+
27
+ export interface PositionInfo {
28
+ symbol: string
29
+ side: PositionSide
30
+ size: number
31
+ entryPrice: number
32
+ raw: any
33
+ }
34
+
35
+ export interface ExchangeCapabilities {
36
+ spot: {
37
+ nativeTakeProfit: boolean
38
+ nativeStopLoss: boolean
39
+ nativeTrailingStop: boolean
40
+ }
41
+ futures: {
42
+ nativeTakeProfit: boolean
43
+ nativeStopLoss: boolean
44
+ nativeTrailingStop: boolean
45
+ }
46
+ }
47
+
48
+ export interface ExchangeAdapter {
49
+ readonly id: string
50
+ readonly label: string
51
+ readonly capabilities: ExchangeCapabilities
52
+
53
+ loadMarkets(): Promise<void>
54
+
55
+ hasSpotMarket(symbol: string): boolean
56
+ hasFuturesMarket(symbol: string): boolean
57
+ getMarket(symbol: string): MarketInfo | undefined
58
+
59
+ fetchTicker(symbol: string): Promise<{ last: number }>
60
+ fetchBalance(): Promise<Record<string, { free: number; used: number; total: number }>>
61
+ fetchOpenOrders(symbol?: string): Promise<OrderInfo[]>
62
+ cancelOrder(symbol: string, orderId: string): Promise<void>
63
+
64
+ marketBuySpot(symbol: string, quoteCost: number): Promise<OrderInfo>
65
+ marketSellSpot(symbol: string, baseAmount: number): Promise<OrderInfo>
66
+ limitOrderSpot(
67
+ symbol: string,
68
+ side: Side,
69
+ baseAmount: number,
70
+ price: number,
71
+ ): Promise<OrderInfo>
72
+
73
+ openFutures(params: {
74
+ symbol: string
75
+ side: PositionSide
76
+ costUSDT: number
77
+ leverage?: number
78
+ }): Promise<OrderInfo>
79
+
80
+ closeFutures(params: {
81
+ symbol: string
82
+ side: PositionSide
83
+ costUSDT?: number
84
+ }): Promise<OrderInfo>
85
+
86
+ fetchPositions?(symbol?: string): Promise<PositionInfo[]>
87
+ }
@@ -0,0 +1,31 @@
1
+ export type LogicalOrderKind = 'take-profit' | 'stop-loss' | 'trailing-stop'
2
+
3
+ export interface LogicalOrderBase {
4
+ id: string
5
+ exchangeId: string
6
+ symbol: string
7
+ side: 'buy' | 'sell'
8
+ qty: number
9
+ }
10
+
11
+ export interface TakeProfitOrder extends LogicalOrderBase {
12
+ kind: 'take-profit'
13
+ triggerPrice: number
14
+ }
15
+
16
+ export interface StopLossOrder extends LogicalOrderBase {
17
+ kind: 'stop-loss'
18
+ triggerPrice: number
19
+ }
20
+
21
+ export interface TrailingStopOrder extends LogicalOrderBase {
22
+ kind: 'trailing-stop'
23
+ callbackRate: number
24
+ activationPrice?: number
25
+ highWatermark?: number
26
+ }
27
+
28
+ export type LogicalOrder =
29
+ | TakeProfitOrder
30
+ | StopLossOrder
31
+ | TrailingStopOrder
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,193 @@
1
+ import ccxt, { type gateio as GateCcxt } from 'ccxt'
2
+ import {
3
+ type ExchangeAdapter,
4
+ type ExchangeCapabilities,
5
+ type MarketInfo,
6
+ type OrderInfo,
7
+ type PositionInfo,
8
+ } from '../core/exchange'
9
+
10
+ export interface GateSpotAdapterConfig {
11
+ id: string
12
+ label?: string
13
+ apiKey: string
14
+ secret: string
15
+ }
16
+
17
+ export class GateSpotAdapter implements ExchangeAdapter {
18
+ readonly id: string
19
+ readonly label: string
20
+ readonly capabilities: ExchangeCapabilities
21
+ private ex: GateCcxt
22
+
23
+ constructor(cfg: GateSpotAdapterConfig) {
24
+ this.id = cfg.id
25
+ this.label = cfg.label ?? 'gate-spot'
26
+
27
+ this.ex = new ccxt.gateio({
28
+ apiKey: cfg.apiKey,
29
+ secret: cfg.secret,
30
+ enableRateLimit: true,
31
+ options: {
32
+ defaultType: 'spot',
33
+ createMarketBuyOrderRequiresPrice: false,
34
+ },
35
+ }) as GateCcxt
36
+
37
+ this.capabilities = {
38
+ spot: {
39
+ nativeTakeProfit: false,
40
+ nativeStopLoss: false,
41
+ nativeTrailingStop: false,
42
+ },
43
+ futures: {
44
+ nativeTakeProfit: false,
45
+ nativeStopLoss: false,
46
+ nativeTrailingStop: false,
47
+ },
48
+ }
49
+ }
50
+
51
+ async loadMarkets(): Promise<void> {
52
+ await this.ex.loadMarkets()
53
+ }
54
+
55
+ hasSpotMarket(symbol: string): boolean {
56
+ const m = this.ex.markets?.[symbol]
57
+ return !!m && m.type === 'spot'
58
+ }
59
+
60
+ hasFuturesMarket(): boolean {
61
+ return false
62
+ }
63
+
64
+ getMarket(symbol: string): MarketInfo | undefined {
65
+ const m = this.ex.markets?.[symbol]
66
+ if (!m) return undefined
67
+ return {
68
+ symbol: m.symbol,
69
+ base: m.base,
70
+ quote: m.quote,
71
+ type: m.type as any,
72
+ precisionAmount: m.precision?.amount,
73
+ precisionPrice: m.precision?.price,
74
+ minCost: m.limits?.cost?.min,
75
+ minAmount: m.limits?.amount?.min,
76
+ }
77
+ }
78
+
79
+ private roundAmount(symbol: string, amount: number): number {
80
+ const m = this.ex.markets?.[symbol]
81
+ if (!m) return amount
82
+ return Number(this.ex.amountToPrecision(symbol, amount))
83
+ }
84
+
85
+ private roundPrice(symbol: string, price: number): number {
86
+ const m = this.ex.markets?.[symbol]
87
+ if (!m) return price
88
+ return Number(this.ex.priceToPrecision(symbol, price))
89
+ }
90
+
91
+ private mapOrder(symbol: string, o: any): OrderInfo {
92
+ const price = o.price ?? o.average ?? o.avgPrice
93
+
94
+ let baseAmount: number
95
+ if (price && o.cost) {
96
+ baseAmount = o.cost / price
97
+ } else if (price && o.amount) {
98
+ baseAmount = o.amount / price
99
+ } else if (typeof o.amount === 'number' && o.amount > 0) {
100
+ baseAmount = o.amount
101
+ } else {
102
+ baseAmount = 0
103
+ }
104
+
105
+ return {
106
+ id: o.id,
107
+ symbol,
108
+ side: o.side,
109
+ type: o.type,
110
+ price,
111
+ amount: baseAmount,
112
+ status: o.status,
113
+ raw: o,
114
+ }
115
+ }
116
+
117
+ async fetchTicker(symbol: string): Promise<{ last: number }> {
118
+ const t = await this.ex.fetchTicker(symbol)
119
+ return { last: t.last ?? 0 }
120
+ }
121
+
122
+ async fetchBalance() {
123
+ const bal = await this.ex.fetchBalance()
124
+ return bal as any
125
+ }
126
+
127
+ async fetchOpenOrders(symbol?: string): Promise<OrderInfo[]> {
128
+ const orders = await this.ex.fetchOpenOrders(symbol)
129
+ return orders.map((o: any) => this.mapOrder(o.symbol, o))
130
+ }
131
+
132
+ async cancelOrder(symbol: string, orderId: string): Promise<void> {
133
+ await this.ex.cancelOrder(orderId, symbol)
134
+ }
135
+
136
+ async marketBuySpot(symbol: string, quoteCost: number): Promise<OrderInfo> {
137
+ const ticker = await this.fetchTicker(symbol)
138
+ if (!ticker.last || ticker.last <= 0) {
139
+ throw new Error(`gate-spot invalid price for ${symbol}: ${ticker.last}`)
140
+ }
141
+
142
+ const m = this.getMarket(symbol)
143
+ if (m?.minCost && quoteCost < m.minCost) {
144
+ throw new Error(
145
+ `gate-spot buy violates minCost: ${quoteCost} < ${m.minCost} ${m.quote}`,
146
+ )
147
+ }
148
+
149
+ const order = await this.ex.createMarketBuyOrderWithCost(symbol, quoteCost, {
150
+ createMarketBuyOrderRequiresPrice: false,
151
+ })
152
+ return this.mapOrder(symbol, order)
153
+ }
154
+
155
+ async marketSellSpot(symbol: string, baseAmount: number): Promise<OrderInfo> {
156
+ const amount = this.roundAmount(symbol, baseAmount)
157
+ if (!amount || amount <= 0) {
158
+ throw new Error(`gate-spot sell amount too small for ${symbol}: ${amount}`)
159
+ }
160
+
161
+ const order = await this.ex.createOrder(symbol, 'market', 'sell', amount)
162
+ return this.mapOrder(symbol, order)
163
+ }
164
+
165
+ async limitOrderSpot(
166
+ symbol: string,
167
+ side: 'buy' | 'sell',
168
+ baseAmount: number,
169
+ price: number,
170
+ ): Promise<OrderInfo> {
171
+ const amount = this.roundAmount(symbol, baseAmount)
172
+ const p = this.roundPrice(symbol, price)
173
+
174
+ if (!amount || amount <= 0) {
175
+ throw new Error(`gate-spot limit amount too small for ${symbol}: ${amount}`)
176
+ }
177
+
178
+ const order = await this.ex.createOrder(symbol, 'limit', side, amount, p)
179
+ return this.mapOrder(symbol, order)
180
+ }
181
+
182
+ async openFutures(): Promise<OrderInfo> {
183
+ throw new Error('gate-spot: futures not supported')
184
+ }
185
+
186
+ async closeFutures(): Promise<OrderInfo> {
187
+ throw new Error('gate-spot: futures not supported')
188
+ }
189
+
190
+ async fetchPositions(): Promise<PositionInfo[]> {
191
+ return []
192
+ }
193
+ }