From 1386455870ff1a3859253c6648842ce717095e64 Mon Sep 17 00:00:00 2001 From: discountry Date: Tue, 23 Sep 2025 02:01:55 +0800 Subject: [PATCH] add presist --- .gitignore | 1 + src/core/maker-engine.ts | 61 ++++++++++-- src/core/trend-engine.ts | 66 +++++++++++-- src/exchanges/aster/client.ts | 168 ++++++++++++++++++++------------ src/exchanges/types.ts | 47 +++++++++ src/state/trade-log.ts | 9 +- src/ui/components/DataTable.tsx | 8 +- src/utils/persistence.ts | 37 +++++++ src/utils/strategy.ts | 2 +- 9 files changed, 319 insertions(+), 80 deletions(-) create mode 100644 src/utils/persistence.ts diff --git a/.gitignore b/.gitignore index a14702c..87ff300 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # dependencies (bun install) node_modules +data/* # output out diff --git a/src/core/maker-engine.ts b/src/core/maker-engine.ts index e9c78e3..879fc67 100644 --- a/src/core/maker-engine.ts +++ b/src/core/maker-engine.ts @@ -8,15 +8,14 @@ import type { } from "../exchanges/types"; import { toPrice1Decimal } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../state/trade-log"; +import { loadState, saveState } from "../utils/persistence"; import { getPosition, type PositionSnapshot } from "../utils/strategy"; import { marketClose, - OrderLockMap, - OrderPendingMap, - OrderTimerMap, placeOrder, unlockOperating, } from "./order-coordinator"; +import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator"; interface DesiredOrder { side: "BUY" | "SELL"; @@ -57,6 +56,10 @@ export class MakerEngine { private readonly tradeLog: ReturnType; private readonly listeners = new Map>(); + private readonly stateFile: string; + private savedStateApplied = false; + private lastPersistedAt = 0; + private savedOpenOrders: AsterOrder[] = []; private timer: ReturnType | null = null; private processing = false; @@ -65,7 +68,9 @@ export class MakerEngine { constructor(private readonly config: MakerConfig, private readonly exchange: ExchangeAdapter) { this.tradeLog = createTradeLog(this.config.maxLogEntries); + this.stateFile = `maker-${this.config.symbol}.json`; this.bootstrap(); + void this.restoreState(); } start(): void { @@ -113,7 +118,10 @@ export class MakerEngine { this.exchange.watchOrders((orders) => { this.syncLocksWithOrders(orders); - this.openOrders = Array.isArray(orders) ? orders.filter((order) => order.type !== "MARKET") : []; + this.openOrders = Array.isArray(orders) + ? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol) + : []; + this.reconcileSavedOpenOrders(); this.emitUpdate(); }); @@ -230,6 +238,7 @@ export class MakerEngine { for (const index of unmatched) { const target = targets[index]; + if (!target) continue; if (target.amount < EPS) continue; try { await placeOrder( @@ -297,8 +306,10 @@ export class MakerEngine { private emitUpdate(): void { const snapshot = this.buildSnapshot(); const handlers = this.listeners.get("update"); - if (!handlers) return; - handlers.forEach((handler) => handler(snapshot)); + if (handlers) { + handlers.forEach((handler) => handler(snapshot)); + } + void this.persistSnapshot(snapshot); } private buildSnapshot(): MakerEngineSnapshot { @@ -330,4 +341,42 @@ export class MakerEngine { lastUpdated: Date.now(), }; } + + private async restoreState(): Promise { + if (this.savedStateApplied) return; + this.savedStateApplied = true; + const state = await loadState<{ + tradeLog?: TradeLogEntry[]; + accountUnrealized?: number; + openOrders?: AsterOrder[]; + }>(this.stateFile); + if (!state) return; + if (Array.isArray(state.tradeLog)) this.tradeLog.replace(state.tradeLog); + if (typeof state.accountUnrealized === "number") this.accountUnrealized = state.accountUnrealized; + if (Array.isArray(state.openOrders)) this.savedOpenOrders = state.openOrders; + } + + private reconcileSavedOpenOrders(): void { + if (!this.savedOpenOrders.length) return; + const currentIds = new Set(this.openOrders.map((order) => order.orderId)); + const missing = this.savedOpenOrders.filter((order) => !currentIds.has(order.orderId)); + if (missing.length) { + this.tradeLog.push("order", `检测到 ${missing.length} 个历史挂单与当前状态不一致,将重新同步`); + } + this.savedOpenOrders = []; + } + + private async persistSnapshot(snapshot?: MakerEngineSnapshot): Promise { + const now = Date.now(); + if (now - this.lastPersistedAt < 1000) return; + this.lastPersistedAt = now; + const current = snapshot ?? this.buildSnapshot(); + await saveState(this.stateFile, { + tradeLog: current.tradeLog, + accountUnrealized: current.accountUnrealized, + openOrders: current.openOrders.filter((order) => order.symbol === this.config.symbol), + position: current.position, + timestamp: current.lastUpdated, + }); + } } diff --git a/src/core/trend-engine.ts b/src/core/trend-engine.ts index 04f52ae..6a885ba 100644 --- a/src/core/trend-engine.ts +++ b/src/core/trend-engine.ts @@ -16,16 +16,15 @@ import { } from "../utils/strategy"; import { marketClose, - OrderLockMap, - OrderPendingMap, - OrderTimerMap, placeMarketOrder, placeStopLossOrder, placeTrailingStopOrder, unlockOperating, } from "./order-coordinator"; +import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "./order-coordinator"; import { toPrice1Decimal } from "../utils/math"; import { createTradeLog, type TradeLogEntry } from "../state/trade-log"; +import { loadState, saveState } from "../utils/persistence"; export interface TrendEngineSnapshot { ready: boolean; @@ -75,12 +74,18 @@ export class TrendEngine { private totalProfit = 0; private totalTrades = 0; private lastOpenPlan: OpenOrderPlan = { side: null, price: null }; + private savedStateApplied = false; + private readonly stateFile: string; + private lastPersistedAt = 0; + private savedOpenOrders: AsterOrder[] = []; private readonly listeners = new Map>(); constructor(private readonly config: TradingConfig, private readonly exchange: ExchangeAdapter) { this.tradeLog = createTradeLog(this.config.maxLogEntries); + this.stateFile = `trend-${this.config.symbol}.json`; this.bootstrap(); + void this.restoreState(); } start(): void { @@ -124,8 +129,9 @@ export class TrendEngine { this.exchange.watchOrders((orders) => { this.synchronizeLocks(orders); this.openOrders = Array.isArray(orders) - ? orders.filter((order) => order.type !== "MARKET") + ? orders.filter((order) => order.type !== "MARKET" && order.symbol === this.config.symbol) : []; + this.reconcileSavedOpenOrders(); this.emitUpdate(); }); this.exchange.watchDepth(this.config.symbol, (depth) => { @@ -392,8 +398,10 @@ export class TrendEngine { private emitUpdate(): void { const snapshot = this.buildSnapshot(); const handlers = this.listeners.get("update"); - if (!handlers) return; - handlers.forEach((handler) => handler(snapshot)); + if (handlers) { + handlers.forEach((handler) => handler(snapshot)); + } + void this.persistSnapshot(snapshot); } private buildSnapshot(): TrendEngineSnapshot { @@ -431,4 +439,50 @@ export class TrendEngine { lastOpenSignal: this.lastOpenPlan, }; } + + private async restoreState(): Promise { + if (this.savedStateApplied) return; + this.savedStateApplied = true; + const state = await loadState<{ + totalProfit?: number; + totalTrades?: number; + lastOpenPlan?: OpenOrderPlan; + tradeLog?: TradeLogEntry[]; + openOrders?: AsterOrder[]; + }>(this.stateFile); + if (!state) return; + if (typeof state.totalProfit === "number") this.totalProfit = state.totalProfit; + if (typeof state.totalTrades === "number") this.totalTrades = state.totalTrades; + if (state.lastOpenPlan) this.lastOpenPlan = state.lastOpenPlan; + if (Array.isArray(state.tradeLog)) this.tradeLog.replace(state.tradeLog); + if (Array.isArray(state.openOrders)) this.savedOpenOrders = state.openOrders; + } + + private reconcileSavedOpenOrders(): void { + if (!this.savedOpenOrders.length) return; + const currentIds = new Set(this.openOrders.map((order) => order.orderId)); + const missing = this.savedOpenOrders.filter((order) => !currentIds.has(order.orderId)); + if (missing.length) { + this.tradeLog.push( + "order", + `检测到 ${missing.length} 个历史挂单与当前状态不符,将按策略逻辑重新挂单` + ); + } + this.savedOpenOrders = []; + } + + private async persistSnapshot(snapshot: TrendEngineSnapshot): Promise { + const now = Date.now(); + if (now - this.lastPersistedAt < 1000) return; + this.lastPersistedAt = now; + await saveState(this.stateFile, { + totalProfit: this.totalProfit, + totalTrades: this.totalTrades, + lastOpenPlan: this.lastOpenPlan, + tradeLog: snapshot.tradeLog, + openOrders: snapshot.openOrders.filter((order) => order.symbol === this.config.symbol), + position: snapshot.position, + timestamp: snapshot.lastUpdated, + }); + } } diff --git a/src/exchanges/aster/client.ts b/src/exchanges/aster/client.ts index 20ae5f2..ce502b6 100644 --- a/src/exchanges/aster/client.ts +++ b/src/exchanges/aster/client.ts @@ -7,6 +7,7 @@ import type { AsterOrder, AsterTicker, CreateOrderParams, + PositionSide, } from "../types"; const REST_BASE = "https://fapi.asterdex.com"; @@ -93,18 +94,18 @@ function fromRestKline(entry: any[], interval: string, symbol: string): AsterKli symbol, interval, openTime: entry[0], - open: entry[1], - high: entry[2], - low: entry[3], - close: entry[4], - volume: entry[5], + open: String(entry[1]), + high: String(entry[2]), + low: String(entry[3]), + close: String(entry[4]), + volume: String(entry[5]), closeTime: entry[6], - quoteAssetVolume: entry[7], - numberOfTrades: entry[8], - takerBuyBaseAssetVolume: entry[9], - takerBuyQuoteAssetVolume: entry[10], + quoteAssetVolume: String(entry[7]), + numberOfTrades: Number(entry[8] ?? 0), + takerBuyBaseAssetVolume: String(entry[9] ?? "0"), + takerBuyQuoteAssetVolume: String(entry[10] ?? "0"), isClosed: Boolean(entry[11]), - } as AsterKline; + }; } function toOrderFromRest(raw: any): AsterOrder { @@ -243,13 +244,22 @@ export class AsterRestClient { async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise { const upper = symbol.toUpperCase(); const url = `${REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`; - const response = await fetch(url); + let response: Response; + try { + response = await fetch(url); + } catch (error) { + throw new Error(`[AsterRestClient] 获取K线失败 ${String(error)}`); + } + const text = await response.text(); if (!response.ok) { - const text = await response.text(); throw new Error(`HTTP ${response.status} ${text}`); } - const payload = (await response.json()) as any[]; - return payload.map((entry) => fromRestKline(entry, interval, upper)); + try { + const payload = JSON.parse(text) as any[]; + return payload.map((entry) => fromRestKline(entry, interval, upper)); + } catch (error) { + throw new Error(`[AsterRestClient] 无法解析K线响应: ${text.slice(0, 200)}`); + } } async getListenKey(): Promise { @@ -278,16 +288,26 @@ export class AsterRestClient { "Content-Type": "application/x-www-form-urlencoded", }, }; - const response = await fetch(url, init); + let response: Response; + try { + response = await fetch(url, init); + } catch (error) { + throw new Error(`[AsterRestClient] 请求失败 ${String(error)}`); + } + const text = await response.text(); if (!response.ok) { - const text = await response.text(); throw new Error(`HTTP ${response.status} ${text}`); } - return (await response.json()) as T; + try { + return JSON.parse(text) as T; + } catch (error) { + throw new Error(`[AsterRestClient] 无法解析响应: ${text.slice(0, 200)}`); + } } private serialize(params: Record): string { return Object.keys(params) + .filter((key) => params[key] !== undefined && params[key] !== null) .sort() .map((key) => `${key}=${encodeURIComponent(String(params[key]))}`) .join("&"); @@ -374,7 +394,17 @@ export class AsterPublicStreams { } }; this.ws.onmessage = (event) => { - const payload = typeof event.data === "string" ? JSON.parse(event.data) : event.data; + let payload: any; + if (typeof event.data === "string") { + try { + payload = JSON.parse(event.data); + } catch (error) { + console.error("[AsterPublicStreams] 无法解析消息", error, event.data); + return; + } + } else { + payload = event.data; + } if (!payload) return; if (payload.result !== undefined) return; // subscription ack const data = payload.data ?? payload; @@ -456,6 +486,7 @@ export class AsterUserStream { private reconnectTimeout: ReturnType | null = null; private readonly accountEvent = new SimpleEvent<{ eventTime: number; payload: AccountUpdatePayload }>(); private readonly orderEvent = new SimpleEvent<{ eventTime: number; payload: OrderUpdatePayload }>(); + private readonly connectEvent = new SimpleEvent(); private isRunning = false; constructor(rest: AsterRestClient) { @@ -470,6 +501,10 @@ export class AsterUserStream { this.orderEvent.add(listener); } + onConnect(listener: () => void): void { + this.connectEvent.add(listener); + } + async start(): Promise { if (this.isRunning) return; this.isRunning = true; @@ -517,10 +552,20 @@ export class AsterUserStream { const url = `${WS_LISTEN_KEY_URL}${this.listenKey}`; this.ws = new WebSocket(url); this.ws.onopen = () => { - // no-op + this.connectEvent.emit(); }; this.ws.onmessage = (event) => { - const payload = typeof event.data === "string" ? JSON.parse(event.data) : event.data; + let payload: any; + if (typeof event.data === "string") { + try { + payload = JSON.parse(event.data); + } catch (error) { + console.error("[AsterUserStream] 无法解析消息", error, event.data); + return; + } + } else { + payload = event.data; + } if (!payload) return; if (payload === "ping") { this.ws?.send("pong"); @@ -572,30 +617,20 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e const balances = event.payload.B ?? []; for (const balance of balances) { const asset = balance.a; - let existing = next.assets.find((item) => item.asset === asset); - if (!existing) { - existing = { + let assetEntry = next.assets.find((item) => item.asset === asset); + if (!assetEntry) { + assetEntry = { asset, walletBalance: "0", - unrealizedProfit: "0", - marginBalance: "0", - maintMargin: "0", - initialMargin: "0", - positionInitialMargin: "0", - openOrderInitialMargin: "0", - crossWalletBalance: "0", - crossUnPnl: "0", availableBalance: "0", - maxWithdrawAmount: "0", - marginAvailable: true, updateTime: event.eventTime, - } as any; - next.assets.push(existing); + }; + next.assets.push(assetEntry); } - if (balance.wb !== undefined) existing.walletBalance = balance.wb; - if (balance.cw !== undefined) existing.crossWalletBalance = balance.cw; - if (balance.bc !== undefined) existing.availableBalance = balance.bc; - existing.updateTime = event.eventTime; + if (balance.wb !== undefined) assetEntry.walletBalance = balance.wb; + if (balance.cw !== undefined) assetEntry.crossWalletBalance = balance.cw; + if (balance.bc !== undefined) assetEntry.availableBalance = balance.bc; + assetEntry.updateTime = event.eventTime; } const positions = event.payload.P ?? []; @@ -604,29 +639,22 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e for (const position of positions) { const symbol = position.s; - let existing = next.positions.find((item) => item.symbol === symbol && item.positionSide === position.ps); - if (!existing) { - existing = { + let positionEntry = next.positions.find((item) => item.symbol === symbol && item.positionSide === (position.ps as PositionSide)); + if (!positionEntry) { + positionEntry = { symbol, positionAmt: "0", entryPrice: "0", unrealizedProfit: "0", - positionSide: position.ps, + positionSide: position.ps as PositionSide, updateTime: event.eventTime, - initialMargin: "0", - maintMargin: "0", - positionInitialMargin: "0", - openOrderInitialMargin: "0", - leverage: "", - isolated: position.mt === "isolated", - maxNotional: "0", - } as any; - next.positions.push(existing); + }; + next.positions.push(positionEntry); } - existing.positionAmt = position.pa ?? existing.positionAmt; - existing.entryPrice = position.ep ?? existing.entryPrice; - existing.unrealizedProfit = position.up ?? existing.unrealizedProfit; - existing.updateTime = event.eventTime; + positionEntry.positionAmt = position.pa ?? positionEntry.positionAmt; + positionEntry.entryPrice = position.ep ?? positionEntry.entryPrice; + positionEntry.unrealizedProfit = position.up ?? positionEntry.unrealizedProfit; + positionEntry.updateTime = event.eventTime; } return next; } @@ -675,20 +703,18 @@ export class AsterGateway { mergeOrderSnapshot(this.openOrders, order); this.ordersEvent.emit(Array.from(this.openOrders.values())); }); + this.userStream.onConnect(() => { + void this.refreshSnapshots(); + }); } async ensureInitialized(symbol: string): Promise { if (this.initialized) return; if (this.initializing) return this.initializing; this.initializing = (async () => { - this.accountSnapshot = await this.rest.getAccount(); - const orders = await this.rest.getOpenOrders(); - this.openOrders.clear(); - orders.forEach((order) => mergeOrderSnapshot(this.openOrders, order)); + await this.refreshSnapshots(); this.initialized = true; await this.userStream.start(); - this.accountEvent.emit(this.accountSnapshot!); - this.ordersEvent.emit(Array.from(this.openOrders.values())); })().catch((error) => { this.initializing = null; throw error; @@ -811,6 +837,24 @@ export class AsterGateway { this.klineRefreshTimers.set(key, timer); } + private async refreshSnapshots(): Promise { + try { + const account = await this.rest.getAccount(); + this.accountSnapshot = account; + this.accountEvent.emit(account); + } catch (error) { + console.error("[AsterGateway] 刷新账户信息失败", error); + } + try { + const orders = await this.rest.getOpenOrders(); + this.openOrders.clear(); + orders.forEach((order) => mergeOrderSnapshot(this.openOrders, order)); + this.ordersEvent.emit(Array.from(this.openOrders.values())); + } catch (error) { + console.error("[AsterGateway] 刷新挂单失败", error); + } + } + getAccountSnapshot(): AsterAccountSnapshot | null { return this.accountSnapshot; } diff --git a/src/exchanges/types.ts b/src/exchanges/types.ts index 3a92ae6..8a5d58e 100644 --- a/src/exchanges/types.ts +++ b/src/exchanges/types.ts @@ -30,6 +30,13 @@ export interface AsterAccountPosition { unrealizedProfit: string; positionSide: PositionSide; updateTime: number; + initialMargin?: string; + maintMargin?: string; + positionInitialMargin?: string; + openOrderInitialMargin?: string; + leverage?: string; + isolated?: boolean; + maxNotional?: string; } export interface AsterAccountAsset { @@ -37,6 +44,16 @@ export interface AsterAccountAsset { walletBalance: string; availableBalance: string; updateTime: number; + unrealizedProfit?: string; + marginBalance?: string; + maintMargin?: string; + initialMargin?: string; + positionInitialMargin?: string; + openOrderInitialMargin?: string; + crossWalletBalance?: string; + crossUnPnl?: string; + maxWithdrawAmount?: string; + marginAvailable?: boolean; } export interface AsterAccountSnapshot { @@ -46,6 +63,15 @@ export interface AsterAccountSnapshot { updateTime: number; totalWalletBalance: string; totalUnrealizedProfit: string; + totalMarginBalance?: string; + totalInitialMargin?: string; + totalMaintMargin?: string; + totalPositionInitialMargin?: string; + totalOpenOrderInitialMargin?: string; + totalCrossWalletBalance?: string; + totalCrossUnPnl?: string; + availableBalance?: string; + maxWithdrawAmount?: string; positions: AsterAccountPosition[]; assets: AsterAccountAsset[]; } @@ -60,6 +86,9 @@ export interface AsterDepth { bids: AsterDepthLevel[]; asks: AsterDepthLevel[]; eventTime?: number; + eventType?: string; + tradeTime?: number; + symbol?: string; } export interface AsterTicker { @@ -71,6 +100,16 @@ export interface AsterTicker { volume: string; quoteVolume: string; eventTime?: number; + eventType?: string; + priceChange?: string; + priceChangePercent?: string; + weightedAvgPrice?: string; + lastQty?: string; + openTime?: number; + closeTime?: number; + firstId?: number; + lastId?: number; + count?: number; } export interface AsterKline { @@ -110,4 +149,12 @@ export interface AsterOrder { reduceOnly: boolean; closePosition: boolean; workingType?: string; + avgPrice?: string; + cumQuote?: string; + origType?: string; + positionSide?: PositionSide; + timeInForce?: TimeInForce; + activatePrice?: string; + priceRate?: string; + priceProtect?: boolean; } diff --git a/src/state/trade-log.ts b/src/state/trade-log.ts index 2ef187c..e461bcb 100644 --- a/src/state/trade-log.ts +++ b/src/state/trade-log.ts @@ -4,8 +4,8 @@ export interface TradeLogEntry { detail: string; } -export function createTradeLog(maxEntries: number) { - const entries: TradeLogEntry[] = []; +export function createTradeLog(maxEntries: number, seed: TradeLogEntry[] = []) { + const entries: TradeLogEntry[] = seed.slice(-maxEntries); function push(type: string, detail: string) { entries.push({ time: new Date().toLocaleString(), type, detail }); if (entries.length > maxEntries) { @@ -15,5 +15,8 @@ export function createTradeLog(maxEntries: number) { function all() { return entries; } - return { push, all }; + function replace(next: TradeLogEntry[]) { + entries.splice(0, entries.length, ...next.slice(-maxEntries)); + } + return { push, all, replace }; } diff --git a/src/ui/components/DataTable.tsx b/src/ui/components/DataTable.tsx index 9e5e82a..751336f 100644 --- a/src/ui/components/DataTable.tsx +++ b/src/ui/components/DataTable.tsx @@ -45,7 +45,10 @@ export function DataTable>({ columns, rows } {columns - .map((col, index) => pad(col.header, widths[index], col.align ?? "left")) + .map((col, index) => { + const width = widths[index] ?? col.header.length; + return pad(col.header, width, col.align ?? "left"); + }) .join(" ")} {rows.map((row, rowIndex) => ( @@ -54,7 +57,8 @@ export function DataTable>({ columns, rows } .map((col, index) => { const align = col.align ?? "left"; const cell = formatCell(row[col.key]); - return pad(cell, widths[index], align); + const width = widths[index] ?? Math.max(col.header.length, cell.length); + return pad(cell, width, align); }) .join(" ")} diff --git a/src/utils/persistence.ts b/src/utils/persistence.ts new file mode 100644 index 0000000..41f1138 --- /dev/null +++ b/src/utils/persistence.ts @@ -0,0 +1,37 @@ +import fs from "fs/promises"; +import path from "path"; + +const DATA_DIR = path.resolve(process.cwd(), "data"); +let dataDirReady = false; + +async function ensureDataDir(): Promise { + if (dataDirReady) return; + await fs.mkdir(DATA_DIR, { recursive: true }).catch(() => undefined); + dataDirReady = true; +} + +export async function loadState(fileName: string): Promise { + try { + await ensureDataDir(); + const filePath = path.join(DATA_DIR, fileName); + const data = await fs.readFile(filePath, "utf8"); + return JSON.parse(data) as T; + } catch (error: any) { + if (error?.code === "ENOENT") return null; + console.error(`[state] load ${fileName} failed`, error); + return null; + } +} + +export async function saveState(fileName: string, state: unknown): Promise { + try { + await ensureDataDir(); + const filePath = path.join(DATA_DIR, fileName); + const tempPath = `${filePath}.tmp`; + const payload = JSON.stringify(state, null, 2); + await fs.writeFile(tempPath, payload, "utf8"); + await fs.rename(tempPath, filePath); + } catch (error) { + console.error(`[state] save ${fileName} failed`, error); + } +} diff --git a/src/utils/strategy.ts b/src/utils/strategy.ts index 505e3a2..ab60bdd 100644 --- a/src/utils/strategy.ts +++ b/src/utils/strategy.ts @@ -1,4 +1,4 @@ -import { AsterAccountSnapshot, AsterKline } from "../exchanges/types"; +import type { AsterAccountSnapshot, AsterKline } from "../exchanges/types"; export interface PositionSnapshot { positionAmt: number;