mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-12 17:58:07 +00:00
Compare commits
11
Commits
eb70bab8c5
...
6b5f2542a4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b5f2542a4 | ||
|
|
4e69b81da2 | ||
|
|
345ad11ba3 | ||
|
|
d925fd93ee | ||
|
|
e81d1396a2 | ||
|
|
20855c2d1d | ||
|
|
d799cf79c2 | ||
|
|
bc29b23027 | ||
|
|
fb906be27c | ||
|
|
fda6bcad1d | ||
|
|
230c0d5e18 |
@@ -36,3 +36,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
|
|
||||||
.tmp
|
.tmp
|
||||||
.tmp/*
|
.tmp/*
|
||||||
|
|
||||||
|
.factory
|
||||||
@@ -1,35 +1,142 @@
|
|||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## Package Manager
|
||||||
The Bun entry point (`index.ts`) lives at the repo root for quick CLI smoke checks. All production code is under `src/`:
|
|
||||||
|
|
||||||
- `src/strategy/` gathers every live trading engine (`maker`, `offset-maker`, `trend`). Shared helpers for strategy wiring sit in `src/strategy/common/`.
|
**必须使用 Bun** — 本项目使用 Bun 作为包管理器和运行时。所有命令都必须用 bun 执行,**不要使用 npm、yarn 或 npx**。
|
||||||
- `src/core/` keeps order coordination plus shared libs used by multiple strategies.
|
|
||||||
- `src/exchanges/` exposes the adapters and REST/websocket clients.
|
|
||||||
- `src/ui/` implements the Ink dashboards for each strategy (they remain independent dashboards).
|
|
||||||
- `src/logging/` contains the trade log helper.
|
|
||||||
- `src/utils/` and `src/config.ts` hold cross-cutting utilities and runtime config.
|
|
||||||
- `docs/` stores reference material, while `tests/` contains Vitest suites.
|
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Project Structure
|
||||||
- `bun install` – install dependencies.
|
|
||||||
- `bun run index.ts` – launch the CLI menu.
|
|
||||||
- `bun x vitest run` – execute the full test suite; `bun x vitest --watch` for incremental runs.
|
|
||||||
|
|
||||||
Strategy-specific scripts still execute through the CLI; there is no separate `legacy/` workspace.
|
入口 `index.ts` 在仓库根目录,用于 CLI 启动。所有生产代码在 `src/` 下:
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
```
|
||||||
Use modern TypeScript with ES modules, two-space indentation, and sorted imports (external → internal). Favor `camelCase` for variables/functions and `PascalCase` for classes/enums. Place new strategies under `src/strategy/`, shared utilities under `src/utils/` or `src/strategy/common/` when they only apply to strategies. Keep comments focused on non-obvious trading logic.
|
src/
|
||||||
|
├── config.ts # 运行时配置(GridConfig、MakerPointsConfig 等)
|
||||||
|
├── runtime-errors.ts # 全局错误处理
|
||||||
|
├── index.tsx # Ink 渲染入口
|
||||||
|
├── cli/ # CLI 参数解析与策略启动
|
||||||
|
│ ├── args.ts
|
||||||
|
│ ├── command-executor.ts
|
||||||
|
│ ├── command-parser.ts
|
||||||
|
│ ├── command-types.ts
|
||||||
|
│ └── strategy-runner.ts
|
||||||
|
├── core/ # 订单协调 + 共享库
|
||||||
|
│ ├── order-coordinator.ts # 限价单/市价单统一下单(锁、去重、速率控制)
|
||||||
|
│ └── lib/
|
||||||
|
│ ├── order-plan.ts
|
||||||
|
│ ├── orders.ts
|
||||||
|
│ └── rate-limit.ts
|
||||||
|
├── exchanges/ # 交易所适配器(每个子目录含 adapter/gateway/order)
|
||||||
|
│ ├── adapter.ts # ExchangeAdapter 接口
|
||||||
|
│ ├── adapter-utils.ts # 适配器工具函数
|
||||||
|
│ ├── types.ts # CreateOrderParams、Order、AccountSnapshot 等公共类型
|
||||||
|
│ ├── order-schema.ts # BaseOrderIntent、LimitOrderIntent 等
|
||||||
|
│ ├── order-handlers.ts # 通用 order handler 工厂
|
||||||
|
│ ├── order-router.ts # 按交易所分发订单处理
|
||||||
|
│ ├── create-adapter.ts # 工厂函数
|
||||||
|
│ ├── resolve-from-env.ts
|
||||||
|
│ ├── dry-run-adapter.ts # 模拟适配器
|
||||||
|
│ ├── aster/ # Aster 交易所
|
||||||
|
│ ├── backpack/ # Backpack 交易所
|
||||||
|
│ ├── binance/ # Binance 交易所(CCXT)
|
||||||
|
│ ├── grvt/ # GRVT 交易所
|
||||||
|
│ ├── lighter/ # Lighter 交易所(含签名/nonce/字节处理)
|
||||||
|
│ ├── nado/ # Nado 交易所
|
||||||
|
│ ├── paradex/ # Paradex 交易所
|
||||||
|
│ └── standx/ # StandX 交易所
|
||||||
|
├── strategy/ # 所有策略引擎
|
||||||
|
│ ├── maker-engine.ts # Maker 做市策略
|
||||||
|
│ ├── maker-points-engine.ts # MakerPoints 积分做市策略
|
||||||
|
│ ├── maker-points-logic.ts # MakerPoints 纯逻辑(可单独测试)
|
||||||
|
│ ├── offset-maker-engine.ts # 偏移做市策略
|
||||||
|
│ ├── trend-engine.ts # 趋势跟踪策略
|
||||||
|
│ ├── grid-engine.ts # 网格交易策略(LevelState + clientOrderId 恢复)
|
||||||
|
│ ├── basis-arb-engine.ts # 基差套利策略
|
||||||
|
│ ├── swing-engine.ts # 波段交易策略
|
||||||
|
│ ├── swing-logic.ts # 波段纯逻辑
|
||||||
|
│ ├── guardian-engine.ts # 监控守护策略
|
||||||
|
│ ├── liquidity-maker-engine.ts # 流动性做市策略
|
||||||
|
│ └── common/ # 策略共享辅助
|
||||||
|
│ ├── binance-depth.ts # Binance 深度分析
|
||||||
|
│ ├── binance-rsi.ts # Binance RSI 指标
|
||||||
|
│ ├── event-emitter.ts # 策略事件发射器
|
||||||
|
│ ├── grid-storage.ts # 网格状态磁盘持久化
|
||||||
|
│ ├── session-volume.ts # 会话成交量统计
|
||||||
|
│ └── subscriptions.ts # WebSocket 安全订阅
|
||||||
|
├── ui/ # Ink 仪表盘(每个策略独立 App)
|
||||||
|
│ ├── App.tsx
|
||||||
|
│ ├── GridApp.tsx
|
||||||
|
│ ├── MakerApp.tsx
|
||||||
|
│ ├── MakerPointsApp.tsx
|
||||||
|
│ ├── OffsetMakerApp.tsx
|
||||||
|
│ ├── TrendApp.tsx
|
||||||
|
│ ├── BasisApp.tsx
|
||||||
|
│ ├── SwingApp.tsx
|
||||||
|
│ ├── GuardianApp.tsx
|
||||||
|
│ ├── LiquidityMakerApp.tsx
|
||||||
|
│ └── components/
|
||||||
|
├── logging/ # 交易日志
|
||||||
|
│ └── trade-log.ts
|
||||||
|
├── notifications/ # Telegram 通知
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── telegram.ts
|
||||||
|
│ └── types.ts
|
||||||
|
├── i18n/ # 国际化
|
||||||
|
│ └── index.ts
|
||||||
|
└── utils/ # 跨领域工具
|
||||||
|
├── math.ts, format.ts, price.ts, depth.ts
|
||||||
|
├── errors.ts, risk.ts, pnl.ts
|
||||||
|
├── strategy.ts, order-status.ts, security.ts
|
||||||
|
├── copyright.ts
|
||||||
|
└── standx-token-expiry.ts
|
||||||
|
```
|
||||||
|
|
||||||
## Testing Guidelines
|
辅助目录:
|
||||||
Vitest powers unit/integration tests. Co-locate new tests next to their subject using `<feature>.test.ts`. Strategies should have coverage for order lifecycle, risk guards, and websocket edge cases. Run `bun x vitest --watch` during development for fast feedback.
|
- `tests/` — Vitest 测试套件(大部分测试在此,少量与逻辑共处于 `src/`)
|
||||||
|
- `docs/` — 交易所 API 参考文档
|
||||||
|
- `scripts/` — 一次性脚本(密钥派生等)
|
||||||
|
- `skills/` — AI 助手技能配置
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commands
|
||||||
Follow lightweight Conventional Commits (e.g. `feat: add hedging status panel`). Scope each commit to a single module or strategy. PRs should include:
|
|
||||||
- Summary of changes.
|
```bash
|
||||||
- Validation notes (commands run, environments touched).
|
bun install # 安装依赖
|
||||||
- Relevant logs or screenshots for behavior changes.
|
bun run dev # 启动 CLI 菜单(等同于 bun run index.ts)
|
||||||
Link issues/tasks when available.
|
bun run test # 执行全部测试(bun x vitest run)
|
||||||
|
bun x vitest --watch # 增量测试
|
||||||
|
bun x tsc --noEmit # 类型检查(无 typecheck 脚本,直接用 tsc)
|
||||||
|
bun run lint # oxlint 检查
|
||||||
|
bun run lint:fix # oxlint 自动修复
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **不要用 `bun test`** — 那是 Bun 内置 runner,不支持 `vi.resetModules()`、`vi.waitFor()` 等 Vitest API。必须用 `bun run test`。
|
||||||
|
|
||||||
|
## Coding Style
|
||||||
|
|
||||||
|
- 现代 TypeScript + ES modules,严格模式(`strict: true`)
|
||||||
|
- 两空格缩进,imports 按 external → internal 排序
|
||||||
|
- 变量/函数用 `camelCase`,类/枚举用 `PascalCase`
|
||||||
|
- 新策略放 `src/strategy/`,策略共享工具放 `src/strategy/common/`
|
||||||
|
- 跨领域工具放 `src/utils/`
|
||||||
|
- 注释只写非显而易见的交易逻辑
|
||||||
|
- JSX 用于 Ink UI 组件(`react-jsx`)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Vitest 驱动所有测试。测试文件两种位置:
|
||||||
|
- `tests/<feature>.test.ts` — 大部分测试
|
||||||
|
- `src/<module>/<feature>.test.ts` — 纯逻辑单元测试(如 `maker-points-logic.test.ts`、`swing-logic.test.ts`)
|
||||||
|
|
||||||
|
策略测试应覆盖:订单生命周期、风控守卫、WebSocket 边界情况、状态恢复。
|
||||||
|
|
||||||
|
## Commit Guidelines
|
||||||
|
|
||||||
|
Conventional Commits(`feat:` / `fix:` / `refactor:` / `test:`),每次提交作用域限单个模块或策略。
|
||||||
|
|
||||||
## Environment & Secrets
|
## Environment & Secrets
|
||||||
Duplicate `.env.example` to `.env` and populate API keys (Aster/GRVT) before running strategies. Do not commit secrets—use local `.env` or deployment secret managers. Rotate keys if they leak into logs or backups.
|
|
||||||
|
复制 `.env.example` 为 `.env` 填入 API 密钥。**不要读取、打印或提交 `.env` 和任何密钥。** 泄露立即轮换。
|
||||||
|
|
||||||
|
## Safety
|
||||||
|
|
||||||
|
- 永远不要读取、运行、打印或访问 `.env` 或任何 secrets
|
||||||
|
- 代码中遇到项目依赖时,先查版本再查文档,避免过时知识导致错误
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
# RitMEX Bot - Claude Instructions
|
|
||||||
|
|
||||||
## Package Manager
|
|
||||||
|
|
||||||
**必须使用 Bun** - 这个项目使用 Bun 作为包管理器和运行时。所有能用 bun 执行的命令都必须使用 bun:
|
|
||||||
|
|
||||||
- 安装依赖: `bun install`
|
|
||||||
- 运行脚本: `bun run <script>`
|
|
||||||
- 执行测试: `bun test`
|
|
||||||
- 类型检查: `bun run typecheck`
|
|
||||||
|
|
||||||
**不要使用 npm、yarn 或 npx**
|
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
routeTrailingStopOrder,
|
routeTrailingStopOrder,
|
||||||
} from "../exchanges/order-router";
|
} from "../exchanges/order-router";
|
||||||
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
|
||||||
import type { AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../exchanges/types";
|
import type { Depth, Kline, Order, Ticker } from "../exchanges/types";
|
||||||
import { startStrategy } from "./strategy-runner";
|
import { startStrategy } from "./strategy-runner";
|
||||||
import type {
|
import type {
|
||||||
CommandErrorPayload,
|
CommandErrorPayload,
|
||||||
@@ -266,7 +266,7 @@ async function handleMarketTicker(
|
|||||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||||
const ticker = await waitForFirst<AsterTicker>(
|
const ticker = await waitForFirst<Ticker>(
|
||||||
(cb) => adapter.watchTicker(symbol, cb),
|
(cb) => adapter.watchTicker(symbol, cb),
|
||||||
command.timeoutMs,
|
command.timeoutMs,
|
||||||
"market ticker"
|
"market ticker"
|
||||||
@@ -279,7 +279,7 @@ async function handleMarketDepth(
|
|||||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||||
const depth = await waitForFirst<AsterDepth>(
|
const depth = await waitForFirst<Depth>(
|
||||||
(cb) => adapter.watchDepth(symbol, cb),
|
(cb) => adapter.watchDepth(symbol, cb),
|
||||||
command.timeoutMs,
|
command.timeoutMs,
|
||||||
"market depth"
|
"market depth"
|
||||||
@@ -300,7 +300,7 @@ async function handleMarketKline(
|
|||||||
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
buildAdapterFromEnvFn: typeof buildAdapterFromEnv
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
const { adapter, exchange, symbol } = createAdapterContext(command, buildAdapterFromEnvFn);
|
||||||
const klines = await waitForFirst<AsterKline[]>(
|
const klines = await waitForFirst<Kline[]>(
|
||||||
(cb) => adapter.watchKlines(symbol, command.interval, cb),
|
(cb) => adapter.watchKlines(symbol, command.interval, cb),
|
||||||
command.timeoutMs,
|
command.timeoutMs,
|
||||||
"market kline"
|
"market kline"
|
||||||
@@ -379,7 +379,7 @@ async function handleOrderCreate(
|
|||||||
timeInForce: payload.timeInForce,
|
timeInForce: payload.timeInForce,
|
||||||
};
|
};
|
||||||
|
|
||||||
let order: AsterOrder;
|
let order: Order;
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case "limit":
|
case "limit":
|
||||||
order = await routeLimitOrder({
|
order = await routeLimitOrder({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AsterOrder } from "../../exchanges/types";
|
import type { Order } from "../../exchanges/types";
|
||||||
|
|
||||||
export interface OrderTarget {
|
export interface OrderTarget {
|
||||||
side: "BUY" | "SELL";
|
side: "BUY" | "SELL";
|
||||||
@@ -8,11 +8,11 @@ export interface OrderTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function makeOrderPlan(
|
export function makeOrderPlan(
|
||||||
openOrders: AsterOrder[],
|
openOrders: Order[],
|
||||||
targets: OrderTarget[]
|
targets: OrderTarget[]
|
||||||
): { toCancel: AsterOrder[]; toPlace: OrderTarget[] } {
|
): { toCancel: Order[]; toPlace: OrderTarget[] } {
|
||||||
const unmatched = new Set(targets.map((_, idx) => idx));
|
const unmatched = new Set(targets.map((_, idx) => idx));
|
||||||
const toCancel: AsterOrder[] = [];
|
const toCancel: Order[] = [];
|
||||||
|
|
||||||
for (const order of openOrders) {
|
for (const order of openOrders) {
|
||||||
const orderPrice = String(order.price);
|
const orderPrice = String(order.price);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { ExchangeAdapter } from "../../exchanges/adapter";
|
import type { ExchangeAdapter } from "../../exchanges/adapter";
|
||||||
import type { AsterOrder } from "../../exchanges/types";
|
import type { Order } from "../../exchanges/types";
|
||||||
import { isUnknownOrderError } from "../../utils/errors";
|
import { isUnknownOrderError } from "../../utils/errors";
|
||||||
|
|
||||||
export async function safeCancelOrder(
|
export async function safeCancelOrder(
|
||||||
exchange: ExchangeAdapter,
|
exchange: ExchangeAdapter,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
order: AsterOrder,
|
order: Order,
|
||||||
onResolved: (orderId: number | string) => void,
|
onResolved: (orderId: number | string) => void,
|
||||||
onUnknown: () => void,
|
onUnknown: () => void,
|
||||||
onError: (err: unknown) => void
|
onError: (err: unknown) => void
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type { AsterOrder } from "../exchanges/types";
|
import type { Order } from "../exchanges/types";
|
||||||
import {
|
import {
|
||||||
routeCloseOrder,
|
routeCloseOrder,
|
||||||
routeLimitOrder,
|
routeLimitOrder,
|
||||||
@@ -88,7 +88,7 @@ export function unlockOperating(
|
|||||||
export async function deduplicateOrders(
|
export async function deduplicateOrders(
|
||||||
adapter: ExchangeAdapter,
|
adapter: ExchangeAdapter,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
openOrders: AsterOrder[],
|
openOrders: Order[],
|
||||||
locks: OrderLockMap,
|
locks: OrderLockMap,
|
||||||
timers: OrderTimerMap,
|
timers: OrderTimerMap,
|
||||||
pendings: OrderPendingMap,
|
pendings: OrderPendingMap,
|
||||||
@@ -134,12 +134,13 @@ type PlaceOrderOptions = {
|
|||||||
skipDedupe?: boolean;
|
skipDedupe?: boolean;
|
||||||
slPrice?: number;
|
slPrice?: number;
|
||||||
tpPrice?: number;
|
tpPrice?: number;
|
||||||
|
clientOrderId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function placeOrder(
|
export async function placeOrder(
|
||||||
adapter: ExchangeAdapter,
|
adapter: ExchangeAdapter,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
openOrders: AsterOrder[],
|
openOrders: Order[],
|
||||||
locks: OrderLockMap,
|
locks: OrderLockMap,
|
||||||
timers: OrderTimerMap,
|
timers: OrderTimerMap,
|
||||||
pendings: OrderPendingMap,
|
pendings: OrderPendingMap,
|
||||||
@@ -150,7 +151,7 @@ export async function placeOrder(
|
|||||||
reduceOnly = false,
|
reduceOnly = false,
|
||||||
guard?: OrderGuardOptions,
|
guard?: OrderGuardOptions,
|
||||||
opts?: PlaceOrderOptions
|
opts?: PlaceOrderOptions
|
||||||
): Promise<AsterOrder | undefined> {
|
): Promise<Order | undefined> {
|
||||||
const type = "LIMIT";
|
const type = "LIMIT";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
const priceNum = Number(price);
|
const priceNum = Number(price);
|
||||||
@@ -180,6 +181,7 @@ export async function placeOrder(
|
|||||||
closePosition,
|
closePosition,
|
||||||
slPrice: opts?.slPrice,
|
slPrice: opts?.slPrice,
|
||||||
tpPrice: opts?.tpPrice,
|
tpPrice: opts?.tpPrice,
|
||||||
|
clientOrderId: opts?.clientOrderId,
|
||||||
});
|
});
|
||||||
pendings[type] = String(order.orderId);
|
pendings[type] = String(order.orderId);
|
||||||
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${opts?.slPrice ? ` sl=${opts.slPrice}` : ""}`);
|
log("order", `挂限价单: ${side} @ ${priceNum} 数量 ${quantity} reduceOnly=${reduceOnly}${opts?.slPrice ? ` sl=${opts.slPrice}` : ""}`);
|
||||||
@@ -197,7 +199,7 @@ export async function placeOrder(
|
|||||||
export async function placeMarketOrder(
|
export async function placeMarketOrder(
|
||||||
adapter: ExchangeAdapter,
|
adapter: ExchangeAdapter,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
openOrders: AsterOrder[],
|
openOrders: Order[],
|
||||||
locks: OrderLockMap,
|
locks: OrderLockMap,
|
||||||
timers: OrderTimerMap,
|
timers: OrderTimerMap,
|
||||||
pendings: OrderPendingMap,
|
pendings: OrderPendingMap,
|
||||||
@@ -207,7 +209,7 @@ export async function placeMarketOrder(
|
|||||||
reduceOnly = false,
|
reduceOnly = false,
|
||||||
guard?: OrderGuardOptions,
|
guard?: OrderGuardOptions,
|
||||||
opts?: { qtyStep: number }
|
opts?: { qtyStep: number }
|
||||||
): Promise<AsterOrder | undefined> {
|
): Promise<Order | undefined> {
|
||||||
const type = "MARKET";
|
const type = "MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
if (!enforceMarkPriceGuard(side, guard?.expectedPrice ?? null, guard, log, "市价单")) return;
|
||||||
@@ -247,7 +249,7 @@ export async function placeMarketOrder(
|
|||||||
export async function placeStopLossOrder(
|
export async function placeStopLossOrder(
|
||||||
adapter: ExchangeAdapter,
|
adapter: ExchangeAdapter,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
openOrders: AsterOrder[],
|
openOrders: Order[],
|
||||||
locks: OrderLockMap,
|
locks: OrderLockMap,
|
||||||
timers: OrderTimerMap,
|
timers: OrderTimerMap,
|
||||||
pendings: OrderPendingMap,
|
pendings: OrderPendingMap,
|
||||||
@@ -258,7 +260,7 @@ export async function placeStopLossOrder(
|
|||||||
log: LogHandler,
|
log: LogHandler,
|
||||||
guard?: OrderGuardOptions,
|
guard?: OrderGuardOptions,
|
||||||
opts?: { priceTick: number; qtyStep: number }
|
opts?: { priceTick: number; qtyStep: number }
|
||||||
): Promise<AsterOrder | undefined> {
|
): Promise<Order | undefined> {
|
||||||
const type = "STOP_MARKET";
|
const type = "STOP_MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
|
if (!enforceMarkPriceGuard(side, stopPrice, guard, log, "止损单")) return;
|
||||||
@@ -314,7 +316,7 @@ export async function placeStopLossOrder(
|
|||||||
export async function placeTrailingStopOrder(
|
export async function placeTrailingStopOrder(
|
||||||
adapter: ExchangeAdapter,
|
adapter: ExchangeAdapter,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
openOrders: AsterOrder[],
|
openOrders: Order[],
|
||||||
locks: OrderLockMap,
|
locks: OrderLockMap,
|
||||||
timers: OrderTimerMap,
|
timers: OrderTimerMap,
|
||||||
pendings: OrderPendingMap,
|
pendings: OrderPendingMap,
|
||||||
@@ -325,7 +327,7 @@ export async function placeTrailingStopOrder(
|
|||||||
log: LogHandler,
|
log: LogHandler,
|
||||||
guard?: OrderGuardOptions,
|
guard?: OrderGuardOptions,
|
||||||
opts?: { priceTick: number; qtyStep: number }
|
opts?: { priceTick: number; qtyStep: number }
|
||||||
): Promise<AsterOrder | undefined> {
|
): Promise<Order | undefined> {
|
||||||
const type = "TRAILING_STOP_MARKET";
|
const type = "TRAILING_STOP_MARKET";
|
||||||
if (isOperating(locks, type)) return;
|
if (isOperating(locks, type)) return;
|
||||||
if (!adapter.supportsTrailingStops()) {
|
if (!adapter.supportsTrailingStops()) {
|
||||||
@@ -375,7 +377,7 @@ export async function placeTrailingStopOrder(
|
|||||||
export async function marketClose(
|
export async function marketClose(
|
||||||
adapter: ExchangeAdapter,
|
adapter: ExchangeAdapter,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
openOrders: AsterOrder[],
|
openOrders: Order[],
|
||||||
locks: OrderLockMap,
|
locks: OrderLockMap,
|
||||||
timers: OrderTimerMap,
|
timers: OrderTimerMap,
|
||||||
pendings: OrderPendingMap,
|
pendings: OrderPendingMap,
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { extractMessage } from "../utils/errors";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a callback so exceptions inside it are swallowed and logged.
|
||||||
|
* Used by adapter watch* methods to prevent gateway callback errors
|
||||||
|
* from killing the event loop.
|
||||||
|
*/
|
||||||
|
export function createSafeInvoke(adapterName: string) {
|
||||||
|
return function safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||||
|
const wrapped = ((...args: any[]) => {
|
||||||
|
try {
|
||||||
|
cb(...args);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[${adapterName}] ${context} handler failed: ${extractMessage(error)}`);
|
||||||
|
}
|
||||||
|
}) as T;
|
||||||
|
return wrapped;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitManager {
|
||||||
|
ensureInitialized(context?: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reusable init-once-with-retry manager.
|
||||||
|
* Every adapter uses the same pattern: cache the init promise, retry on
|
||||||
|
* failure with exponential back-off, and deduplicate context error logs.
|
||||||
|
*/
|
||||||
|
export function createInitManager(
|
||||||
|
adapterName: string,
|
||||||
|
doInitialize: () => Promise<void>,
|
||||||
|
): InitManager {
|
||||||
|
let initPromise: Promise<void> | null = null;
|
||||||
|
const initContexts = new Set<string>();
|
||||||
|
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let retryDelayMs = 3000;
|
||||||
|
let lastInitErrorAt = 0;
|
||||||
|
|
||||||
|
function clearRetry(): void {
|
||||||
|
if (retryTimer) {
|
||||||
|
clearTimeout(retryTimer);
|
||||||
|
retryTimer = null;
|
||||||
|
}
|
||||||
|
retryDelayMs = 3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInitError(context: string, error: unknown): void {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastInitErrorAt < 5000) return;
|
||||||
|
lastInitErrorAt = now;
|
||||||
|
console.error(`[${adapterName}] ${context} failed`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRetry(): void {
|
||||||
|
if (retryTimer) return;
|
||||||
|
retryTimer = setTimeout(() => {
|
||||||
|
retryTimer = null;
|
||||||
|
if (initPromise) return;
|
||||||
|
retryDelayMs = Math.min(retryDelayMs * 2, 60_000);
|
||||||
|
void ensureInitialized("retry");
|
||||||
|
}, retryDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureInitialized(context?: string): Promise<void> {
|
||||||
|
if (!initPromise) {
|
||||||
|
initContexts.clear();
|
||||||
|
initPromise = doInitialize()
|
||||||
|
.then((value) => {
|
||||||
|
clearRetry();
|
||||||
|
return value;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
handleInitError("initialize", error);
|
||||||
|
initPromise = null;
|
||||||
|
scheduleRetry();
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (context && !initContexts.has(context)) {
|
||||||
|
initContexts.add(context);
|
||||||
|
initPromise.catch((error) => {
|
||||||
|
handleInitError(context, error);
|
||||||
|
scheduleRetry();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ensureInitialized };
|
||||||
|
}
|
||||||
+14
-14
@@ -1,30 +1,30 @@
|
|||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
AsterKline,
|
Kline,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export interface AccountListener {
|
export interface AccountListener {
|
||||||
(snapshot: AsterAccountSnapshot): void;
|
(snapshot: AccountSnapshot): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderListener {
|
export interface OrderListener {
|
||||||
(orders: AsterOrder[]): void;
|
(orders: Order[]): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DepthListener {
|
export interface DepthListener {
|
||||||
(depth: AsterDepth): void;
|
(depth: Depth): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TickerListener {
|
export interface TickerListener {
|
||||||
(ticker: AsterTicker): void;
|
(ticker: Ticker): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KlineListener {
|
export interface KlineListener {
|
||||||
(klines: AsterKline[]): void;
|
(klines: Kline[]): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FundingRateSnapshot {
|
export interface FundingRateSnapshot {
|
||||||
@@ -72,18 +72,18 @@ export interface ExchangeAdapter {
|
|||||||
watchTicker(symbol: string, cb: TickerListener): void;
|
watchTicker(symbol: string, cb: TickerListener): void;
|
||||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void;
|
watchKlines(symbol: string, interval: string, cb: KlineListener): void;
|
||||||
watchFundingRate?(symbol: string, cb: FundingRateListener): void;
|
watchFundingRate?(symbol: string, cb: FundingRateListener): void;
|
||||||
createOrder(params: CreateOrderParams): Promise<AsterOrder>;
|
createOrder(params: CreateOrderParams): Promise<Order>;
|
||||||
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
|
cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void>;
|
||||||
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
||||||
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
||||||
getPrecision?(): Promise<ExchangePrecision | null>;
|
getPrecision?(): Promise<ExchangePrecision | null>;
|
||||||
// 连接保护相关方法(可选,仅 StandX 支持)
|
// Connection protection methods (optional)
|
||||||
onConnectionEvent?(listener: ConnectionEventListener): void;
|
onConnectionEvent?(listener: ConnectionEventListener): void;
|
||||||
offConnectionEvent?(listener: ConnectionEventListener): void;
|
offConnectionEvent?(listener: ConnectionEventListener): void;
|
||||||
onRestHealthEvent?(listener: RestHealthListener): void;
|
onRestHealthEvent?(listener: RestHealthListener): void;
|
||||||
offRestHealthEvent?(listener: RestHealthListener): void;
|
offRestHealthEvent?(listener: RestHealthListener): void;
|
||||||
queryOpenOrders?(): Promise<AsterOrder[]>;
|
queryOpenOrders?(): Promise<Order[]>;
|
||||||
queryAccountSnapshot?(): Promise<AsterAccountSnapshot | null>;
|
queryAccountSnapshot?(): Promise<AccountSnapshot | null>;
|
||||||
changeMarginMode?(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void>;
|
changeMarginMode?(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void>;
|
||||||
forceCancelAllOrders?(): Promise<boolean>;
|
forceCancelAllOrders?(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
import type {
|
|
||||||
AccountListener,
|
|
||||||
DepthListener,
|
|
||||||
ExchangeAdapter,
|
|
||||||
ExchangePrecision,
|
|
||||||
KlineListener,
|
|
||||||
OrderListener,
|
|
||||||
TickerListener,
|
|
||||||
} from "./adapter";
|
|
||||||
import type { AsterOrder, CreateOrderParams, AsterDepth, AsterTicker, AsterKline } from "./types";
|
|
||||||
import { extractMessage } from "../utils/errors";
|
|
||||||
import { AsterGateway } from "./aster/client";
|
|
||||||
|
|
||||||
export interface AsterCredentials {
|
|
||||||
apiKey?: string;
|
|
||||||
apiSecret?: string;
|
|
||||||
symbol?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AsterExchangeAdapter implements ExchangeAdapter {
|
|
||||||
readonly id = "aster";
|
|
||||||
private readonly gateway: AsterGateway;
|
|
||||||
private readonly symbol: string;
|
|
||||||
private initPromise: Promise<void> | null = null;
|
|
||||||
private lastInitErrorAt = 0;
|
|
||||||
private readonly initContexts = new Set<string>();
|
|
||||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private retryDelayMs = 3000;
|
|
||||||
|
|
||||||
constructor(credentials: AsterCredentials = {}) {
|
|
||||||
this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret });
|
|
||||||
this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
|
||||||
const wrapped = ((...args: any[]) => {
|
|
||||||
try {
|
|
||||||
cb(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[AsterExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureInitialized(context?: string): Promise<void> {
|
|
||||||
if (!this.initPromise) {
|
|
||||||
this.initContexts.clear();
|
|
||||||
this.initPromise = this.gateway.ensureInitialized(this.symbol).then((value) => {
|
|
||||||
this.clearRetry();
|
|
||||||
return value;
|
|
||||||
}).catch((error) => {
|
|
||||||
this.handleInitError("initialize", error);
|
|
||||||
this.initPromise = null;
|
|
||||||
this.scheduleRetry();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (context && !this.initContexts.has(context)) {
|
|
||||||
this.initContexts.add(context);
|
|
||||||
this.initPromise.catch((error) => {
|
|
||||||
this.handleInitError(context, error);
|
|
||||||
this.scheduleRetry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRetry(): void {
|
|
||||||
if (this.retryTimer) return;
|
|
||||||
this.retryTimer = setTimeout(() => {
|
|
||||||
this.retryTimer = null;
|
|
||||||
if (this.initPromise) return;
|
|
||||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
|
||||||
void this.ensureInitialized("retry");
|
|
||||||
}, this.retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRetry(): void {
|
|
||||||
if (this.retryTimer) {
|
|
||||||
clearTimeout(this.retryTimer);
|
|
||||||
this.retryTimer = null;
|
|
||||||
}
|
|
||||||
this.retryDelayMs = 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitError(context: string, error: unknown): void {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastInitErrorAt < 5000) return;
|
|
||||||
this.lastInitErrorAt = now;
|
|
||||||
console.error(`[AsterExchangeAdapter] ${context} failed`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
watchAccount(cb: AccountListener): void {
|
|
||||||
void this.ensureInitialized("watchAccount");
|
|
||||||
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
|
|
||||||
cb(snapshot);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
watchOrders(cb: OrderListener): void {
|
|
||||||
void this.ensureInitialized("watchOrders");
|
|
||||||
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
|
|
||||||
cb(orders);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
watchDepth(symbol: string, cb: DepthListener): void {
|
|
||||||
void this.ensureInitialized("watchDepth");
|
|
||||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: AsterDepth) => {
|
|
||||||
cb(depth);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
watchTicker(symbol: string, cb: TickerListener): void {
|
|
||||||
void this.ensureInitialized("watchTicker");
|
|
||||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: AsterTicker) => {
|
|
||||||
cb(ticker);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
|
||||||
void this.ensureInitialized("watchKlines");
|
|
||||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: AsterKline[]) => {
|
|
||||||
cb(klines);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
|
||||||
await this.ensureInitialized("createOrder");
|
|
||||||
return this.gateway.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
|
||||||
await this.ensureInitialized("cancelOrder");
|
|
||||||
await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) });
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
|
||||||
await this.ensureInitialized("cancelOrders");
|
|
||||||
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
|
|
||||||
}
|
|
||||||
|
|
||||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
|
||||||
await this.ensureInitialized("cancelAllOrders");
|
|
||||||
await this.gateway.cancelAllOrders(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPrecision(): Promise<ExchangePrecision | null> {
|
|
||||||
try {
|
|
||||||
const precision = await this.gateway.getPrecision(this.symbol);
|
|
||||||
if (!precision) return null;
|
|
||||||
return {
|
|
||||||
priceTick: precision.priceTick,
|
|
||||||
qtyStep: precision.qtyStep,
|
|
||||||
priceDecimals: precision.priceDecimals,
|
|
||||||
sizeDecimals: precision.sizeDecimals,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[AsterExchangeAdapter] getPrecision failed", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import type {
|
||||||
|
AccountListener,
|
||||||
|
DepthListener,
|
||||||
|
ExchangeAdapter,
|
||||||
|
ExchangePrecision,
|
||||||
|
KlineListener,
|
||||||
|
OrderListener,
|
||||||
|
TickerListener,
|
||||||
|
} from "../adapter";
|
||||||
|
import type { Order, CreateOrderParams, Depth, Ticker, Kline } from "../types";
|
||||||
|
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||||
|
import { AsterGateway } from "./gateway";
|
||||||
|
|
||||||
|
export interface AsterCredentials {
|
||||||
|
apiKey?: string;
|
||||||
|
apiSecret?: string;
|
||||||
|
symbol?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsterExchangeAdapter implements ExchangeAdapter {
|
||||||
|
readonly id = "aster";
|
||||||
|
private readonly gateway: AsterGateway;
|
||||||
|
private readonly symbol: string;
|
||||||
|
private readonly safeInvoke = createSafeInvoke("AsterExchangeAdapter");
|
||||||
|
private readonly init: ReturnType<typeof createInitManager>;
|
||||||
|
|
||||||
|
constructor(credentials: AsterCredentials = {}) {
|
||||||
|
this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret });
|
||||||
|
this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase();
|
||||||
|
this.init = createInitManager("AsterExchangeAdapter", () =>
|
||||||
|
this.gateway.ensureInitialized(this.symbol),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsTrailingStops(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchAccount(cb: AccountListener): void {
|
||||||
|
void this.init.ensureInitialized("watchAccount");
|
||||||
|
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
|
||||||
|
cb(snapshot);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchOrders(cb: OrderListener): void {
|
||||||
|
void this.init.ensureInitialized("watchOrders");
|
||||||
|
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
|
||||||
|
cb(orders);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchDepth(symbol: string, cb: DepthListener): void {
|
||||||
|
void this.init.ensureInitialized("watchDepth");
|
||||||
|
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: Depth) => {
|
||||||
|
cb(depth);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchTicker(symbol: string, cb: TickerListener): void {
|
||||||
|
void this.init.ensureInitialized("watchTicker");
|
||||||
|
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: Ticker) => {
|
||||||
|
cb(ticker);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||||
|
void this.init.ensureInitialized("watchKlines");
|
||||||
|
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: Kline[]) => {
|
||||||
|
cb(klines);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
|
await this.init.ensureInitialized("createOrder");
|
||||||
|
return this.gateway.createOrder(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
|
await this.init.ensureInitialized("cancelOrder");
|
||||||
|
await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) });
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
|
await this.init.ensureInitialized("cancelOrders");
|
||||||
|
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||||
|
await this.init.ensureInitialized("cancelAllOrders");
|
||||||
|
await this.gateway.cancelAllOrders(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||||
|
try {
|
||||||
|
const precision = await this.gateway.getPrecision(this.symbol);
|
||||||
|
if (!precision) return null;
|
||||||
|
return {
|
||||||
|
priceTick: precision.priceTick,
|
||||||
|
qtyStep: precision.qtyStep,
|
||||||
|
priceDecimals: precision.priceDecimals,
|
||||||
|
sizeDecimals: precision.sizeDecimals,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AsterExchangeAdapter] getPrecision failed", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { setInterval, clearInterval, setTimeout, clearTimeout } from "timers";
|
import { setInterval, clearInterval, setTimeout, clearTimeout } from "timers";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountPosition,
|
AccountPosition,
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
|
Ticker,
|
||||||
|
CreateOrderParams,
|
||||||
|
PositionSide,
|
||||||
|
} from "../types";
|
||||||
|
import type {
|
||||||
AsterSpotAccount,
|
AsterSpotAccount,
|
||||||
AsterSpotAggTrade,
|
AsterSpotAggTrade,
|
||||||
AsterSpotBookTicker,
|
AsterSpotBookTicker,
|
||||||
@@ -18,18 +23,15 @@ import type {
|
|||||||
AsterSpotTicker24h,
|
AsterSpotTicker24h,
|
||||||
AsterSpotTrade,
|
AsterSpotTrade,
|
||||||
AsterSpotUserTrade,
|
AsterSpotUserTrade,
|
||||||
AsterTicker,
|
|
||||||
AsterFuturesExchangeInfo,
|
AsterFuturesExchangeInfo,
|
||||||
AsterFuturesSymbolInfo,
|
AsterFuturesSymbolInfo,
|
||||||
CancelSpotOrderParams,
|
CancelSpotOrderParams,
|
||||||
CreateOrderParams,
|
|
||||||
CreateSpotOrderParams,
|
CreateSpotOrderParams,
|
||||||
PositionSide,
|
|
||||||
QuerySpotOrderParams,
|
QuerySpotOrderParams,
|
||||||
SpotAllOrdersParams,
|
SpotAllOrdersParams,
|
||||||
SpotOpenOrdersParams,
|
SpotOpenOrdersParams,
|
||||||
SpotUserTradesParams,
|
SpotUserTradesParams,
|
||||||
} from "../types";
|
} from "./types";
|
||||||
import { decimalsOf } from "../../utils/math";
|
import { decimalsOf } from "../../utils/math";
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
@@ -244,7 +246,7 @@ export class AsterSpotRestClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateSpotOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateSpotOrderParams): Promise<Order> {
|
||||||
const response = await this.request<any>({
|
const response = await this.request<any>({
|
||||||
path: "/api/v1/order",
|
path: "/api/v1/order",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -255,7 +257,7 @@ export class AsterSpotRestClient {
|
|||||||
return toOrderFromRest(response);
|
return toOrderFromRest(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: CancelSpotOrderParams): Promise<AsterOrder> {
|
async cancelOrder(params: CancelSpotOrderParams): Promise<Order> {
|
||||||
const response = await this.request<any>({
|
const response = await this.request<any>({
|
||||||
path: "/api/v1/order",
|
path: "/api/v1/order",
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
@@ -270,7 +272,7 @@ export class AsterSpotRestClient {
|
|||||||
return toOrderFromRest(response);
|
return toOrderFromRest(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrder(params: QuerySpotOrderParams): Promise<AsterOrder> {
|
async getOrder(params: QuerySpotOrderParams): Promise<Order> {
|
||||||
const response = await this.request<any>({
|
const response = await this.request<any>({
|
||||||
path: "/api/v1/order",
|
path: "/api/v1/order",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -285,7 +287,7 @@ export class AsterSpotRestClient {
|
|||||||
return toOrderFromRest(response);
|
return toOrderFromRest(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<AsterOrder[]> {
|
async getOpenOrders(params: SpotOpenOrdersParams = {}): Promise<Order[]> {
|
||||||
const response = await this.request<any[]>({
|
const response = await this.request<any[]>({
|
||||||
path: "/api/v1/openOrders",
|
path: "/api/v1/openOrders",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -319,7 +321,7 @@ export class AsterSpotRestClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllOrders(params: SpotAllOrdersParams): Promise<AsterOrder[]> {
|
async getAllOrders(params: SpotAllOrdersParams): Promise<Order[]> {
|
||||||
const response = await this.request<any[]>({
|
const response = await this.request<any[]>({
|
||||||
path: "/api/v1/allOrders",
|
path: "/api/v1/allOrders",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -549,7 +551,7 @@ export class AsterSpotRestClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDepth(streamSymbol: string, data: any): AsterDepth {
|
function toDepth(streamSymbol: string, data: any): Depth {
|
||||||
return {
|
return {
|
||||||
eventType: data.e,
|
eventType: data.e,
|
||||||
eventTime: data.E,
|
eventTime: data.E,
|
||||||
@@ -561,7 +563,7 @@ function toDepth(streamSymbol: string, data: any): AsterDepth {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toTicker(data: any): AsterTicker {
|
function toTicker(data: any): Ticker {
|
||||||
return {
|
return {
|
||||||
eventType: data.e,
|
eventType: data.e,
|
||||||
eventTime: data.E,
|
eventTime: data.E,
|
||||||
@@ -584,7 +586,7 @@ function toTicker(data: any): AsterTicker {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toKline(data: any): AsterKline {
|
function toKline(data: any): Kline {
|
||||||
return {
|
return {
|
||||||
eventType: data.e,
|
eventType: data.e,
|
||||||
eventTime: data.E,
|
eventTime: data.E,
|
||||||
@@ -607,7 +609,7 @@ function toKline(data: any): AsterKline {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fromRestKline(entry: any[], interval: string, symbol: string): AsterKline {
|
function fromRestKline(entry: any[], interval: string, symbol: string): Kline {
|
||||||
return {
|
return {
|
||||||
eventType: undefined,
|
eventType: undefined,
|
||||||
eventTime: undefined,
|
eventTime: undefined,
|
||||||
@@ -628,7 +630,7 @@ function fromRestKline(entry: any[], interval: string, symbol: string): AsterKli
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toOrderFromRest(raw: any): AsterOrder {
|
function toOrderFromRest(raw: any): Order {
|
||||||
return {
|
return {
|
||||||
avgPrice: raw.avgPrice ?? "0",
|
avgPrice: raw.avgPrice ?? "0",
|
||||||
clientOrderId: raw.clientOrderId ?? "",
|
clientOrderId: raw.clientOrderId ?? "",
|
||||||
@@ -656,7 +658,7 @@ function toOrderFromRest(raw: any): AsterOrder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toOrderFromEvent(event: any): AsterOrder {
|
function toOrderFromEvent(event: any): Order {
|
||||||
return {
|
return {
|
||||||
avgPrice: event.ap ?? "0",
|
avgPrice: event.ap ?? "0",
|
||||||
clientOrderId: event.c ?? "",
|
clientOrderId: event.c ?? "",
|
||||||
@@ -684,7 +686,7 @@ function toOrderFromEvent(event: any): AsterOrder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPositionFromRisk(raw: any): AsterAccountPosition {
|
function toPositionFromRisk(raw: any): AccountPosition {
|
||||||
const positionSide = String(raw.positionSide ?? raw.ps ?? "BOTH").toUpperCase() as PositionSide;
|
const positionSide = String(raw.positionSide ?? raw.ps ?? "BOTH").toUpperCase() as PositionSide;
|
||||||
return {
|
return {
|
||||||
symbol: raw.symbol ?? raw.s ?? "",
|
symbol: raw.symbol ?? raw.s ?? "",
|
||||||
@@ -708,16 +710,16 @@ function toPositionFromRisk(raw: any): AsterAccountPosition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepCloneAccount(snapshot: AsterAccountSnapshot | null): AsterAccountSnapshot | null {
|
function deepCloneAccount(snapshot: AccountSnapshot | null): AccountSnapshot | null {
|
||||||
return snapshot ? JSON.parse(JSON.stringify(snapshot)) : null;
|
return snapshot ? JSON.parse(JSON.stringify(snapshot)) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sumUnrealizedProfit(positions: AsterAccountPosition[]): string {
|
function sumUnrealizedProfit(positions: AccountPosition[]): string {
|
||||||
const total = positions.reduce((acc, position) => acc + Number(position.unrealizedProfit ?? 0), 0);
|
const total = positions.reduce((acc, position) => acc + Number(position.unrealizedProfit ?? 0), 0);
|
||||||
return total.toFixed(8);
|
return total.toFixed(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clonePositions(positions: AsterAccountPosition[]): AsterAccountPosition[] {
|
function clonePositions(positions: AccountPosition[]): AccountPosition[] {
|
||||||
return positions.map((position) => ({
|
return positions.map((position) => ({
|
||||||
...position,
|
...position,
|
||||||
updateTime: position.updateTime ?? Date.now(),
|
updateTime: position.updateTime ?? Date.now(),
|
||||||
@@ -763,18 +765,18 @@ export class AsterRestClient {
|
|||||||
this.apiSecret = requireEnv(options.apiSecret ?? process.env.ASTER_API_SECRET, "ASTER_API_SECRET");
|
this.apiSecret = requireEnv(options.apiSecret ?? process.env.ASTER_API_SECRET, "ASTER_API_SECRET");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAccount(): Promise<AsterAccountSnapshot> {
|
async getAccount(): Promise<AccountSnapshot> {
|
||||||
return this.signedRequest<AsterAccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} });
|
return this.signedRequest<AccountSnapshot>({ path: "/fapi/v2/account", method: "GET", params: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOpenOrders(symbol?: string): Promise<AsterOrder[]> {
|
async getOpenOrders(symbol?: string): Promise<Order[]> {
|
||||||
const params: Record<string, unknown> = {};
|
const params: Record<string, unknown> = {};
|
||||||
if (symbol) params.symbol = symbol;
|
if (symbol) params.symbol = symbol;
|
||||||
const raw = await this.signedRequest<any[]>({ path: "/fapi/v1/openOrders", method: "GET", params });
|
const raw = await this.signedRequest<any[]>({ path: "/fapi/v1/openOrders", method: "GET", params });
|
||||||
return raw.map(toOrderFromRest);
|
return raw.map(toOrderFromRest);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPositions(symbol?: string): Promise<AsterAccountPosition[]> {
|
async getPositions(symbol?: string): Promise<AccountPosition[]> {
|
||||||
const params: Record<string, unknown> = {};
|
const params: Record<string, unknown> = {};
|
||||||
if (symbol) params.symbol = symbol.toUpperCase();
|
if (symbol) params.symbol = symbol.toUpperCase();
|
||||||
const raw = await this.signedRequest<any[]>({ path: "/fapi/v2/positionRisk", method: "GET", params });
|
const raw = await this.signedRequest<any[]>({ path: "/fapi/v2/positionRisk", method: "GET", params });
|
||||||
@@ -800,7 +802,7 @@ export class AsterRestClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
// Sanitize and normalize params for Aster futures API. Paradex-specific flags
|
// Sanitize and normalize params for Aster futures API. Paradex-specific flags
|
||||||
// like reduceOnly/closePosition on STOP/TRAILING should not leak here.
|
// like reduceOnly/closePosition on STOP/TRAILING should not leak here.
|
||||||
const payload: Record<string, unknown> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
@@ -813,6 +815,7 @@ export class AsterRestClient {
|
|||||||
if (params.activationPrice !== undefined) payload.activationPrice = params.activationPrice;
|
if (params.activationPrice !== undefined) payload.activationPrice = params.activationPrice;
|
||||||
if (params.callbackRate !== undefined) payload.callbackRate = params.callbackRate;
|
if (params.callbackRate !== undefined) payload.callbackRate = params.callbackRate;
|
||||||
if (params.quantity !== undefined) payload.quantity = Math.abs(params.quantity);
|
if (params.quantity !== undefined) payload.quantity = Math.abs(params.quantity);
|
||||||
|
if (params.clientOrderId !== undefined) payload.newClientOrderId = params.clientOrderId;
|
||||||
|
|
||||||
// Aster rejects reduceOnly/closePosition for certain order types (e.g. STOP/TRAILING).
|
// Aster rejects reduceOnly/closePosition for certain order types (e.g. STOP/TRAILING).
|
||||||
// Keep the behavior exchange-specific by stripping them here for Aster.
|
// Keep the behavior exchange-specific by stripping them here for Aster.
|
||||||
@@ -830,12 +833,12 @@ export class AsterRestClient {
|
|||||||
return toOrderFromRest(response);
|
return toOrderFromRest(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<AsterOrder> {
|
async cancelOrder(params: { symbol: string; orderId?: number; origClientOrderId?: string }): Promise<Order> {
|
||||||
const response = await this.signedRequest<any>({ path: "/fapi/v1/order", method: "DELETE", params });
|
const response = await this.signedRequest<any>({ path: "/fapi/v1/order", method: "DELETE", params });
|
||||||
return toOrderFromRest(response);
|
return toOrderFromRest(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<AsterOrder[]> {
|
async cancelOrders(params: { symbol: string; orderIdList?: Array<number | string>; origClientOrderIdList?: string[] }): Promise<Order[]> {
|
||||||
const payload: Record<string, unknown> = { symbol: params.symbol };
|
const payload: Record<string, unknown> = { symbol: params.symbol };
|
||||||
if (params.orderIdList?.length) {
|
if (params.orderIdList?.length) {
|
||||||
payload.orderIdList = `[${params.orderIdList
|
payload.orderIdList = `[${params.orderIdList
|
||||||
@@ -853,7 +856,7 @@ export class AsterRestClient {
|
|||||||
await this.signedRequest({ path: "/fapi/v1/allOpenOrders", method: "DELETE", params });
|
await this.signedRequest({ path: "/fapi/v1/allOpenOrders", method: "DELETE", params });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise<AsterKline[]> {
|
async getKlines(symbol: string, interval: string, limit = DEFAULT_KLINE_LIMIT): Promise<Kline[]> {
|
||||||
const upper = symbol.toUpperCase();
|
const upper = symbol.toUpperCase();
|
||||||
const url = `${FUTURES_REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`;
|
const url = `${FUTURES_REST_BASE}/fapi/v1/continuousKlines?pair=${upper}&contractType=PERPETUAL&interval=${encodeURIComponent(interval)}&limit=${limit}`;
|
||||||
let response: Response;
|
let response: Response;
|
||||||
@@ -949,9 +952,9 @@ export class AsterRestClient {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DepthHandler = (depth: AsterDepth) => void;
|
type DepthHandler = (depth: Depth) => void;
|
||||||
type TickerHandler = (ticker: AsterTicker) => void;
|
type TickerHandler = (ticker: Ticker) => void;
|
||||||
type KlineHandler = (kline: AsterKline) => void;
|
type KlineHandler = (kline: Kline) => void;
|
||||||
|
|
||||||
type StreamKind = "depth" | "ticker" | "kline";
|
type StreamKind = "depth" | "ticker" | "kline";
|
||||||
|
|
||||||
@@ -1244,7 +1247,7 @@ export class AsterUserStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AsterAccountSnapshot | null {
|
function updateAccountSnapshot(snapshot: AccountSnapshot | null, event: { eventTime: number; payload: AccountUpdatePayload }): AccountSnapshot | null {
|
||||||
if (!snapshot) return snapshot;
|
if (!snapshot) return snapshot;
|
||||||
const next = deepCloneAccount(snapshot);
|
const next = deepCloneAccount(snapshot);
|
||||||
if (!next) return snapshot;
|
if (!next) return snapshot;
|
||||||
@@ -1294,7 +1297,7 @@ function updateAccountSnapshot(snapshot: AsterAccountSnapshot | null, event: { e
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeOrderSnapshot(map: Map<string, AsterOrder>, order: AsterOrder): void {
|
function mergeOrderSnapshot(map: Map<string, Order>, order: Order): void {
|
||||||
const rawId = order.orderId;
|
const rawId = order.orderId;
|
||||||
if (rawId === undefined || rawId === null) return;
|
if (rawId === undefined || rawId === null) return;
|
||||||
const key = String(rawId);
|
const key = String(rawId);
|
||||||
@@ -1311,18 +1314,18 @@ export class AsterGateway {
|
|||||||
private readonly publicStreams: AsterPublicStreams;
|
private readonly publicStreams: AsterPublicStreams;
|
||||||
private readonly userStream: AsterUserStream;
|
private readonly userStream: AsterUserStream;
|
||||||
|
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private readonly openOrders = new Map<string, AsterOrder>();
|
private readonly openOrders = new Map<string, Order>();
|
||||||
private positionSyncTimer: ReturnType<typeof setInterval> | null = null;
|
private positionSyncTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
private positionSyncInFlight = false;
|
private positionSyncInFlight = false;
|
||||||
|
|
||||||
private readonly accountEvent = new SimpleEvent<AsterAccountSnapshot>();
|
private readonly accountEvent = new SimpleEvent<AccountSnapshot>();
|
||||||
private readonly ordersEvent = new SimpleEvent<AsterOrder[]>();
|
private readonly ordersEvent = new SimpleEvent<Order[]>();
|
||||||
private readonly depthEvents = new Map<string, SimpleEvent<AsterDepth>>();
|
private readonly depthEvents = new Map<string, SimpleEvent<Depth>>();
|
||||||
private readonly tickerEvents = new Map<string, SimpleEvent<AsterTicker>>();
|
private readonly tickerEvents = new Map<string, SimpleEvent<Ticker>>();
|
||||||
private readonly klineEvents = new Map<string, SimpleEvent<AsterKline[]>>();
|
private readonly klineEvents = new Map<string, SimpleEvent<Kline[]>>();
|
||||||
|
|
||||||
private readonly klineStores = new Map<string, AsterKline[]>();
|
private readonly klineStores = new Map<string, Kline[]>();
|
||||||
private readonly klineRefreshTimers = new Map<string, ReturnType<typeof setInterval>>();
|
private readonly klineRefreshTimers = new Map<string, ReturnType<typeof setInterval>>();
|
||||||
private readonly klineInitialFetches = new Map<string, Promise<void>>();
|
private readonly klineInitialFetches = new Map<string, Promise<void>>();
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
@@ -1381,21 +1384,21 @@ export class AsterGateway {
|
|||||||
return this.initializing;
|
return this.initializing;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): void {
|
onAccount(listener: (snapshot: AccountSnapshot) => void): void {
|
||||||
this.accountEvent.add(listener);
|
this.accountEvent.add(listener);
|
||||||
if (this.accountSnapshot) listener(this.accountSnapshot);
|
if (this.accountSnapshot) listener(this.accountSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOrders(listener: (orders: AsterOrder[]) => void): void {
|
onOrders(listener: (orders: Order[]) => void): void {
|
||||||
this.ordersEvent.add(listener);
|
this.ordersEvent.add(listener);
|
||||||
listener(Array.from(this.openOrders.values()));
|
listener(Array.from(this.openOrders.values()));
|
||||||
}
|
}
|
||||||
|
|
||||||
onDepth(symbol: string, listener: (depth: AsterDepth) => void): void {
|
onDepth(symbol: string, listener: (depth: Depth) => void): void {
|
||||||
const upper = symbol.toUpperCase();
|
const upper = symbol.toUpperCase();
|
||||||
let event = this.depthEvents.get(upper);
|
let event = this.depthEvents.get(upper);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
event = new SimpleEvent<AsterDepth>();
|
event = new SimpleEvent<Depth>();
|
||||||
this.depthEvents.set(upper, event);
|
this.depthEvents.set(upper, event);
|
||||||
this.publicStreams.subscribeDepth(upper, (depth) => {
|
this.publicStreams.subscribeDepth(upper, (depth) => {
|
||||||
event?.emit(depth);
|
event?.emit(depth);
|
||||||
@@ -1404,11 +1407,11 @@ export class AsterGateway {
|
|||||||
event.add(listener);
|
event.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTicker(symbol: string, listener: (ticker: AsterTicker) => void): void {
|
onTicker(symbol: string, listener: (ticker: Ticker) => void): void {
|
||||||
const upper = symbol.toUpperCase();
|
const upper = symbol.toUpperCase();
|
||||||
let event = this.tickerEvents.get(upper);
|
let event = this.tickerEvents.get(upper);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
event = new SimpleEvent<AsterTicker>();
|
event = new SimpleEvent<Ticker>();
|
||||||
this.tickerEvents.set(upper, event);
|
this.tickerEvents.set(upper, event);
|
||||||
this.publicStreams.subscribeTicker(upper, (ticker) => {
|
this.publicStreams.subscribeTicker(upper, (ticker) => {
|
||||||
event?.emit(ticker);
|
event?.emit(ticker);
|
||||||
@@ -1417,12 +1420,12 @@ export class AsterGateway {
|
|||||||
event.add(listener);
|
event.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
onKlines(symbol: string, interval: string, listener: (klines: AsterKline[]) => void): void {
|
onKlines(symbol: string, interval: string, listener: (klines: Kline[]) => void): void {
|
||||||
const upper = symbol.toUpperCase();
|
const upper = symbol.toUpperCase();
|
||||||
const key = `${upper}:${interval}`;
|
const key = `${upper}:${interval}`;
|
||||||
let event = this.klineEvents.get(key);
|
let event = this.klineEvents.get(key);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
event = new SimpleEvent<AsterKline[]>();
|
event = new SimpleEvent<Kline[]>();
|
||||||
this.klineEvents.set(key, event);
|
this.klineEvents.set(key, event);
|
||||||
this.publicStreams.subscribeKline(symbol, interval, (kline) => {
|
this.publicStreams.subscribeKline(symbol, interval, (kline) => {
|
||||||
const storeKey = `${upper}:${interval}`;
|
const storeKey = `${upper}:${interval}`;
|
||||||
@@ -1509,7 +1512,7 @@ export class AsterGateway {
|
|||||||
console.error("[AsterGateway] 刷新持仓失败", positionError);
|
console.error("[AsterGateway] 刷新持仓失败", positionError);
|
||||||
}
|
}
|
||||||
const normalizedPositions = clonePositions(positions);
|
const normalizedPositions = clonePositions(positions);
|
||||||
const snapshot: AsterAccountSnapshot = {
|
const snapshot: AccountSnapshot = {
|
||||||
...account,
|
...account,
|
||||||
positions: normalizedPositions,
|
positions: normalizedPositions,
|
||||||
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
|
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
|
||||||
@@ -1547,7 +1550,7 @@ export class AsterGateway {
|
|||||||
if (!Array.isArray(positions)) return;
|
if (!Array.isArray(positions)) return;
|
||||||
const normalizedPositions = clonePositions(positions);
|
const normalizedPositions = clonePositions(positions);
|
||||||
if (!this.accountSnapshot) {
|
if (!this.accountSnapshot) {
|
||||||
const snapshot: AsterAccountSnapshot = {
|
const snapshot: AccountSnapshot = {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
canWithdraw: true,
|
canWithdraw: true,
|
||||||
@@ -1561,7 +1564,7 @@ export class AsterGateway {
|
|||||||
this.accountEvent.emit(snapshot);
|
this.accountEvent.emit(snapshot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextSnapshot: AsterAccountSnapshot = {
|
const nextSnapshot: AccountSnapshot = {
|
||||||
...this.accountSnapshot,
|
...this.accountSnapshot,
|
||||||
positions: normalizedPositions,
|
positions: normalizedPositions,
|
||||||
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
|
totalUnrealizedProfit: sumUnrealizedProfit(normalizedPositions),
|
||||||
@@ -1576,15 +1579,15 @@ export class AsterGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccountSnapshot(): AsterAccountSnapshot | null {
|
getAccountSnapshot(): AccountSnapshot | null {
|
||||||
return this.accountSnapshot;
|
return this.accountSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenOrdersSnapshot(): AsterOrder[] {
|
getOpenOrdersSnapshot(): Order[] {
|
||||||
return Array.from(this.openOrders.values());
|
return Array.from(this.openOrders.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
const normalized = await this.normalizeOrderParams(params);
|
const normalized = await this.normalizeOrderParams(params);
|
||||||
const order = await this.rest.createOrder(normalized);
|
const order = await this.rest.createOrder(normalized);
|
||||||
mergeOrderSnapshot(this.openOrders, order);
|
mergeOrderSnapshot(this.openOrders, order);
|
||||||
@@ -1,101 +1,17 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "Aster",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTX",
|
||||||
}
|
supportsTrailingStop: true,
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
supportsTriggerType: true,
|
||||||
params.timeInForce = intent.timeInForce;
|
});
|
||||||
}
|
|
||||||
if (intent.reduceOnly !== undefined) {
|
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
|
||||||
}
|
|
||||||
if (intent.closePosition !== undefined) {
|
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTX",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
triggerType: intent.triggerType,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "TRAILING_STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
activationPrice: intent.activationPrice,
|
|
||||||
callbackRate: intent.callbackRate,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import type { DepthLevel, OrderSide, OrderType, TimeInForce } from "../types";
|
||||||
|
|
||||||
|
export interface AsterSpotRateLimit {
|
||||||
|
rateLimitType: string;
|
||||||
|
interval: string;
|
||||||
|
intervalNum: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotExchangeFilter {
|
||||||
|
filterType: string;
|
||||||
|
[key: string]: string | number | boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterFuturesSymbolFilter {
|
||||||
|
filterType: string;
|
||||||
|
tickSize?: string;
|
||||||
|
stepSize?: string;
|
||||||
|
minPrice?: string;
|
||||||
|
maxPrice?: string;
|
||||||
|
minQty?: string;
|
||||||
|
maxQty?: string;
|
||||||
|
[key: string]: string | number | boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterFuturesSymbolInfo {
|
||||||
|
symbol: string;
|
||||||
|
pair?: string;
|
||||||
|
contractType?: string;
|
||||||
|
pricePrecision?: number;
|
||||||
|
quantityPrecision?: number;
|
||||||
|
baseAssetPrecision?: number;
|
||||||
|
quotePrecision?: number;
|
||||||
|
underlyingType?: string;
|
||||||
|
filters?: AsterFuturesSymbolFilter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterFuturesExchangeInfo {
|
||||||
|
timezone?: string;
|
||||||
|
serverTime?: number;
|
||||||
|
symbols?: AsterFuturesSymbolInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotAssetInfo {
|
||||||
|
asset: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotSymbolInfo {
|
||||||
|
symbol: string;
|
||||||
|
status: string;
|
||||||
|
baseAsset: string;
|
||||||
|
quoteAsset: string;
|
||||||
|
baseAssetPrecision?: number;
|
||||||
|
quotePrecision?: number;
|
||||||
|
pricePrecision?: number;
|
||||||
|
quantityPrecision?: number;
|
||||||
|
orderTypes: string[];
|
||||||
|
timeInForce: string[];
|
||||||
|
ocoAllowed: boolean;
|
||||||
|
filters: AsterSpotExchangeFilter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotExchangeInfo {
|
||||||
|
timezone: string;
|
||||||
|
serverTime: number;
|
||||||
|
rateLimits: AsterSpotRateLimit[];
|
||||||
|
exchangeFilters: AsterSpotExchangeFilter[];
|
||||||
|
assets?: AsterSpotAssetInfo[];
|
||||||
|
symbols: AsterSpotSymbolInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotDepth {
|
||||||
|
lastUpdateId: number;
|
||||||
|
E?: number;
|
||||||
|
T?: number;
|
||||||
|
bids: DepthLevel[];
|
||||||
|
asks: DepthLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotTrade {
|
||||||
|
id: number;
|
||||||
|
price: string;
|
||||||
|
qty: string;
|
||||||
|
baseQty?: string;
|
||||||
|
quoteQty?: string;
|
||||||
|
time: number;
|
||||||
|
isBuyerMaker: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotHistoricalTrade extends AsterSpotTrade {
|
||||||
|
isBestMatch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotAggTrade {
|
||||||
|
a: number;
|
||||||
|
p: string;
|
||||||
|
q: string;
|
||||||
|
f: number;
|
||||||
|
l: number;
|
||||||
|
T: number;
|
||||||
|
m: boolean;
|
||||||
|
M?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotKline {
|
||||||
|
openTime: number;
|
||||||
|
open: string;
|
||||||
|
high: string;
|
||||||
|
low: string;
|
||||||
|
close: string;
|
||||||
|
volume: string;
|
||||||
|
closeTime: number;
|
||||||
|
quoteAssetVolume: string;
|
||||||
|
numberOfTrades: number;
|
||||||
|
takerBuyBaseAssetVolume: string;
|
||||||
|
takerBuyQuoteAssetVolume: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotTicker24h {
|
||||||
|
symbol: string;
|
||||||
|
priceChange: string;
|
||||||
|
priceChangePercent: string;
|
||||||
|
weightedAvgPrice: string;
|
||||||
|
prevClosePrice: string;
|
||||||
|
lastPrice: string;
|
||||||
|
lastQty: string;
|
||||||
|
bidPrice: string;
|
||||||
|
bidQty: string;
|
||||||
|
askPrice: string;
|
||||||
|
askQty: string;
|
||||||
|
openPrice: string;
|
||||||
|
highPrice: string;
|
||||||
|
lowPrice: string;
|
||||||
|
volume: string;
|
||||||
|
quoteVolume: string;
|
||||||
|
openTime: number;
|
||||||
|
closeTime: number;
|
||||||
|
firstId: number;
|
||||||
|
lastId: number;
|
||||||
|
count: number;
|
||||||
|
baseAsset?: string;
|
||||||
|
quoteAsset?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotPriceTicker {
|
||||||
|
symbol: string;
|
||||||
|
price: string;
|
||||||
|
time?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotBookTicker {
|
||||||
|
symbol: string;
|
||||||
|
bidPrice: string;
|
||||||
|
bidQty: string;
|
||||||
|
askPrice: string;
|
||||||
|
askQty: string;
|
||||||
|
time?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotCommissionRate {
|
||||||
|
symbol: string;
|
||||||
|
makerCommissionRate: string;
|
||||||
|
takerCommissionRate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSpotOrderParams {
|
||||||
|
symbol: string;
|
||||||
|
side: OrderSide;
|
||||||
|
type: OrderType;
|
||||||
|
timeInForce?: TimeInForce;
|
||||||
|
quantity?: number | string;
|
||||||
|
quoteOrderQty?: number | string;
|
||||||
|
price?: number | string;
|
||||||
|
newClientOrderId?: string;
|
||||||
|
stopPrice?: number | string;
|
||||||
|
recvWindow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CancelSpotOrderParams {
|
||||||
|
symbol: string;
|
||||||
|
orderId?: number | string;
|
||||||
|
origClientOrderId?: string;
|
||||||
|
recvWindow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuerySpotOrderParams extends CancelSpotOrderParams {}
|
||||||
|
|
||||||
|
export interface SpotOpenOrdersParams {
|
||||||
|
symbol?: string;
|
||||||
|
recvWindow?: number;
|
||||||
|
orderIdList?: Array<number | string>;
|
||||||
|
origClientOrderIdList?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotAllOrdersParams {
|
||||||
|
symbol: string;
|
||||||
|
orderId?: number;
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
limit?: number;
|
||||||
|
recvWindow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotAccountBalance {
|
||||||
|
asset: string;
|
||||||
|
free: string;
|
||||||
|
locked: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotAccount {
|
||||||
|
feeTier: number;
|
||||||
|
canTrade: boolean;
|
||||||
|
canDeposit: boolean;
|
||||||
|
canWithdraw: boolean;
|
||||||
|
canBurnAsset?: boolean;
|
||||||
|
updateTime: number;
|
||||||
|
makerCommission?: string;
|
||||||
|
takerCommission?: string;
|
||||||
|
buyerCommission?: string;
|
||||||
|
sellerCommission?: string;
|
||||||
|
balances: AsterSpotAccountBalance[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotUserTradesParams {
|
||||||
|
symbol?: string;
|
||||||
|
orderId?: number;
|
||||||
|
startTime?: number;
|
||||||
|
endTime?: number;
|
||||||
|
fromId?: number;
|
||||||
|
limit?: number;
|
||||||
|
recvWindow?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsterSpotUserTrade {
|
||||||
|
symbol: string;
|
||||||
|
id: number;
|
||||||
|
orderId: number;
|
||||||
|
side: OrderSide;
|
||||||
|
price: string;
|
||||||
|
qty: string;
|
||||||
|
quoteQty?: string;
|
||||||
|
commission: string;
|
||||||
|
commissionAsset: string;
|
||||||
|
time: number;
|
||||||
|
counterpartyId?: number;
|
||||||
|
maker: boolean;
|
||||||
|
buyer: boolean;
|
||||||
|
}
|
||||||
@@ -6,9 +6,10 @@ import type {
|
|||||||
OrderListener,
|
OrderListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { Order, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
import { extractMessage } from "../../utils/errors";
|
||||||
import { BackpackGateway, type BackpackGatewayOptions } from "./gateway";
|
import { BackpackGateway, type BackpackGatewayOptions } from "./gateway";
|
||||||
|
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||||
|
|
||||||
export interface BackpackCredentials {
|
export interface BackpackCredentials {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@@ -23,11 +24,8 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
|||||||
readonly id = "backpack";
|
readonly id = "backpack";
|
||||||
private readonly gateway: BackpackGateway;
|
private readonly gateway: BackpackGateway;
|
||||||
private readonly symbol: string;
|
private readonly symbol: string;
|
||||||
private initPromise: Promise<void> | null = null;
|
private readonly safeInvoke = createSafeInvoke("BackpackExchangeAdapter");
|
||||||
private readonly initContexts = new Set<string>();
|
private readonly init: ReturnType<typeof createInitManager>;
|
||||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private retryDelayMs = 3000;
|
|
||||||
private lastInitErrorAt = 0;
|
|
||||||
|
|
||||||
constructor(credentials: BackpackCredentials = {}) {
|
constructor(credentials: BackpackCredentials = {}) {
|
||||||
const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY;
|
const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY;
|
||||||
@@ -54,6 +52,9 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
|||||||
|
|
||||||
this.gateway = new BackpackGateway(gatewayOptions);
|
this.gateway = new BackpackGateway(gatewayOptions);
|
||||||
this.symbol = symbol;
|
this.symbol = symbol;
|
||||||
|
this.init = createInitManager("BackpackExchangeAdapter", () =>
|
||||||
|
this.gateway.ensureInitialized(this.symbol),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
@@ -61,118 +62,50 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(cb: AccountListener): void {
|
watchAccount(cb: AccountListener): void {
|
||||||
void this.ensureInitialized("watchAccount");
|
void this.init.ensureInitialized("watchAccount");
|
||||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: OrderListener): void {
|
watchOrders(cb: OrderListener): void {
|
||||||
void this.ensureInitialized("watchOrders");
|
void this.init.ensureInitialized("watchOrders");
|
||||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||||
void this.ensureInitialized("watchDepth");
|
void this.init.ensureInitialized("watchDepth");
|
||||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||||
void this.ensureInitialized("watchTicker");
|
void this.init.ensureInitialized("watchTicker");
|
||||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||||
void this.ensureInitialized(`watchKlines:${interval}`);
|
void this.init.ensureInitialized(`watchKlines:${interval}`);
|
||||||
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized("createOrder");
|
await this.init.ensureInitialized("createOrder");
|
||||||
return this.gateway.createOrder(params);
|
return this.gateway.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrder");
|
await this.init.ensureInitialized("cancelOrder");
|
||||||
await this.gateway.cancelOrder({ orderId: params.orderId });
|
await this.gateway.cancelOrder({ orderId: params.orderId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrders");
|
await this.init.ensureInitialized("cancelOrders");
|
||||||
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
|
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelAllOrders");
|
await this.init.ensureInitialized("cancelAllOrders");
|
||||||
await this.gateway.cancelAllOrders();
|
await this.gateway.cancelAllOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
|
||||||
const wrapped = ((...args: any[]) => {
|
|
||||||
try {
|
|
||||||
cb(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[BackpackExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureInitialized(context?: string): Promise<void> {
|
|
||||||
if (!this.initPromise) {
|
|
||||||
this.initContexts.clear();
|
|
||||||
this.initPromise = this.gateway
|
|
||||||
.ensureInitialized(this.symbol)
|
|
||||||
.then((value) => {
|
|
||||||
if (process.env.BACKPACK_DEBUG === "1") {
|
|
||||||
console.error(`[BackpackExchangeAdapter] initialize succeeded`);
|
|
||||||
}
|
|
||||||
if (process.env.BACKPACK_DEBUG === "1") {
|
|
||||||
console.error(`[BackpackExchangeAdapter] initialize succeeded`);
|
|
||||||
}
|
|
||||||
this.clearRetry();
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.handleInitError("initialize", error);
|
|
||||||
this.initPromise = null;
|
|
||||||
this.scheduleRetry();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (context && !this.initContexts.has(context)) {
|
|
||||||
this.initContexts.add(context);
|
|
||||||
this.initPromise.catch((error) => {
|
|
||||||
this.handleInitError(context, error);
|
|
||||||
this.scheduleRetry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRetry(): void {
|
|
||||||
if (this.retryTimer) return;
|
|
||||||
this.retryTimer = setTimeout(() => {
|
|
||||||
this.retryTimer = null;
|
|
||||||
if (this.initPromise) return;
|
|
||||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
|
||||||
void this.ensureInitialized("retry");
|
|
||||||
}, this.retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRetry(): void {
|
|
||||||
if (this.retryTimer) {
|
|
||||||
clearTimeout(this.retryTimer);
|
|
||||||
this.retryTimer = null;
|
|
||||||
}
|
|
||||||
this.retryDelayMs = 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitError(context: string, error: unknown): void {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastInitErrorAt < 5000) return;
|
|
||||||
this.lastInitErrorAt = now;
|
|
||||||
console.error(`[BackpackExchangeAdapter] ${context} failed`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private logError(context: string, error: unknown): void {
|
private logError(context: string, error: unknown): void {
|
||||||
if (process.env.BACKPACK_DEBUG === "1" || process.env.BACKPACK_DEBUG === "true") {
|
if (process.env.BACKPACK_DEBUG === "1" || process.env.BACKPACK_DEBUG === "true") {
|
||||||
console.error(`[BackpackExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
|
console.error(`[BackpackExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import NodeWebSocket from "ws";
|
|||||||
import { sign, utils as edUtils, hashes as edHashes } from "@noble/ed25519";
|
import { sign, utils as edUtils, hashes as edHashes } from "@noble/ed25519";
|
||||||
import { sha512 } from "@noble/hashes/sha512";
|
import { sha512 } from "@noble/hashes/sha512";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterAccountPosition,
|
AccountPosition,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
AsterKline,
|
Kline,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
OrderType,
|
OrderType,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -92,8 +92,8 @@ export class BackpackGateway {
|
|||||||
private tickerPollTimer: ReturnType<typeof setInterval> | null = null;
|
private tickerPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
private readonly klinePollTimers = new Map<string, ReturnType<typeof setInterval>>();
|
private readonly klinePollTimers = new Map<string, ReturnType<typeof setInterval>>();
|
||||||
|
|
||||||
private readonly localOrders = new Map<string, AsterOrder>();
|
private readonly localOrders = new Map<string, Order>();
|
||||||
private lastBalanceSnapshot: AsterAccountSnapshot | null = null;
|
private lastBalanceSnapshot: AccountSnapshot | null = null;
|
||||||
private marketId = "";
|
private marketId = "";
|
||||||
|
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
@@ -244,7 +244,7 @@ export class BackpackGateway {
|
|||||||
this.exchange.fetchOpenOrders(this.marketSymbol),
|
this.exchange.fetchOpenOrders(this.marketSymbol),
|
||||||
this.exchange.fetchOrders(this.marketSymbol, undefined, 200, {}),
|
this.exchange.fetchOrders(this.marketSymbol, undefined, 200, {}),
|
||||||
]);
|
]);
|
||||||
const active = new Map<string, AsterOrder>();
|
const active = new Map<string, Order>();
|
||||||
for (const entry of [...openOrders, ...allOrders]) {
|
for (const entry of [...openOrders, ...allOrders]) {
|
||||||
const status = this.normalizeStatus(entry.status ?? (entry.info?.status as string));
|
const status = this.normalizeStatus(entry.status ?? (entry.info?.status as string));
|
||||||
if (this.isTerminalStatus(status)) continue;
|
if (this.isTerminalStatus(status)) continue;
|
||||||
@@ -315,7 +315,7 @@ export class BackpackGateway {
|
|||||||
|
|
||||||
// ---- Order actions -----------------------------------------------------
|
// ---- Order actions -----------------------------------------------------
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
const symbol = this.marketSymbol;
|
const symbol = this.marketSymbol;
|
||||||
const normalizedType = this.normalizeOrderType(params.type);
|
const normalizedType = this.normalizeOrderType(params.type);
|
||||||
@@ -399,11 +399,11 @@ export class BackpackGateway {
|
|||||||
|
|
||||||
// ---- Mapping helpers ---------------------------------------------------
|
// ---- Mapping helpers ---------------------------------------------------
|
||||||
|
|
||||||
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
|
private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
|
||||||
return this.mapBalanceToAccountSnapshotWithPositions(balance, []);
|
return this.mapBalanceToAccountSnapshotWithPositions(balance, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchAccountSnapshot(): Promise<AsterAccountSnapshot> {
|
private async fetchAccountSnapshot(): Promise<AccountSnapshot> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
const [balance, positions] = await Promise.all([
|
const [balance, positions] = await Promise.all([
|
||||||
this.exchange.fetchBalance(),
|
this.exchange.fetchBalance(),
|
||||||
@@ -417,7 +417,7 @@ export class BackpackGateway {
|
|||||||
return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []);
|
return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AsterAccountSnapshot {
|
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AccountSnapshot {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const assets = this.normalizeAssets(balance, now);
|
const assets = this.normalizeAssets(balance, now);
|
||||||
const positions = this.normalizePositions(rawPositions, now);
|
const positions = this.normalizePositions(rawPositions, now);
|
||||||
@@ -425,7 +425,7 @@ export class BackpackGateway {
|
|||||||
const totalUnrealized = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
|
const totalUnrealized = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
|
||||||
const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance));
|
const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance));
|
||||||
|
|
||||||
const snapshot: AsterAccountSnapshot = {
|
const snapshot: AccountSnapshot = {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
canWithdraw: true,
|
canWithdraw: true,
|
||||||
@@ -447,9 +447,9 @@ export class BackpackGateway {
|
|||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeAssets(balance: Balances, now: number): AsterAccountSnapshot["assets"] {
|
private normalizeAssets(balance: Balances, now: number): AccountSnapshot["assets"] {
|
||||||
const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]);
|
const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]);
|
||||||
const assets: AsterAccountSnapshot["assets"] = [];
|
const assets: AccountSnapshot["assets"] = [];
|
||||||
for (const [currency, value] of Object.entries(balance)) {
|
for (const [currency, value] of Object.entries(balance)) {
|
||||||
if (metaKeys.has(currency)) continue;
|
if (metaKeys.has(currency)) continue;
|
||||||
if (!value || typeof value !== "object") continue;
|
if (!value || typeof value !== "object") continue;
|
||||||
@@ -460,9 +460,9 @@ export class BackpackGateway {
|
|||||||
return assets;
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
|
private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
|
||||||
if (!Array.isArray(rawPositions)) return [];
|
if (!Array.isArray(rawPositions)) return [];
|
||||||
const positions: AsterAccountSnapshot["positions"] = [];
|
const positions: AccountSnapshot["positions"] = [];
|
||||||
for (const raw of rawPositions) {
|
for (const raw of rawPositions) {
|
||||||
const info = raw?.info ?? raw ?? {};
|
const info = raw?.info ?? raw ?? {};
|
||||||
const quantity = this.toNumber(raw?.contracts ?? info.netExposureQuantity ?? info.netQuantity);
|
const quantity = this.toNumber(raw?.contracts ?? info.netExposureQuantity ?? info.netQuantity);
|
||||||
@@ -493,7 +493,7 @@ export class BackpackGateway {
|
|||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
|
private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
|
||||||
return {
|
return {
|
||||||
lastUpdateId: orderbook.nonce || Date.now(),
|
lastUpdateId: orderbook.nonce || Date.now(),
|
||||||
bids: (orderbook.bids ?? [])
|
bids: (orderbook.bids ?? [])
|
||||||
@@ -506,7 +506,7 @@ export class BackpackGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
|
private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
|
||||||
return {
|
return {
|
||||||
symbol: ticker.symbol,
|
symbol: ticker.symbol,
|
||||||
lastPrice: ticker.last?.toString() ?? "0",
|
lastPrice: ticker.last?.toString() ?? "0",
|
||||||
@@ -522,7 +522,7 @@ export class BackpackGateway {
|
|||||||
private mapOHLCVToKline(
|
private mapOHLCVToKline(
|
||||||
candle: [number, number, number, number, number, number],
|
candle: [number, number, number, number, number, number],
|
||||||
interval: string
|
interval: string
|
||||||
): AsterKline {
|
): Kline {
|
||||||
const [openTime, open, high, low, close, volume] = candle;
|
const [openTime, open, high, low, close, volume] = candle;
|
||||||
return {
|
return {
|
||||||
openTime,
|
openTime,
|
||||||
@@ -536,7 +536,7 @@ export class BackpackGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapRestOrder(order: CcxtOrder): AsterOrder {
|
private mapRestOrder(order: CcxtOrder): Order {
|
||||||
const info = (order.info ?? {}) as Record<string, unknown>;
|
const info = (order.info ?? {}) as Record<string, unknown>;
|
||||||
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
|
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
|
||||||
let type = this.normalizeOrderType(order.type ?? (info.o as string)) as OrderType;
|
let type = this.normalizeOrderType(order.type ?? (info.o as string)) as OrderType;
|
||||||
@@ -579,7 +579,7 @@ export class BackpackGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapWsOrder(data: Record<string, unknown>): AsterOrder {
|
private mapWsOrder(data: Record<string, unknown>): Order {
|
||||||
const sideRaw = String(data.S ?? "").toUpperCase();
|
const sideRaw = String(data.S ?? "").toUpperCase();
|
||||||
const side: "BUY" | "SELL" = sideRaw === "BID" ? "BUY" : "SELL";
|
const side: "BUY" | "SELL" = sideRaw === "BID" ? "BUY" : "SELL";
|
||||||
const triggerPresent = data.P != null || data.B != null;
|
const triggerPresent = data.P != null || data.B != null;
|
||||||
@@ -626,7 +626,7 @@ export class BackpackGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapWsPosition(data: Record<string, unknown>): AsterAccountPosition | null {
|
private mapWsPosition(data: Record<string, unknown>): AccountPosition | null {
|
||||||
const quantityRaw = data.q ?? data.Q;
|
const quantityRaw = data.q ?? data.Q;
|
||||||
const qty = Number(this.toStringAmount(quantityRaw));
|
const qty = Number(this.toStringAmount(quantityRaw));
|
||||||
if (!Number.isFinite(qty)) return null;
|
if (!Number.isFinite(qty)) return null;
|
||||||
@@ -651,8 +651,8 @@ export class BackpackGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mergeWsPosition(position: AsterAccountPosition): void {
|
private mergeWsPosition(position: AccountPosition): void {
|
||||||
const snapshot: AsterAccountSnapshot = this.lastBalanceSnapshot
|
const snapshot: AccountSnapshot = this.lastBalanceSnapshot
|
||||||
? {
|
? {
|
||||||
...this.lastBalanceSnapshot,
|
...this.lastBalanceSnapshot,
|
||||||
positions: this.lastBalanceSnapshot.positions ? [...this.lastBalanceSnapshot.positions] : [],
|
positions: this.lastBalanceSnapshot.positions ? [...this.lastBalanceSnapshot.positions] : [],
|
||||||
@@ -692,7 +692,7 @@ export class BackpackGateway {
|
|||||||
this.emitAccount(snapshot);
|
this.emitAccount(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitAccount(snapshot: AsterAccountSnapshot): void {
|
private emitAccount(snapshot: AccountSnapshot): void {
|
||||||
for (const listener of this.accountListeners) {
|
for (const listener of this.accountListeners) {
|
||||||
try {
|
try {
|
||||||
listener(snapshot);
|
listener(snapshot);
|
||||||
|
|||||||
@@ -1,84 +1,16 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "Backpack",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTX",
|
||||||
}
|
supportsTrailingStop: false,
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
supportsTriggerType: false,
|
||||||
params.timeInForce = intent.timeInForce;
|
});
|
||||||
}
|
|
||||||
if (intent.reduceOnly !== undefined) {
|
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTX",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
throw new Error("Backpack exchange does not support trailing stop orders");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { clearTimeout, setTimeout } from "timers";
|
|
||||||
import type {
|
import type {
|
||||||
AccountListener,
|
AccountListener,
|
||||||
DepthListener,
|
DepthListener,
|
||||||
@@ -9,9 +8,9 @@ import type {
|
|||||||
OrderListener,
|
OrderListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { Order, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
|
||||||
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
|
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
|
||||||
|
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||||
|
|
||||||
export interface BinanceCredentials {
|
export interface BinanceCredentials {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@@ -32,11 +31,8 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
|||||||
private readonly gateway: BinanceGateway;
|
private readonly gateway: BinanceGateway;
|
||||||
private readonly symbol: string;
|
private readonly symbol: string;
|
||||||
private readonly marketType: "spot" | "perp" | "auto";
|
private readonly marketType: "spot" | "perp" | "auto";
|
||||||
private initPromise: Promise<void> | null = null;
|
private readonly safeInvoke = createSafeInvoke("BinanceExchangeAdapter");
|
||||||
private readonly initContexts = new Set<string>();
|
private readonly init: ReturnType<typeof createInitManager>;
|
||||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private retryDelayMs = 3000;
|
|
||||||
private lastInitErrorAt = 0;
|
|
||||||
|
|
||||||
constructor(credentials: BinanceCredentials = {}) {
|
constructor(credentials: BinanceCredentials = {}) {
|
||||||
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
|
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
|
||||||
@@ -61,6 +57,9 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
|||||||
futuresWsUrl: credentials.futuresWsUrl,
|
futuresWsUrl: credentials.futuresWsUrl,
|
||||||
logger: credentials.logger,
|
logger: credentials.logger,
|
||||||
});
|
});
|
||||||
|
this.init = createInitManager("BinanceExchangeAdapter", () =>
|
||||||
|
this.gateway.ensureInitialized(this.symbol),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
@@ -69,163 +68,94 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
|||||||
|
|
||||||
watchAccount(cb: AccountListener): void {
|
watchAccount(cb: AccountListener): void {
|
||||||
const safe = this.safeInvoke("watchAccount", cb);
|
const safe = this.safeInvoke("watchAccount", cb);
|
||||||
void this.ensureInitialized("watchAccount")
|
void this.init.ensureInitialized("watchAccount")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.gateway.onAccount(safe);
|
this.gateway.onAccount(safe);
|
||||||
})
|
});
|
||||||
.catch((error) => this.handleInitError("watchAccount", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: OrderListener): void {
|
watchOrders(cb: OrderListener): void {
|
||||||
const safe = this.safeInvoke("watchOrders", cb);
|
const safe = this.safeInvoke("watchOrders", cb);
|
||||||
void this.ensureInitialized("watchOrders")
|
void this.init.ensureInitialized("watchOrders")
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.gateway.onOrders(safe);
|
this.gateway.onOrders(safe);
|
||||||
})
|
});
|
||||||
.catch((error) => this.handleInitError("watchOrders", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(symbol: string, cb: DepthListener): void {
|
watchDepth(symbol: string, cb: DepthListener): void {
|
||||||
const safe = this.safeInvoke("watchDepth", cb);
|
const safe = this.safeInvoke("watchDepth", cb);
|
||||||
void this.ensureInitialized(`watchDepth:${symbol}`)
|
void this.init.ensureInitialized(`watchDepth:${symbol}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.gateway.onDepth(symbol, safe);
|
this.gateway.onDepth(symbol, safe);
|
||||||
})
|
});
|
||||||
.catch((error) => this.handleInitError("watchDepth", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(symbol: string, cb: TickerListener): void {
|
watchTicker(symbol: string, cb: TickerListener): void {
|
||||||
const safe = this.safeInvoke("watchTicker", cb);
|
const safe = this.safeInvoke("watchTicker", cb);
|
||||||
void this.ensureInitialized(`watchTicker:${symbol}`)
|
void this.init.ensureInitialized(`watchTicker:${symbol}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.gateway.onTicker(symbol, safe);
|
this.gateway.onTicker(symbol, safe);
|
||||||
})
|
});
|
||||||
.catch((error) => this.handleInitError("watchTicker", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||||
const safe = this.safeInvoke("watchKlines", cb);
|
const safe = this.safeInvoke("watchKlines", cb);
|
||||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`)
|
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.gateway.onKlines(symbol, interval, safe);
|
this.gateway.onKlines(symbol, interval, safe);
|
||||||
})
|
});
|
||||||
.catch((error) => this.handleInitError("watchKlines", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||||
const safe = this.safeInvoke("watchFundingRate", cb);
|
const safe = this.safeInvoke("watchFundingRate", cb);
|
||||||
void this.ensureInitialized(`watchFundingRate:${symbol}`)
|
void this.init.ensureInitialized(`watchFundingRate:${symbol}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.gateway.onFundingRate(symbol, safe);
|
this.gateway.onFundingRate(symbol, safe);
|
||||||
})
|
});
|
||||||
.catch((error) => this.handleInitError("watchFundingRate", error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized("createOrder");
|
await this.init.ensureInitialized("createOrder");
|
||||||
return this.gateway.createOrder(params);
|
return this.gateway.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrder");
|
await this.init.ensureInitialized("cancelOrder");
|
||||||
await this.gateway.cancelOrder(params);
|
await this.gateway.cancelOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrders");
|
await this.init.ensureInitialized("cancelOrders");
|
||||||
await this.gateway.cancelOrders(params);
|
await this.gateway.cancelOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelAllOrders");
|
await this.init.ensureInitialized("cancelAllOrders");
|
||||||
await this.gateway.cancelAllOrders(params);
|
await this.gateway.cancelAllOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrecision(): Promise<ExchangePrecision | null> {
|
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||||
await this.ensureInitialized("getPrecision");
|
await this.init.ensureInitialized("getPrecision");
|
||||||
return this.gateway.getPrecision(this.symbol);
|
return this.gateway.getPrecision(this.symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
async queryOpenOrders(): Promise<Order[]> {
|
||||||
await this.ensureInitialized("queryOpenOrders");
|
await this.init.ensureInitialized("queryOpenOrders");
|
||||||
return this.gateway.queryOpenOrders();
|
return this.gateway.queryOpenOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryAccountSnapshot() {
|
async queryAccountSnapshot() {
|
||||||
await this.ensureInitialized("queryAccountSnapshot");
|
await this.init.ensureInitialized("queryAccountSnapshot");
|
||||||
return this.gateway.queryAccountSnapshot();
|
return this.gateway.queryAccountSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||||
await this.ensureInitialized("changeMarginMode");
|
await this.init.ensureInitialized("changeMarginMode");
|
||||||
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async forceCancelAllOrders(): Promise<boolean> {
|
async forceCancelAllOrders(): Promise<boolean> {
|
||||||
await this.ensureInitialized("forceCancelAllOrders");
|
await this.init.ensureInitialized("forceCancelAllOrders");
|
||||||
return this.gateway.forceCancelAllOrders();
|
return this.gateway.forceCancelAllOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
|
||||||
const wrapped = ((...args: any[]) => {
|
|
||||||
try {
|
|
||||||
cb(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[BinanceExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureInitialized(context?: string): Promise<void> {
|
|
||||||
if (!this.initPromise) {
|
|
||||||
this.initContexts.clear();
|
|
||||||
this.initPromise = this.gateway
|
|
||||||
.ensureInitialized(this.symbol)
|
|
||||||
.then((value) => {
|
|
||||||
this.clearRetry();
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.handleInitError("initialize", error);
|
|
||||||
this.initPromise = null;
|
|
||||||
this.scheduleRetry();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context && !this.initContexts.has(context)) {
|
|
||||||
this.initContexts.add(context);
|
|
||||||
this.initPromise.catch((error) => {
|
|
||||||
this.handleInitError(context, error);
|
|
||||||
this.scheduleRetry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRetry(): void {
|
|
||||||
if (this.retryTimer) return;
|
|
||||||
this.retryTimer = setTimeout(() => {
|
|
||||||
this.retryTimer = null;
|
|
||||||
if (this.initPromise) return;
|
|
||||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
|
||||||
void this.ensureInitialized("retry");
|
|
||||||
}, this.retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRetry(): void {
|
|
||||||
if (this.retryTimer) {
|
|
||||||
clearTimeout(this.retryTimer);
|
|
||||||
this.retryTimer = null;
|
|
||||||
}
|
|
||||||
this.retryDelayMs = 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitError(context: string, error: unknown): void {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastInitErrorAt < 5000) return;
|
|
||||||
this.lastInitErrorAt = now;
|
|
||||||
console.error(`[BinanceExchangeAdapter] ${context} failed`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import axios from "axios";
|
|||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import NodeWebSocket from "ws";
|
import NodeWebSocket from "ws";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountAsset,
|
AccountAsset,
|
||||||
AsterAccountPosition,
|
AccountPosition,
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
OrderType,
|
OrderType,
|
||||||
PositionSide,
|
PositionSide,
|
||||||
@@ -225,9 +225,9 @@ export class BinanceGateway {
|
|||||||
|
|
||||||
private readonly accountListeners = new Set<AccountListener>();
|
private readonly accountListeners = new Set<AccountListener>();
|
||||||
private readonly orderListeners = new Set<OrderListener>();
|
private readonly orderListeners = new Set<OrderListener>();
|
||||||
private readonly depthSubs = new Map<string, PublicSubscription<AsterDepth>>();
|
private readonly depthSubs = new Map<string, PublicSubscription<Depth>>();
|
||||||
private readonly tickerSubs = new Map<string, PublicSubscription<AsterTicker>>();
|
private readonly tickerSubs = new Map<string, PublicSubscription<Ticker>>();
|
||||||
private readonly klineSubs = new Map<string, PublicSubscription<AsterKline[]>>();
|
private readonly klineSubs = new Map<string, PublicSubscription<Kline[]>>();
|
||||||
private readonly fundingSubs = new Map<string, PublicSubscription<{ symbol: string; fundingRate: number; updateTime: number }>>();
|
private readonly fundingSubs = new Map<string, PublicSubscription<{ symbol: string; fundingRate: number; updateTime: number }>>();
|
||||||
|
|
||||||
private accountPollTimer: ReturnType<typeof setInterval> | null = null;
|
private accountPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -254,15 +254,15 @@ export class BinanceGateway {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly localOrders = new Map<string, AsterOrder>();
|
private readonly localOrders = new Map<string, Order>();
|
||||||
|
|
||||||
private readonly spotBalances = new Map<string, { free: number; locked: number }>();
|
private readonly spotBalances = new Map<string, { free: number; locked: number }>();
|
||||||
private readonly perpBalances = new Map<string, { wallet: number; available: number }>();
|
private readonly perpBalances = new Map<string, { wallet: number; available: number }>();
|
||||||
private readonly perpPositions = new Map<string, AsterAccountPosition>();
|
private readonly perpPositions = new Map<string, AccountPosition>();
|
||||||
private readonly lastMarkPriceBySymbol = new Map<string, number>();
|
private readonly lastMarkPriceBySymbol = new Map<string, number>();
|
||||||
|
|
||||||
private lastSpotSnapshot: AsterAccountSnapshot | null = null;
|
private lastSpotSnapshot: AccountSnapshot | null = null;
|
||||||
private lastPerpSnapshot: AsterAccountSnapshot | null = null;
|
private lastPerpSnapshot: AccountSnapshot | null = null;
|
||||||
|
|
||||||
constructor(options: BinanceGatewayOptions) {
|
constructor(options: BinanceGatewayOptions) {
|
||||||
this.apiKey = options.apiKey;
|
this.apiKey = options.apiKey;
|
||||||
@@ -465,7 +465,7 @@ export class BinanceGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized(params.symbol);
|
await this.ensureInitialized(params.symbol);
|
||||||
const market = this.resolveMarket(params.symbol);
|
const market = this.resolveMarket(params.symbol);
|
||||||
const exchange = this.getExchange(market.kind);
|
const exchange = this.getExchange(market.kind);
|
||||||
@@ -487,6 +487,9 @@ export class BinanceGateway {
|
|||||||
if (params.callbackRate != null) {
|
if (params.callbackRate != null) {
|
||||||
extra.callbackRate = params.callbackRate;
|
extra.callbackRate = params.callbackRate;
|
||||||
}
|
}
|
||||||
|
if (params.clientOrderId != null) {
|
||||||
|
extra.newClientOrderId = params.clientOrderId;
|
||||||
|
}
|
||||||
|
|
||||||
if (market.kind === "perp") {
|
if (market.kind === "perp") {
|
||||||
if (params.reduceOnly != null) {
|
if (params.reduceOnly != null) {
|
||||||
@@ -601,10 +604,10 @@ export class BinanceGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
async queryOpenOrders(): Promise<Order[]> {
|
||||||
await this.ensureInitialized(this.defaultSymbol);
|
await this.ensureInitialized(this.defaultSymbol);
|
||||||
const kinds = this.getPrivateKinds();
|
const kinds = this.getPrivateKinds();
|
||||||
const result: AsterOrder[] = [];
|
const result: Order[] = [];
|
||||||
for (const kind of kinds) {
|
for (const kind of kinds) {
|
||||||
const exchange = this.getExchange(kind);
|
const exchange = this.getExchange(kind);
|
||||||
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
|
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
|
||||||
@@ -619,7 +622,7 @@ export class BinanceGateway {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
await this.ensureInitialized(this.defaultSymbol);
|
await this.ensureInitialized(this.defaultSymbol);
|
||||||
const kinds = this.getPrivateKinds();
|
const kinds = this.getPrivateKinds();
|
||||||
for (const kind of kinds) {
|
for (const kind of kinds) {
|
||||||
@@ -910,7 +913,7 @@ export class BinanceGateway {
|
|||||||
this.lastPerpSnapshot = this.buildPerpSnapshot();
|
this.lastPerpSnapshot = this.buildPerpSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapSpotExecutionReport(payload: any): AsterOrder | null {
|
private mapSpotExecutionReport(payload: any): Order | null {
|
||||||
const order = payload;
|
const order = payload;
|
||||||
const symbolRaw = String(order?.s ?? "").toUpperCase();
|
const symbolRaw = String(order?.s ?? "").toUpperCase();
|
||||||
if (!symbolRaw) return null;
|
if (!symbolRaw) return null;
|
||||||
@@ -940,7 +943,7 @@ export class BinanceGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapPerpOrderTradeUpdate(payload: any): AsterOrder | null {
|
private mapPerpOrderTradeUpdate(payload: any): Order | null {
|
||||||
const order = payload?.o;
|
const order = payload?.o;
|
||||||
if (!order) return null;
|
if (!order) return null;
|
||||||
const symbolRaw = String(order?.s ?? "").toUpperCase();
|
const symbolRaw = String(order?.s ?? "").toUpperCase();
|
||||||
@@ -1111,7 +1114,7 @@ export class BinanceGateway {
|
|||||||
(ws as any).onerror = (event: any) => onError(event?.error ?? event);
|
(ws as any).onerror = (event: any) => onError(event?.error ?? event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapDepthPayload(payload: unknown, market: BinanceMarketRef): AsterDepth | null {
|
private mapDepthPayload(payload: unknown, market: BinanceMarketRef): Depth | null {
|
||||||
const data = payload as any;
|
const data = payload as any;
|
||||||
const bids = Array.isArray(data?.b) ? data.b : [];
|
const bids = Array.isArray(data?.b) ? data.b : [];
|
||||||
const asks = Array.isArray(data?.a) ? data.a : [];
|
const asks = Array.isArray(data?.a) ? data.a : [];
|
||||||
@@ -1129,7 +1132,7 @@ export class BinanceGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTickerPayload(payload: unknown, market: BinanceMarketRef): AsterTicker | null {
|
private mapTickerPayload(payload: unknown, market: BinanceMarketRef): Ticker | null {
|
||||||
const data = payload as any;
|
const data = payload as any;
|
||||||
const lastPrice = String(data?.c ?? "");
|
const lastPrice = String(data?.c ?? "");
|
||||||
if (!lastPrice) return null;
|
if (!lastPrice) return null;
|
||||||
@@ -1156,7 +1159,7 @@ export class BinanceGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapKlinePayload(payload: unknown, interval: string): AsterKline[] | null {
|
private mapKlinePayload(payload: unknown, interval: string): Kline[] | null {
|
||||||
const data = payload as any;
|
const data = payload as any;
|
||||||
const kline = data?.k;
|
const kline = data?.k;
|
||||||
if (!kline) return null;
|
if (!kline) return null;
|
||||||
@@ -1288,7 +1291,7 @@ export class BinanceGateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async attachPerpPositions(): Promise<void> {
|
private async attachPerpPositions(): Promise<void> {
|
||||||
const next = new Map<string, AsterAccountPosition>();
|
const next = new Map<string, AccountPosition>();
|
||||||
try {
|
try {
|
||||||
const raw = (await this.perpExchange.fetchPositions()) as any[];
|
const raw = (await this.perpExchange.fetchPositions()) as any[];
|
||||||
for (const row of raw ?? []) {
|
for (const row of raw ?? []) {
|
||||||
@@ -1325,7 +1328,7 @@ export class BinanceGateway {
|
|||||||
private async fetchAndUpdateOrders(kind: MarketKind): Promise<void> {
|
private async fetchAndUpdateOrders(kind: MarketKind): Promise<void> {
|
||||||
const exchange = this.getExchange(kind);
|
const exchange = this.getExchange(kind);
|
||||||
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
|
const openOrders = (await exchange.fetchOpenOrders()) as CcxtOrder[];
|
||||||
const remote = new Map<string, AsterOrder>();
|
const remote = new Map<string, Order>();
|
||||||
for (const order of openOrders) {
|
for (const order of openOrders) {
|
||||||
const market = this.resolveMarketByCcxtSymbol(kind, String(order?.symbol ?? ""));
|
const market = this.resolveMarketByCcxtSymbol(kind, String(order?.symbol ?? ""));
|
||||||
const mapped = this.mapCcxtOrder(order, kind, market?.id ?? String(order?.symbol ?? ""));
|
const mapped = this.mapCcxtOrder(order, kind, market?.id ?? String(order?.symbol ?? ""));
|
||||||
@@ -1367,7 +1370,7 @@ export class BinanceGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private upsertOrder(order: AsterOrder, kind: MarketKind, marketId: string): void {
|
private upsertOrder(order: Order, kind: MarketKind, marketId: string): void {
|
||||||
const id = String(order.orderId);
|
const id = String(order.orderId);
|
||||||
const symbol = this.resolveDisplaySymbol(kind, marketId);
|
const symbol = this.resolveDisplaySymbol(kind, marketId);
|
||||||
const normalized = { ...order, symbol };
|
const normalized = { ...order, symbol };
|
||||||
@@ -1402,7 +1405,7 @@ export class BinanceGateway {
|
|||||||
if (changed) this.emitOrders();
|
if (changed) this.emitOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
private isOrderActive(order: AsterOrder): boolean {
|
private isOrderActive(order: Order): boolean {
|
||||||
const status = String(order.status ?? "").toUpperCase();
|
const status = String(order.status ?? "").toUpperCase();
|
||||||
if (!status) return true;
|
if (!status) return true;
|
||||||
if (status === "FILLED" || status === "CANCELED" || status === "CANCELLED" || status === "REJECTED" || status === "EXPIRED") {
|
if (status === "FILLED" || status === "CANCELED" || status === "CANCELLED" || status === "REJECTED" || status === "EXPIRED") {
|
||||||
@@ -1412,7 +1415,7 @@ export class BinanceGateway {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCombinedAccountSnapshot(): AsterAccountSnapshot | null {
|
private buildCombinedAccountSnapshot(): AccountSnapshot | null {
|
||||||
const kinds = this.getPrivateKinds();
|
const kinds = this.getPrivateKinds();
|
||||||
if (kinds.length === 0) return null;
|
if (kinds.length === 0) return null;
|
||||||
if (kinds.length === 1) {
|
if (kinds.length === 1) {
|
||||||
@@ -1421,7 +1424,7 @@ export class BinanceGateway {
|
|||||||
|
|
||||||
const spot = this.buildSpotSnapshot();
|
const spot = this.buildSpotSnapshot();
|
||||||
const perp = this.buildPerpSnapshot();
|
const perp = this.buildPerpSnapshot();
|
||||||
const perpAssetsTagged: AsterAccountAsset[] = perp.assets.map((asset) => ({
|
const perpAssetsTagged: AccountAsset[] = perp.assets.map((asset) => ({
|
||||||
...asset,
|
...asset,
|
||||||
asset: `${asset.asset}0`,
|
asset: `${asset.asset}0`,
|
||||||
}));
|
}));
|
||||||
@@ -1441,8 +1444,8 @@ export class BinanceGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildSpotSnapshot(): AsterAccountSnapshot {
|
private buildSpotSnapshot(): AccountSnapshot {
|
||||||
const assets: AsterAccountAsset[] = [];
|
const assets: AccountAsset[] = [];
|
||||||
let totalWallet = 0;
|
let totalWallet = 0;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [asset, balance] of this.spotBalances.entries()) {
|
for (const [asset, balance] of this.spotBalances.entries()) {
|
||||||
@@ -1471,9 +1474,9 @@ export class BinanceGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildPerpSnapshot(): AsterAccountSnapshot {
|
private buildPerpSnapshot(): AccountSnapshot {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const assets: AsterAccountAsset[] = [];
|
const assets: AccountAsset[] = [];
|
||||||
let totalWallet = 0;
|
let totalWallet = 0;
|
||||||
for (const [asset, balance] of this.perpBalances.entries()) {
|
for (const [asset, balance] of this.perpBalances.entries()) {
|
||||||
totalWallet += balance.wallet;
|
totalWallet += balance.wallet;
|
||||||
@@ -1506,7 +1509,7 @@ export class BinanceGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapCcxtOrder(order: CcxtOrder, kind: MarketKind, marketId: string): AsterOrder {
|
private mapCcxtOrder(order: CcxtOrder, kind: MarketKind, marketId: string): Order {
|
||||||
const symbol = this.resolveDisplaySymbol(kind, marketId);
|
const symbol = this.resolveDisplaySymbol(kind, marketId);
|
||||||
const side = String(order.side ?? "buy").toUpperCase() === "SELL" ? "SELL" : "BUY";
|
const side = String(order.side ?? "buy").toUpperCase() === "SELL" ? "SELL" : "BUY";
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,100 +1,16 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "Binance",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTX",
|
||||||
}
|
supportsTrailingStop: true,
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
supportsTriggerType: true,
|
||||||
params.timeInForce = intent.timeInForce;
|
});
|
||||||
}
|
|
||||||
if (intent.reduceOnly !== undefined) {
|
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
|
||||||
}
|
|
||||||
if (intent.closePosition !== undefined) {
|
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTX",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
triggerType: intent.triggerType,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "TRAILING_STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
activationPrice: intent.activationPrice,
|
|
||||||
callbackRate: intent.callbackRate,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExchangeAdapter } from "./adapter";
|
import type { ExchangeAdapter } from "./adapter";
|
||||||
import { AsterExchangeAdapter, type AsterCredentials } from "./aster-adapter";
|
import { AsterExchangeAdapter, type AsterCredentials } from "./aster/adapter";
|
||||||
import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
|
import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter";
|
||||||
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
|
import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter";
|
||||||
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
|
import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter";
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import type {
|
|||||||
TickerListener,
|
TickerListener,
|
||||||
} from "./adapter";
|
} from "./adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterOrder,
|
Order,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export class DryRunExchangeAdapter implements ExchangeAdapter {
|
|||||||
this.inner.watchFundingRate(symbol, cb);
|
this.inner.watchFundingRate(symbol, cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
this.record("createOrder", params);
|
this.record("createOrder", params);
|
||||||
return createSyntheticOrder(params, ++this.syntheticCounter);
|
return createSyntheticOrder(params, ++this.syntheticCounter);
|
||||||
}
|
}
|
||||||
@@ -104,12 +104,12 @@ export class DryRunExchangeAdapter implements ExchangeAdapter {
|
|||||||
this.inner.offRestHealthEvent?.(listener);
|
this.inner.offRestHealthEvent?.(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
async queryOpenOrders(): Promise<Order[]> {
|
||||||
if (!this.inner.queryOpenOrders) return [];
|
if (!this.inner.queryOpenOrders) return [];
|
||||||
return this.inner.queryOpenOrders();
|
return this.inner.queryOpenOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
if (!this.inner.queryAccountSnapshot) return null;
|
if (!this.inner.queryAccountSnapshot) return null;
|
||||||
return this.inner.queryAccountSnapshot();
|
return this.inner.queryAccountSnapshot();
|
||||||
}
|
}
|
||||||
@@ -128,12 +128,12 @@ export class DryRunExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSyntheticOrder(params: CreateOrderParams, counter: number): AsterOrder {
|
function createSyntheticOrder(params: CreateOrderParams, counter: number): Order {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const orderId = `dry-run-${now}-${counter}`;
|
const orderId = params.clientOrderId ?? `dry-run-${now}-${counter}`;
|
||||||
return {
|
return {
|
||||||
orderId,
|
orderId,
|
||||||
clientOrderId: orderId,
|
clientOrderId: params.clientOrderId ?? orderId,
|
||||||
symbol: params.symbol,
|
symbol: params.symbol,
|
||||||
side: params.side,
|
side: params.side,
|
||||||
type: params.type,
|
type: params.type,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { setTimeout, clearTimeout } from "timers";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { createRequire } from "module";
|
import { createRequire } from "module";
|
||||||
import type {
|
import type {
|
||||||
@@ -9,8 +8,7 @@ import type {
|
|||||||
OrderListener,
|
OrderListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { Order, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
|
||||||
import {
|
import {
|
||||||
GrvtGateway,
|
GrvtGateway,
|
||||||
type GrvtEnvironment,
|
type GrvtEnvironment,
|
||||||
@@ -18,6 +16,7 @@ import {
|
|||||||
type GrvtHostsOverride,
|
type GrvtHostsOverride,
|
||||||
type GrvtSignatureProvider,
|
type GrvtSignatureProvider,
|
||||||
} from "./gateway";
|
} from "./gateway";
|
||||||
|
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||||
|
|
||||||
export interface GrvtCredentials {
|
export interface GrvtCredentials {
|
||||||
cookie?: string;
|
cookie?: string;
|
||||||
@@ -40,11 +39,8 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
|||||||
private readonly gateway: GrvtGateway;
|
private readonly gateway: GrvtGateway;
|
||||||
private readonly symbol: string;
|
private readonly symbol: string;
|
||||||
private readonly instrument: string;
|
private readonly instrument: string;
|
||||||
private initPromise: Promise<void> | null = null;
|
private readonly safeInvoke = createSafeInvoke("GrvtExchangeAdapter");
|
||||||
private readonly initContexts = new Set<string>();
|
private readonly init: ReturnType<typeof createInitManager>;
|
||||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private retryDelayMs = 3000;
|
|
||||||
private lastInitErrorAt = 0;
|
|
||||||
private klineInterval = "1m";
|
private klineInterval = "1m";
|
||||||
|
|
||||||
constructor(credentials: GrvtCredentials = {}) {
|
constructor(credentials: GrvtCredentials = {}) {
|
||||||
@@ -91,6 +87,9 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
|||||||
pollIntervals: credentials.pollIntervals,
|
pollIntervals: credentials.pollIntervals,
|
||||||
logger: credentials.logger,
|
logger: credentials.logger,
|
||||||
});
|
});
|
||||||
|
this.init = createInitManager("GrvtExchangeAdapter", () =>
|
||||||
|
this.gateway.ensureInitialized(this.klineInterval),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
@@ -98,115 +97,50 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(cb: AccountListener): void {
|
watchAccount(cb: AccountListener): void {
|
||||||
void this.ensureInitialized("watchAccount");
|
void this.init.ensureInitialized("watchAccount");
|
||||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: OrderListener): void {
|
watchOrders(cb: OrderListener): void {
|
||||||
void this.ensureInitialized("watchOrders");
|
void this.init.ensureInitialized("watchOrders");
|
||||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||||
void this.ensureInitialized("watchDepth");
|
void this.init.ensureInitialized("watchDepth");
|
||||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||||
void this.ensureInitialized("watchTicker");
|
void this.init.ensureInitialized("watchTicker");
|
||||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||||
this.klineInterval = interval ?? this.klineInterval;
|
this.klineInterval = interval ?? this.klineInterval;
|
||||||
void this.ensureInitialized("watchKlines", this.klineInterval);
|
void this.init.ensureInitialized("watchKlines");
|
||||||
this.gateway.onKlines(this.safeInvoke("watchKlines", cb));
|
this.gateway.onKlines(this.safeInvoke("watchKlines", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized("createOrder");
|
await this.init.ensureInitialized("createOrder");
|
||||||
return this.gateway.createOrder(params);
|
return this.gateway.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrder");
|
await this.init.ensureInitialized("cancelOrder");
|
||||||
await this.gateway.cancelOrder(params);
|
await this.gateway.cancelOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrders");
|
await this.init.ensureInitialized("cancelOrders");
|
||||||
await this.gateway.cancelOrders(params);
|
await this.gateway.cancelOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelAllOrders");
|
await this.init.ensureInitialized("cancelAllOrders");
|
||||||
await this.gateway.cancelAllOrders();
|
await this.gateway.cancelAllOrders();
|
||||||
}
|
}
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
|
||||||
const wrapped = ((...args: any[]) => {
|
|
||||||
try {
|
|
||||||
cb(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[GrvtExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureInitialized(context?: string, interval?: string): Promise<void> {
|
|
||||||
if (interval) {
|
|
||||||
this.klineInterval = interval;
|
|
||||||
}
|
|
||||||
if (!this.initPromise) {
|
|
||||||
this.initContexts.clear();
|
|
||||||
this.initPromise = this.gateway
|
|
||||||
.ensureInitialized(this.klineInterval)
|
|
||||||
.then((value) => {
|
|
||||||
this.clearRetry();
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.handleInitError("initialize", error);
|
|
||||||
this.initPromise = null;
|
|
||||||
this.scheduleRetry();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (context && !this.initContexts.has(context)) {
|
|
||||||
this.initContexts.add(context);
|
|
||||||
this.initPromise.catch((error) => {
|
|
||||||
this.handleInitError(context, error);
|
|
||||||
this.scheduleRetry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRetry(): void {
|
|
||||||
if (this.retryTimer) return;
|
|
||||||
this.retryTimer = setTimeout(() => {
|
|
||||||
this.retryTimer = null;
|
|
||||||
if (this.initPromise) return;
|
|
||||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
|
||||||
void this.ensureInitialized("retry");
|
|
||||||
}, this.retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRetry(): void {
|
|
||||||
if (this.retryTimer) {
|
|
||||||
clearTimeout(this.retryTimer);
|
|
||||||
this.retryTimer = null;
|
|
||||||
}
|
|
||||||
this.retryDelayMs = 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitError(context: string, error: unknown): void {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastInitErrorAt < 5000) return;
|
|
||||||
this.lastInitErrorAt = now;
|
|
||||||
console.error(`[GrvtExchangeAdapter] ${context} failed`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function requireValue<T>(value: T | undefined | null, key: string): T {
|
function requireValue<T>(value: T | undefined | null, key: string): T {
|
||||||
|
|||||||
@@ -23,21 +23,23 @@ import type {
|
|||||||
IOrder,
|
IOrder,
|
||||||
} from "@grvt/client/interfaces";
|
} from "@grvt/client/interfaces";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterAccountPosition,
|
AccountPosition,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
OrderSide,
|
OrderSide,
|
||||||
|
} from "../types";
|
||||||
|
import type {
|
||||||
GrvtSignedOrder,
|
GrvtSignedOrder,
|
||||||
GrvtSignature,
|
GrvtSignature,
|
||||||
GrvtUnsignedOrder,
|
GrvtUnsignedOrder,
|
||||||
GrvtTimeInForce,
|
GrvtTimeInForce,
|
||||||
GrvtOrderMetadataInput,
|
GrvtOrderMetadataInput,
|
||||||
GrvtTriggerMetadata,
|
GrvtTriggerMetadata,
|
||||||
} from "../types";
|
} from "./types";
|
||||||
|
|
||||||
const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000;
|
const DEFAULT_ACCOUNT_POLL_INTERVAL_MS = 5000;
|
||||||
const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500;
|
const DEFAULT_ORDERS_POLL_INTERVAL_MS = 2500;
|
||||||
@@ -172,7 +174,7 @@ interface HostsConfig {
|
|||||||
|
|
||||||
interface KlineCache {
|
interface KlineCache {
|
||||||
interval: string;
|
interval: string;
|
||||||
values: AsterKline[];
|
values: Kline[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InstrumentInfo {
|
interface InstrumentInfo {
|
||||||
@@ -215,12 +217,12 @@ export class GrvtGateway {
|
|||||||
private instrumentInfo: InstrumentInfo | null = null;
|
private instrumentInfo: InstrumentInfo | null = null;
|
||||||
private sessionPromise: Promise<void> | null = null;
|
private sessionPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
private positions: AsterAccountPosition[] = [];
|
private positions: AccountPosition[] = [];
|
||||||
private lastPositionsUpdateAt: number = 0;
|
private lastPositionsUpdateAt: number = 0;
|
||||||
private depthSnapshot: AsterDepth | null = null;
|
private depthSnapshot: Depth | null = null;
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
private klineCache: KlineCache | null = null;
|
private klineCache: KlineCache | null = null;
|
||||||
|
|
||||||
// WebSocket state
|
// WebSocket state
|
||||||
@@ -228,11 +230,11 @@ export class GrvtGateway {
|
|||||||
private wsConnected = false;
|
private wsConnected = false;
|
||||||
private positionsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
private positionsRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
private accountListeners = new Set<(snapshot: AsterAccountSnapshot) => void>();
|
private accountListeners = new Set<(snapshot: AccountSnapshot) => void>();
|
||||||
private ordersListeners = new Set<(orders: AsterOrder[]) => void>();
|
private ordersListeners = new Set<(orders: Order[]) => void>();
|
||||||
private depthListeners = new Set<(depth: AsterDepth) => void>();
|
private depthListeners = new Set<(depth: Depth) => void>();
|
||||||
private tickerListeners = new Set<(ticker: AsterTicker) => void>();
|
private tickerListeners = new Set<(ticker: Ticker) => void>();
|
||||||
private klineListeners = new Set<(klines: AsterKline[]) => void>();
|
private klineListeners = new Set<(klines: Kline[]) => void>();
|
||||||
|
|
||||||
private accountTimer: ReturnType<typeof setInterval> | null = null;
|
private accountTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
private ordersTimer: ReturnType<typeof setInterval> | null = null;
|
private ordersTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -316,7 +318,7 @@ export class GrvtGateway {
|
|||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccount(listener: (snapshot: AsterAccountSnapshot) => void): () => void {
|
onAccount(listener: (snapshot: AccountSnapshot) => void): () => void {
|
||||||
this.accountListeners.add(listener);
|
this.accountListeners.add(listener);
|
||||||
if (this.accountSnapshot) listener(cloneAccount(this.accountSnapshot));
|
if (this.accountSnapshot) listener(cloneAccount(this.accountSnapshot));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -324,7 +326,7 @@ export class GrvtGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onOrders(listener: (orders: AsterOrder[]) => void): () => void {
|
onOrders(listener: (orders: Order[]) => void): () => void {
|
||||||
this.ordersListeners.add(listener);
|
this.ordersListeners.add(listener);
|
||||||
if (this.openOrders.length) listener(cloneOrders(this.openOrders));
|
if (this.openOrders.length) listener(cloneOrders(this.openOrders));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -332,7 +334,7 @@ export class GrvtGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onDepth(listener: (depth: AsterDepth) => void): () => void {
|
onDepth(listener: (depth: Depth) => void): () => void {
|
||||||
this.depthListeners.add(listener);
|
this.depthListeners.add(listener);
|
||||||
if (this.depthSnapshot) listener(cloneDepth(this.depthSnapshot));
|
if (this.depthSnapshot) listener(cloneDepth(this.depthSnapshot));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -340,7 +342,7 @@ export class GrvtGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onTicker(listener: (ticker: AsterTicker) => void): () => void {
|
onTicker(listener: (ticker: Ticker) => void): () => void {
|
||||||
this.tickerListeners.add(listener);
|
this.tickerListeners.add(listener);
|
||||||
if (this.tickerSnapshot) listener(cloneTicker(this.tickerSnapshot));
|
if (this.tickerSnapshot) listener(cloneTicker(this.tickerSnapshot));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -348,7 +350,7 @@ export class GrvtGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onKlines(listener: (klines: AsterKline[]) => void): () => void {
|
onKlines(listener: (klines: Kline[]) => void): () => void {
|
||||||
this.klineListeners.add(listener);
|
this.klineListeners.add(listener);
|
||||||
if (this.klineCache) listener(cloneKlines(this.klineCache.values));
|
if (this.klineCache) listener(cloneKlines(this.klineCache.values));
|
||||||
return () => {
|
return () => {
|
||||||
@@ -356,19 +358,19 @@ export class GrvtGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccountSnapshot(): AsterAccountSnapshot | null {
|
getAccountSnapshot(): AccountSnapshot | null {
|
||||||
return this.accountSnapshot ? cloneAccount(this.accountSnapshot) : null;
|
return this.accountSnapshot ? cloneAccount(this.accountSnapshot) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenOrders(): AsterOrder[] {
|
getOpenOrders(): Order[] {
|
||||||
return cloneOrders(this.openOrders);
|
return cloneOrders(this.openOrders);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPositions(): AsterAccountPosition[] {
|
getPositions(): AccountPosition[] {
|
||||||
return this.positions.map((position) => ({ ...position }));
|
return this.positions.map((position) => ({ ...position }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
if (params.type === "TRAILING_STOP_MARKET") {
|
if (params.type === "TRAILING_STOP_MARKET") {
|
||||||
throw new Error(TRAILING_NOT_SUPPORTED_ERROR);
|
throw new Error(TRAILING_NOT_SUPPORTED_ERROR);
|
||||||
}
|
}
|
||||||
@@ -660,7 +662,7 @@ export class GrvtGateway {
|
|||||||
this.emitKlines(klines);
|
this.emitKlines(klines);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mergeOrder(order: AsterOrder): void {
|
private mergeOrder(order: Order): void {
|
||||||
const index = this.openOrders.findIndex((item) => String(item.orderId) === String(order.orderId));
|
const index = this.openOrders.findIndex((item) => String(item.orderId) === String(order.orderId));
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.openOrders[index] = order;
|
this.openOrders[index] = order;
|
||||||
@@ -678,7 +680,7 @@ export class GrvtGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitAccount(snapshot: AsterAccountSnapshot): void {
|
private emitAccount(snapshot: AccountSnapshot): void {
|
||||||
const cloned = cloneAccount(snapshot);
|
const cloned = cloneAccount(snapshot);
|
||||||
this.accountListeners.forEach((listener) => {
|
this.accountListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
@@ -689,7 +691,7 @@ export class GrvtGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitOrders(orders: AsterOrder[]): void {
|
private emitOrders(orders: Order[]): void {
|
||||||
const cloned = cloneOrders(orders);
|
const cloned = cloneOrders(orders);
|
||||||
this.ordersListeners.forEach((listener) => {
|
this.ordersListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
@@ -700,7 +702,7 @@ export class GrvtGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitDepth(depth: AsterDepth): void {
|
private emitDepth(depth: Depth): void {
|
||||||
const cloned = cloneDepth(depth);
|
const cloned = cloneDepth(depth);
|
||||||
this.depthListeners.forEach((listener) => {
|
this.depthListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
@@ -711,7 +713,7 @@ export class GrvtGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitTicker(ticker: AsterTicker): void {
|
private emitTicker(ticker: Ticker): void {
|
||||||
const cloned = cloneTicker(ticker);
|
const cloned = cloneTicker(ticker);
|
||||||
this.tickerListeners.forEach((listener) => {
|
this.tickerListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
@@ -722,7 +724,7 @@ export class GrvtGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitKlines(klines: AsterKline[]): void {
|
private emitKlines(klines: Kline[]): void {
|
||||||
const cloned = cloneKlines(klines);
|
const cloned = cloneKlines(klines);
|
||||||
this.klineListeners.forEach((listener) => {
|
this.klineListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
@@ -974,15 +976,15 @@ function normalizeHex(value: string): string {
|
|||||||
return `0x${trimmed.toLowerCase()}`;
|
return `0x${trimmed.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneAccount(snapshot: AsterAccountSnapshot): AsterAccountSnapshot {
|
function cloneAccount(snapshot: AccountSnapshot): AccountSnapshot {
|
||||||
return JSON.parse(JSON.stringify(snapshot));
|
return JSON.parse(JSON.stringify(snapshot));
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneOrders(orders: AsterOrder[]): AsterOrder[] {
|
function cloneOrders(orders: Order[]): Order[] {
|
||||||
return orders.map((order) => ({ ...order }));
|
return orders.map((order) => ({ ...order }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneDepth(depth: AsterDepth): AsterDepth {
|
function cloneDepth(depth: Depth): Depth {
|
||||||
return {
|
return {
|
||||||
...depth,
|
...depth,
|
||||||
bids: depth.bids.map((level) => [...level] as [string, string]),
|
bids: depth.bids.map((level) => [...level] as [string, string]),
|
||||||
@@ -990,11 +992,11 @@ function cloneDepth(depth: AsterDepth): AsterDepth {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneTicker(ticker: AsterTicker): AsterTicker {
|
function cloneTicker(ticker: Ticker): Ticker {
|
||||||
return { ...ticker };
|
return { ...ticker };
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneKlines(klines: AsterKline[]): AsterKline[] {
|
function cloneKlines(klines: Kline[]): Kline[] {
|
||||||
return klines.map((kline) => ({ ...kline }));
|
return klines.map((kline) => ({ ...kline }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1023,11 +1025,11 @@ function scaleDecimal(value: string | number | undefined, decimals: number): big
|
|||||||
return scaleDecimal(fixed.toFixed(decimals), decimals);
|
return scaleDecimal(fixed.toFixed(decimals), decimals);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sumUnrealized(positions: AsterAccountPosition[]): number {
|
function sumUnrealized(positions: AccountPosition[]): number {
|
||||||
return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0);
|
return positions.reduce((total, position) => total + Number(position.unrealizedProfit ?? 0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNewestPositionEventTime(positions: AsterAccountPosition[]): number {
|
function getNewestPositionEventTime(positions: AccountPosition[]): number {
|
||||||
if (!positions.length) return Date.now();
|
if (!positions.length) return Date.now();
|
||||||
return positions.reduce((max, p) => Math.max(max, Number(p.updateTime) || 0), 0);
|
return positions.reduce((max, p) => Math.max(max, Number(p.updateTime) || 0), 0);
|
||||||
}
|
}
|
||||||
@@ -1036,7 +1038,7 @@ function mapAccountSnapshot(
|
|||||||
response: IApiSubAccountSummaryResponse,
|
response: IApiSubAccountSummaryResponse,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
instrument: string
|
instrument: string
|
||||||
): AsterAccountSnapshot {
|
): AccountSnapshot {
|
||||||
const result = response.result;
|
const result = response.result;
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return emptyAccount(symbol);
|
return emptyAccount(symbol);
|
||||||
@@ -1068,7 +1070,7 @@ function mapPositions(
|
|||||||
response: IApiPositionsResponse,
|
response: IApiPositionsResponse,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
instrument: string
|
instrument: string
|
||||||
): AsterAccountPosition[] {
|
): AccountPosition[] {
|
||||||
return (response.result ?? [])
|
return (response.result ?? [])
|
||||||
.filter((entry) => !entry.instrument || entry.instrument === instrument)
|
.filter((entry) => !entry.instrument || entry.instrument === instrument)
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
@@ -1084,11 +1086,11 @@ function mapPositions(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): AsterOrder[] {
|
function mapOpenOrders(response: IApiOpenOrdersResponse, symbol: string): Order[] {
|
||||||
return (response.result ?? []).map((order) => mapOrder(order, symbol));
|
return (response.result ?? []).map((order) => mapOrder(order, symbol));
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapOrder(order: IOrder, symbol: string): AsterOrder {
|
function mapOrder(order: IOrder, symbol: string): Order {
|
||||||
const leg = order.legs?.[0];
|
const leg = order.legs?.[0];
|
||||||
const state = order.state;
|
const state = order.state;
|
||||||
const metadata = order.metadata;
|
const metadata = order.metadata;
|
||||||
@@ -1126,7 +1128,7 @@ function mapOrder(order: IOrder, symbol: string): AsterOrder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterDepth | null {
|
function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): Depth | null {
|
||||||
const result = response.result;
|
const result = response.result;
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
const toLevel = (entries: Array<{ price?: string; size?: string }> | undefined) =>
|
const toLevel = (entries: Array<{ price?: string; size?: string }> | undefined) =>
|
||||||
@@ -1140,7 +1142,7 @@ function mapDepth(response: IApiOrderbookLevelsResponse, symbol: string): AsterD
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker | null {
|
function mapTicker(response: IApiTickerResponse, symbol: string): Ticker | null {
|
||||||
const result = response.result;
|
const result = response.result;
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
const buyVolume = Number(result.buy_volume_24h_b ?? 0);
|
const buyVolume = Number(result.buy_volume_24h_b ?? 0);
|
||||||
@@ -1161,7 +1163,7 @@ function mapTicker(response: IApiTickerResponse, symbol: string): AsterTicker |
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapKlines(response: IApiCandlestickResponse, _symbol: string): AsterKline[] {
|
function mapKlines(response: IApiCandlestickResponse, _symbol: string): Kline[] {
|
||||||
return (response.result ?? []).reverse().map((entry) => ({
|
return (response.result ?? []).reverse().map((entry) => ({
|
||||||
openTime: nsToMs(entry.open_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
|
openTime: nsToMs(entry.open_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
|
||||||
closeTime: nsToMs(entry.close_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
|
closeTime: nsToMs(entry.close_time ?? Date.now() * ONE_SECOND_IN_NANOSECONDS),
|
||||||
@@ -1174,7 +1176,7 @@ function mapKlines(response: IApiCandlestickResponse, _symbol: string): AsterKli
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): AsterOrder {
|
function mapCreateOrderResponse(response: IApiCreateOrderResponse, symbol: string): Order {
|
||||||
const order = response.result ?? (response as unknown as { order?: IOrder }).order;
|
const order = response.result ?? (response as unknown as { order?: IOrder }).order;
|
||||||
if (!order) {
|
if (!order) {
|
||||||
return {
|
return {
|
||||||
@@ -1551,7 +1553,7 @@ function padPrivateKey(value: string): string {
|
|||||||
return hex;
|
return hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emptyAccount(symbol: string): AsterAccountSnapshot {
|
function emptyAccount(symbol: string): AccountSnapshot {
|
||||||
return {
|
return {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
@@ -1568,5 +1570,5 @@ function emptyAccount(symbol: string): AsterAccountSnapshot {
|
|||||||
updateTime: Date.now(),
|
updateTime: Date.now(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as AsterAccountSnapshot;
|
} as AccountSnapshot;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-89
@@ -1,92 +1,21 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "GRVT",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTX",
|
||||||
}
|
supportsTrailingStop: false,
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
supportsTriggerType: true,
|
||||||
params.timeInForce = intent.timeInForce;
|
defaultStopTriggerType: "STOP_LOSS",
|
||||||
}
|
stopDefaultReduceOnly: true,
|
||||||
if (intent.reduceOnly !== undefined) {
|
stopDefaultClosePosition: true,
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
closeDefaultClosePosition: true,
|
||||||
}
|
});
|
||||||
if (intent.closePosition !== undefined) {
|
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTX",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
triggerType: intent.triggerType ?? "STOP_LOSS",
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
throw new Error("GRVT exchange does not support trailing stop orders");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
export interface GrvtOrderLeg {
|
||||||
|
instrument: string;
|
||||||
|
size: string;
|
||||||
|
limit_price?: string;
|
||||||
|
is_buying_asset?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GrvtTimeInForce =
|
||||||
|
| "GOOD_TILL_TIME"
|
||||||
|
| "ALL_OR_NONE"
|
||||||
|
| "IMMEDIATE_OR_CANCEL"
|
||||||
|
| "FILL_OR_KILL";
|
||||||
|
|
||||||
|
export interface GrvtOrderMetadata {
|
||||||
|
client_order_id?: string;
|
||||||
|
create_time?: string;
|
||||||
|
broker?: string | null;
|
||||||
|
trigger?: GrvtTriggerMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtOrderState {
|
||||||
|
status?: string;
|
||||||
|
reject_reason?: string | null;
|
||||||
|
book_size?: string[];
|
||||||
|
traded_size?: string[];
|
||||||
|
update_time?: string;
|
||||||
|
avg_fill_price?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtOrder {
|
||||||
|
order_id: string;
|
||||||
|
client_order_id?: string;
|
||||||
|
sub_account_id?: string;
|
||||||
|
is_market?: boolean;
|
||||||
|
time_in_force?: GrvtTimeInForce;
|
||||||
|
post_only?: boolean;
|
||||||
|
reduce_only?: boolean;
|
||||||
|
legs?: GrvtOrderLeg[];
|
||||||
|
metadata?: GrvtOrderMetadata;
|
||||||
|
state?: GrvtOrderState;
|
||||||
|
instrument?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtTrade {
|
||||||
|
price: string;
|
||||||
|
size: string;
|
||||||
|
taker_side: "BUY" | "SELL";
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtTradeHistoryResponse {
|
||||||
|
result?: GrvtTrade[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtWebsocketMessage<T> {
|
||||||
|
stream: string;
|
||||||
|
selector: string;
|
||||||
|
sequence_number?: string;
|
||||||
|
feed: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtOrderUpdateFeed {
|
||||||
|
order_id: string;
|
||||||
|
client_order_id?: string;
|
||||||
|
sub_account_id?: string;
|
||||||
|
state?: GrvtOrderState;
|
||||||
|
traded_size?: string[];
|
||||||
|
update_time?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtPositionUpdateFeed {
|
||||||
|
instrument: string;
|
||||||
|
size: string;
|
||||||
|
entry_price?: string;
|
||||||
|
mark_price?: string;
|
||||||
|
unrealized_pnl?: string;
|
||||||
|
sub_account_id?: string;
|
||||||
|
update_time?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtDepthUpdateFeed {
|
||||||
|
instrument: string;
|
||||||
|
bids: GrvtDepthLevel[];
|
||||||
|
asks: GrvtDepthLevel[];
|
||||||
|
event_time?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtTickerUpdateFeed {
|
||||||
|
instrument: string;
|
||||||
|
mark_price?: string;
|
||||||
|
last_trade_price?: string;
|
||||||
|
best_bid_price?: string;
|
||||||
|
best_ask_price?: string;
|
||||||
|
volume_24h?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtOpenOrdersResponse {
|
||||||
|
result?: GrvtOrder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtPositionsResponse {
|
||||||
|
result?: GrvtPosition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtPosition {
|
||||||
|
instrument: string;
|
||||||
|
size: string;
|
||||||
|
entry_price?: string;
|
||||||
|
mark_price?: string;
|
||||||
|
unrealized_pnl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtAccountSnapshot {
|
||||||
|
total_unrealized_pnl?: string;
|
||||||
|
positions: GrvtPosition[];
|
||||||
|
settle_currency?: string;
|
||||||
|
available_balance?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtBalancesResponse {
|
||||||
|
result?: {
|
||||||
|
total_unrealized_pnl?: string;
|
||||||
|
positions?: GrvtPosition[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtDepthLevel {
|
||||||
|
price: string;
|
||||||
|
size: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtDepth {
|
||||||
|
instrument: string;
|
||||||
|
event_time?: string;
|
||||||
|
bids: GrvtDepthLevel[];
|
||||||
|
asks: GrvtDepthLevel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtTicker {
|
||||||
|
instrument: string;
|
||||||
|
mark_price?: string;
|
||||||
|
last_trade_price?: string;
|
||||||
|
best_bid_price?: string;
|
||||||
|
best_ask_price?: string;
|
||||||
|
volume_24h?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtKline {
|
||||||
|
open_time: number;
|
||||||
|
close_time: number;
|
||||||
|
open: string;
|
||||||
|
high: string;
|
||||||
|
low: string;
|
||||||
|
close: string;
|
||||||
|
volume: string;
|
||||||
|
number_of_trades?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtSignature {
|
||||||
|
signer: string;
|
||||||
|
r: string;
|
||||||
|
s: string;
|
||||||
|
v: number;
|
||||||
|
expiration: string;
|
||||||
|
nonce: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtUnsignedOrderLeg {
|
||||||
|
instrument: string;
|
||||||
|
size: string;
|
||||||
|
limit_price?: string;
|
||||||
|
is_buying_asset: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtTriggerMetadata {
|
||||||
|
trigger_type: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||||
|
tpsl: {
|
||||||
|
trigger_by: "UNSPECIFIED" | "INDEX" | "LAST" | "MID" | "MARK";
|
||||||
|
trigger_price: string;
|
||||||
|
close_position: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtOrderMetadataInput {
|
||||||
|
client_order_id: string;
|
||||||
|
trigger?: GrvtTriggerMetadata;
|
||||||
|
broker?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtUnsignedOrder {
|
||||||
|
sub_account_id: string;
|
||||||
|
is_market: boolean;
|
||||||
|
time_in_force: GrvtTimeInForce;
|
||||||
|
post_only: boolean;
|
||||||
|
reduce_only: boolean;
|
||||||
|
legs: GrvtUnsignedOrderLeg[];
|
||||||
|
metadata: GrvtOrderMetadataInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrvtSignedOrder extends GrvtUnsignedOrder {
|
||||||
|
signature: GrvtSignature;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
OrderListener,
|
OrderListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { Order, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
import { extractMessage } from "../../utils/errors";
|
||||||
import { LighterGateway, type LighterGatewayOptions } from "./gateway";
|
import { LighterGateway, type LighterGatewayOptions } from "./gateway";
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export class LighterExchangeAdapter implements ExchangeAdapter {
|
|||||||
this.gateway.watchKlines(interval, handler);
|
this.gateway.watchKlines(interval, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized("createOrder");
|
await this.ensureInitialized("createOrder");
|
||||||
return this.gateway.createOrder(params);
|
return this.gateway.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import type {
|
|||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountAsset,
|
AccountAsset,
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { OrderSide, OrderType } from "../types";
|
import type { OrderSide, OrderType } from "../types";
|
||||||
@@ -189,12 +189,12 @@ export class LighterGateway {
|
|||||||
private accountPollInFlight = false;
|
private accountPollInFlight = false;
|
||||||
private ordersResyncTimer: ReturnType<typeof setInterval> | null = null;
|
private ordersResyncTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
private ordersResyncInFlight = false;
|
private ordersResyncInFlight = false;
|
||||||
private readonly klineCache = new Map<string, AsterKline[]>();
|
private readonly klineCache = new Map<string, Kline[]>();
|
||||||
private readonly accountEvent = createEvent<AsterAccountSnapshot>();
|
private readonly accountEvent = createEvent<AccountSnapshot>();
|
||||||
private readonly ordersEvent = createEvent<AsterOrder[]>();
|
private readonly ordersEvent = createEvent<Order[]>();
|
||||||
private readonly depthEvent = createEvent<AsterDepth>();
|
private readonly depthEvent = createEvent<Depth>();
|
||||||
private readonly tickerEvent = createEvent<AsterTicker>();
|
private readonly tickerEvent = createEvent<Ticker>();
|
||||||
private readonly klinesEvent = createEvent<AsterKline[]>();
|
private readonly klinesEvent = createEvent<Kline[]>();
|
||||||
private readonly auth = { token: null as string | null, expiresAt: 0 };
|
private readonly auth = { token: null as string | null, expiresAt: 0 };
|
||||||
private readonly l1Address: string | null;
|
private readonly l1Address: string | null;
|
||||||
private loggedCreateOrderPayload = false;
|
private loggedCreateOrderPayload = false;
|
||||||
@@ -359,8 +359,8 @@ export class LighterGateway {
|
|||||||
this.klinesEvent.add(handler);
|
this.klinesEvent.add(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
const run = async (): Promise<AsterOrder> => {
|
const run = async (): Promise<Order> => {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
const conversion = this.mapCreateOrderParams(params);
|
const conversion = this.mapCreateOrderParams(params);
|
||||||
const { baseAmountScaledString, priceScaledString, triggerPriceScaledString, ...signParams } = conversion;
|
const { baseAmountScaledString, priceScaledString, triggerPriceScaledString, ...signParams } = conversion;
|
||||||
@@ -1710,7 +1710,7 @@ export class LighterGateway {
|
|||||||
const bestAsk = getBestPrice(this.orderBook.asks, "ask");
|
const bestAsk = getBestPrice(this.orderBook.asks, "ask");
|
||||||
if (bestBid == null && bestAsk == null) return;
|
if (bestBid == null && bestAsk == null) return;
|
||||||
const last = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : (bestBid ?? bestAsk ?? 0);
|
const last = bestBid != null && bestAsk != null ? (bestBid + bestAsk) / 2 : (bestBid ?? bestAsk ?? 0);
|
||||||
const ticker: AsterTicker = {
|
const ticker: Ticker = {
|
||||||
symbol: this.displaySymbol,
|
symbol: this.displaySymbol,
|
||||||
eventType: "lighterSyntheticTicker",
|
eventType: "lighterSyntheticTicker",
|
||||||
eventTime: Date.now(),
|
eventTime: Date.now(),
|
||||||
@@ -1736,10 +1736,10 @@ export class LighterGateway {
|
|||||||
this.tickerEvent.emit(ticker);
|
this.tickerEvent.emit(ticker);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildAccountAssets(): AsterAccountAsset[] {
|
private buildAccountAssets(): AccountAsset[] {
|
||||||
if (!this.assets.size) return [];
|
if (!this.assets.size) return [];
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const list: AsterAccountAsset[] = [];
|
const list: AccountAsset[] = [];
|
||||||
for (const asset of this.assets.values()) {
|
for (const asset of this.assets.values()) {
|
||||||
const balanceNum = parseNumber(asset.balance);
|
const balanceNum = parseNumber(asset.balance);
|
||||||
const lockedNum = parseNumber(asset.locked_balance ?? 0);
|
const lockedNum = parseNumber(asset.locked_balance ?? 0);
|
||||||
@@ -2039,7 +2039,7 @@ function mergeLevels(existing: LighterOrderBookLevel[], updates: LighterOrderBoo
|
|||||||
return Array.from(map.entries()).map(([price, size]) => ({ price, size } as LighterOrderBookLevel));
|
return Array.from(map.entries()).map(([price, size]) => ({ price, size } as LighterOrderBookLevel));
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneKlines(klines: AsterKline[]): AsterKline[] {
|
function cloneKlines(klines: Kline[]): Kline[] {
|
||||||
return klines.map((kline) => ({ ...kline }));
|
return klines.map((kline) => ({ ...kline }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type {
|
import type {
|
||||||
AsterAccountAsset,
|
AccountAsset,
|
||||||
AsterAccountPosition,
|
AccountPosition,
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterDepthLevel,
|
DepthLevel,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
OrderSide,
|
OrderSide,
|
||||||
OrderType,
|
OrderType,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -23,8 +23,8 @@ import { coerceBooleanFlag, normalizeBooleanFlag } from "./flags";
|
|||||||
import { normalizeOrderIdentity } from "./order-identity";
|
import { normalizeOrderIdentity } from "./order-identity";
|
||||||
import { normalizeOrderStatus } from "./status";
|
import { normalizeOrderStatus } from "./status";
|
||||||
|
|
||||||
export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): AsterDepth {
|
export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Depth {
|
||||||
const toLevels = (levels: LighterOrderBookLevel[]): AsterDepthLevel[] =>
|
const toLevels = (levels: LighterOrderBookLevel[]): DepthLevel[] =>
|
||||||
levels.map((level) => [level.price, level.size]);
|
levels.map((level) => [level.price, level.size]);
|
||||||
return {
|
return {
|
||||||
symbol,
|
symbol,
|
||||||
@@ -36,7 +36,7 @@ export function toDepth(symbol: string, snapshot: LighterOrderBookSnapshot): Ast
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toTicker(symbol: string, stats: LighterMarketStats): AsterTicker {
|
export function toTicker(symbol: string, stats: LighterMarketStats): Ticker {
|
||||||
return {
|
return {
|
||||||
symbol,
|
symbol,
|
||||||
eventType: "lighterTicker",
|
eventType: "lighterTicker",
|
||||||
@@ -50,10 +50,10 @@ export function toTicker(symbol: string, stats: LighterMarketStats): AsterTicker
|
|||||||
priceChange: stats.daily_price_change != null ? String(stats.daily_price_change) : undefined,
|
priceChange: stats.daily_price_change != null ? String(stats.daily_price_change) : undefined,
|
||||||
markPrice: stats.mid_price ?? stats.mark_price ?? stats.index_price,
|
markPrice: stats.mid_price ?? stats.mark_price ?? stats.index_price,
|
||||||
weightedAvgPrice: undefined,
|
weightedAvgPrice: undefined,
|
||||||
} as AsterTicker;
|
} as Ticker;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toKlines(symbol: string, interval: string, klines: LighterKline[]): AsterKline[] {
|
export function toKlines(symbol: string, interval: string, klines: LighterKline[]): Kline[] {
|
||||||
return klines.map((entry) => ({
|
return klines.map((entry) => ({
|
||||||
symbol,
|
symbol,
|
||||||
eventType: "lighterKline",
|
eventType: "lighterKline",
|
||||||
@@ -72,11 +72,11 @@ export function toKlines(symbol: string, interval: string, klines: LighterKline[
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toOrders(symbol: string, orders: LighterOrder[]): AsterOrder[] {
|
export function toOrders(symbol: string, orders: LighterOrder[]): Order[] {
|
||||||
return orders.map((order) => lighterOrderToAster(symbol, order));
|
return orders.map((order) => lighterOrderToAster(symbol, order));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lighterOrderToAster(symbol: string, order: LighterOrder): AsterOrder {
|
export function lighterOrderToAster(symbol: string, order: LighterOrder): Order {
|
||||||
const booleanIsAsk = normalizeBooleanFlag(order.is_ask);
|
const booleanIsAsk = normalizeBooleanFlag(order.is_ask);
|
||||||
const normalizedSide = order.side?.toLowerCase();
|
const normalizedSide = order.side?.toLowerCase();
|
||||||
const side: OrderSide =
|
const side: OrderSide =
|
||||||
@@ -162,7 +162,7 @@ export function toAccountSnapshot(
|
|||||||
symbol: string,
|
symbol: string,
|
||||||
details: LighterAccountDetails,
|
details: LighterAccountDetails,
|
||||||
positions: LighterPosition[] = [],
|
positions: LighterPosition[] = [],
|
||||||
assets: AsterAccountAsset[] = [],
|
assets: AccountAsset[] = [],
|
||||||
options?: {
|
options?: {
|
||||||
marketSymbol?: string | null;
|
marketSymbol?: string | null;
|
||||||
marketId?: number | null;
|
marketId?: number | null;
|
||||||
@@ -172,7 +172,7 @@ export function toAccountSnapshot(
|
|||||||
baseAssetId?: number | null;
|
baseAssetId?: number | null;
|
||||||
quoteAssetId?: number | null;
|
quoteAssetId?: number | null;
|
||||||
}
|
}
|
||||||
): AsterAccountSnapshot {
|
): AccountSnapshot {
|
||||||
const targetSymbol = options?.marketSymbol ?? null;
|
const targetSymbol = options?.marketSymbol ?? null;
|
||||||
const targetMarketId =
|
const targetMarketId =
|
||||||
options?.marketId != null && Number.isFinite(Number(options.marketId))
|
options?.marketId != null && Number.isFinite(Number(options.marketId))
|
||||||
@@ -228,7 +228,7 @@ export function toAccountSnapshot(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultAsset(details: LighterAccountDetails, quoteAsset: string): AsterAccountAsset[] {
|
function defaultAsset(details: LighterAccountDetails, quoteAsset: string): AccountAsset[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
asset: quoteAsset || "USDC",
|
asset: quoteAsset || "USDC",
|
||||||
@@ -239,7 +239,7 @@ function defaultAsset(details: LighterAccountDetails, quoteAsset: string): Aster
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeTotalWalletBalance(assets: AsterAccountAsset[], details: LighterAccountDetails): string {
|
function computeTotalWalletBalance(assets: AccountAsset[], details: LighterAccountDetails): string {
|
||||||
const sum = assets.reduce((acc, asset) => acc + Number(asset.walletBalance ?? 0), 0);
|
const sum = assets.reduce((acc, asset) => acc + Number(asset.walletBalance ?? 0), 0);
|
||||||
if (Number.isFinite(sum) && sum > 0) {
|
if (Number.isFinite(sum) && sum > 0) {
|
||||||
return sum.toString();
|
return sum.toString();
|
||||||
@@ -247,7 +247,7 @@ function computeTotalWalletBalance(assets: AsterAccountAsset[], details: Lighter
|
|||||||
return details.total_asset_value ?? details.collateral ?? "0";
|
return details.total_asset_value ?? details.collateral ?? "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
function findAsset(assets: AsterAccountAsset[], target: string | undefined | null): AsterAccountAsset | undefined {
|
function findAsset(assets: AccountAsset[], target: string | undefined | null): AccountAsset | undefined {
|
||||||
if (!target) return undefined;
|
if (!target) return undefined;
|
||||||
const normalizedTarget = target.toUpperCase().split(/[-:/]/)[0];
|
const normalizedTarget = target.toUpperCase().split(/[-:/]/)[0];
|
||||||
return assets.find((asset) => asset.asset.toUpperCase().split(/[-:/]/)[0] === normalizedTarget);
|
return assets.find((asset) => asset.asset.toUpperCase().split(/[-:/]/)[0] === normalizedTarget);
|
||||||
@@ -261,7 +261,7 @@ function normalizeMarketType(value: string | null | undefined): "perp" | "spot"
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function lighterPositionToAster(symbol: string, position: LighterPosition): AsterAccountPosition {
|
function lighterPositionToAster(symbol: string, position: LighterPosition): AccountPosition {
|
||||||
const sign = position.sign ?? 0;
|
const sign = position.sign ?? 0;
|
||||||
const positionSide = sign > 0 ? "LONG" : sign < 0 ? "SHORT" : "BOTH";
|
const positionSide = sign > 0 ? "LONG" : sign < 0 ? "SHORT" : "BOTH";
|
||||||
const magnitude = Number(position.position ?? 0);
|
const magnitude = Number(position.position ?? 0);
|
||||||
|
|||||||
@@ -1,93 +1,22 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "Lighter",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTC",
|
||||||
}
|
defaultMarketTimeInForce: "IOC",
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
defaultCloseTimeInForce: "IOC",
|
||||||
params.timeInForce = intent.timeInForce;
|
supportsTrailingStop: false,
|
||||||
}
|
supportsTriggerType: false,
|
||||||
if (intent.reduceOnly !== undefined) {
|
stopDefaultReduceOnly: true,
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
stopDefaultClosePosition: true,
|
||||||
}
|
closeDefaultClosePosition: true,
|
||||||
if (intent.closePosition !== undefined) {
|
});
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
timeInForce: intent.timeInForce ?? "IOC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
throw new Error("Lighter exchange does not support trailing stop orders");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
timeInForce: intent.timeInForce ?? "IOC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { setTimeout, clearTimeout } from "timers";
|
|
||||||
import type {
|
import type {
|
||||||
AccountListener,
|
AccountListener,
|
||||||
DepthListener,
|
DepthListener,
|
||||||
@@ -9,9 +8,10 @@ import type {
|
|||||||
OrderListener,
|
OrderListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { Order, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
import { extractMessage } from "../../utils/errors";
|
||||||
import { NadoGateway, type NadoGatewayOptions } from "./gateway";
|
import { NadoGateway, type NadoGatewayOptions } from "./gateway";
|
||||||
|
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||||
import type { ChainEnv } from "@nadohq/shared";
|
import type { ChainEnv } from "@nadohq/shared";
|
||||||
import type { Address } from "viem";
|
import type { Address } from "viem";
|
||||||
|
|
||||||
@@ -35,11 +35,8 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
|||||||
|
|
||||||
private readonly gateway: NadoGateway;
|
private readonly gateway: NadoGateway;
|
||||||
private readonly symbol: string;
|
private readonly symbol: string;
|
||||||
private initPromise: Promise<void> | null = null;
|
private readonly safeInvoke = createSafeInvoke("NadoExchangeAdapter");
|
||||||
private readonly initContexts = new Set<string>();
|
private readonly init: ReturnType<typeof createInitManager>;
|
||||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private retryDelayMs = 3000;
|
|
||||||
private lastInitErrorAt = 0;
|
|
||||||
|
|
||||||
constructor(credentials: NadoCredentials = {}) {
|
constructor(credentials: NadoCredentials = {}) {
|
||||||
const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY;
|
const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY;
|
||||||
@@ -79,6 +76,9 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
|||||||
(process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined),
|
(process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined),
|
||||||
logger: (context, error) => this.logError(context, error),
|
logger: (context, error) => this.logError(context, error),
|
||||||
});
|
});
|
||||||
|
this.init = createInitManager("NadoExchangeAdapter", () =>
|
||||||
|
this.gateway.ensureInitialized(this.symbol),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
@@ -86,52 +86,52 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(cb: AccountListener): void {
|
watchAccount(cb: AccountListener): void {
|
||||||
void this.ensureInitialized("watchAccount");
|
void this.init.ensureInitialized("watchAccount");
|
||||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: OrderListener): void {
|
watchOrders(cb: OrderListener): void {
|
||||||
void this.ensureInitialized("watchOrders");
|
void this.init.ensureInitialized("watchOrders");
|
||||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(symbol: string, cb: DepthListener): void {
|
watchDepth(symbol: string, cb: DepthListener): void {
|
||||||
void this.ensureInitialized(`watchDepth:${symbol}`);
|
void this.init.ensureInitialized(`watchDepth:${symbol}`);
|
||||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(symbol: string, cb: TickerListener): void {
|
watchTicker(symbol: string, cb: TickerListener): void {
|
||||||
void this.ensureInitialized(`watchTicker:${symbol}`);
|
void this.init.ensureInitialized(`watchTicker:${symbol}`);
|
||||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||||
void this.ensureInitialized(`watchFundingRate:${symbol}`);
|
void this.init.ensureInitialized(`watchFundingRate:${symbol}`);
|
||||||
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized("createOrder");
|
await this.init.ensureInitialized("createOrder");
|
||||||
return this.gateway.createOrder(params);
|
return this.gateway.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrder");
|
await this.init.ensureInitialized("cancelOrder");
|
||||||
await this.gateway.cancelOrder(params);
|
await this.gateway.cancelOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrders");
|
await this.init.ensureInitialized("cancelOrders");
|
||||||
await this.gateway.cancelOrders(params);
|
await this.gateway.cancelOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelAllOrders");
|
await this.init.ensureInitialized("cancelAllOrders");
|
||||||
await this.gateway.cancelAllOrders(params);
|
await this.gateway.cancelAllOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,70 +144,6 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
|
||||||
const wrapped = ((...args: any[]) => {
|
|
||||||
try {
|
|
||||||
cb(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[NadoExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureInitialized(context?: string): Promise<void> {
|
|
||||||
if (!this.initPromise) {
|
|
||||||
this.initContexts.clear();
|
|
||||||
this.initPromise = this.gateway
|
|
||||||
.ensureInitialized(this.symbol)
|
|
||||||
.then((value) => {
|
|
||||||
this.clearRetry();
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.handleInitError("initialize", error);
|
|
||||||
this.initPromise = null;
|
|
||||||
this.scheduleRetry();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context && !this.initContexts.has(context)) {
|
|
||||||
this.initContexts.add(context);
|
|
||||||
this.initPromise.catch((error) => {
|
|
||||||
this.handleInitError(context, error);
|
|
||||||
this.scheduleRetry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRetry(): void {
|
|
||||||
if (this.retryTimer) return;
|
|
||||||
this.retryTimer = setTimeout(() => {
|
|
||||||
this.retryTimer = null;
|
|
||||||
if (this.initPromise) return;
|
|
||||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
|
||||||
void this.ensureInitialized("retry");
|
|
||||||
}, this.retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRetry(): void {
|
|
||||||
if (this.retryTimer) {
|
|
||||||
clearTimeout(this.retryTimer);
|
|
||||||
this.retryTimer = null;
|
|
||||||
}
|
|
||||||
this.retryDelayMs = 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitError(context: string, error: unknown): void {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastInitErrorAt < 5000) return;
|
|
||||||
this.lastInitErrorAt = now;
|
|
||||||
console.error(`[NadoExchangeAdapter] ${context} failed`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private logError(context: string, error: unknown): void {
|
private logError(context: string, error: unknown): void {
|
||||||
const detail = extractMessage(error);
|
const detail = extractMessage(error);
|
||||||
const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`;
|
const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`;
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ import type {
|
|||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountAsset,
|
AccountAsset,
|
||||||
AsterAccountPosition,
|
AccountPosition,
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
TimeInForce,
|
TimeInForce,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -331,7 +331,7 @@ export class NadoGateway {
|
|||||||
private readonly openOrdersByDigest = new Map<string, LocalOrder>();
|
private readonly openOrdersByDigest = new Map<string, LocalOrder>();
|
||||||
private readonly triggerOrdersByDigest = new Map<string, TriggerOrder>();
|
private readonly triggerOrdersByDigest = new Map<string, TriggerOrder>();
|
||||||
|
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private lastAccountSyncAt = 0;
|
private lastAccountSyncAt = 0;
|
||||||
|
|
||||||
private gatewayWs: NodeWebSocket | null = null;
|
private gatewayWs: NodeWebSocket | null = null;
|
||||||
@@ -356,7 +356,7 @@ export class NadoGateway {
|
|||||||
private accountPollTimer: ReturnType<typeof setInterval> | null = null;
|
private accountPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
private ordersPollTimer: ReturnType<typeof setInterval> | null = null;
|
private ordersPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
private triggerOrdersPollTimer: ReturnType<typeof setInterval> | null = null;
|
private triggerOrdersPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
private klinesState = new Map<string, { productId: number; periodSec: number; klines: AsterKline[] }>();
|
private klinesState = new Map<string, { productId: number; periodSec: number; klines: Kline[] }>();
|
||||||
|
|
||||||
private subscriptionRequestId = 1;
|
private subscriptionRequestId = 1;
|
||||||
private subscriptionAuthComplete = false;
|
private subscriptionAuthComplete = false;
|
||||||
@@ -567,7 +567,7 @@ export class NadoGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized(params.symbol);
|
await this.ensureInitialized(params.symbol);
|
||||||
const meta = this.getSymbolMetaOrThrow(params.symbol);
|
const meta = this.getSymbolMetaOrThrow(params.symbol);
|
||||||
if (params.type === "STOP_MARKET") {
|
if (params.type === "STOP_MARKET") {
|
||||||
@@ -1139,7 +1139,7 @@ export class NadoGateway {
|
|||||||
return Number.isFinite(asNumber) && asNumber > 0 ? Math.floor(asNumber) : 60;
|
return Number.isFinite(asNumber) && asNumber > 0 ? Math.floor(asNumber) : 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise<AsterKline[]> {
|
private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise<Kline[]> {
|
||||||
try {
|
try {
|
||||||
const result = await this.indexer.getCandlesticks({ productId, period: periodSec, limit });
|
const result = await this.indexer.getCandlesticks({ productId, period: periodSec, limit });
|
||||||
const reversed = Array.from(result).reverse();
|
const reversed = Array.from(result).reverse();
|
||||||
@@ -1262,8 +1262,8 @@ export class NadoGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildAsterOrdersSnapshot(): AsterOrder[] {
|
private buildAsterOrdersSnapshot(): Order[] {
|
||||||
const orders: AsterOrder[] = [];
|
const orders: Order[] = [];
|
||||||
for (const order of this.openOrdersByDigest.values()) {
|
for (const order of this.openOrdersByDigest.values()) {
|
||||||
const meta = this.symbolMetaByProductId.get(order.productId);
|
const meta = this.symbolMetaByProductId.get(order.productId);
|
||||||
if (!meta) continue;
|
if (!meta) continue;
|
||||||
@@ -1322,7 +1322,7 @@ export class NadoGateway {
|
|||||||
return orders;
|
return orders;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDepthSnapshot(productId: number, symbol: string): AsterDepth | null {
|
private buildDepthSnapshot(productId: number, symbol: string): Depth | null {
|
||||||
const bbo = this.bestBidOfferByProductId.get(productId);
|
const bbo = this.bestBidOfferByProductId.get(productId);
|
||||||
if (!bbo) return null;
|
if (!bbo) return null;
|
||||||
const bids: [string, string][] = [[fromX18(bbo.bidX18).toFixed(), fromX18(bbo.bidQtyX18).toFixed()]];
|
const bids: [string, string][] = [[fromX18(bbo.bidX18).toFixed(), fromX18(bbo.bidQtyX18).toFixed()]];
|
||||||
@@ -1336,7 +1336,7 @@ export class NadoGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildTickerSnapshot(productId: number, symbol: string): AsterTicker | null {
|
private buildTickerSnapshot(productId: number, symbol: string): Ticker | null {
|
||||||
const bbo = this.bestBidOfferByProductId.get(productId);
|
const bbo = this.bestBidOfferByProductId.get(productId);
|
||||||
if (!bbo) return null;
|
if (!bbo) return null;
|
||||||
const trade = this.lastTradePriceByProductId.get(productId);
|
const trade = this.lastTradePriceByProductId.get(productId);
|
||||||
@@ -1360,10 +1360,10 @@ export class NadoGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapSubaccountInfoToAsterSnapshot(data: NonNullable<NadoSubaccountInfoResponse["data"]>): AsterAccountSnapshot {
|
private mapSubaccountInfoToAsterSnapshot(data: NonNullable<NadoSubaccountInfoResponse["data"]>): AccountSnapshot {
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
const assets: AsterAccountAsset[] = [];
|
const assets: AccountAsset[] = [];
|
||||||
const positions: AsterAccountPosition[] = [];
|
const positions: AccountPosition[] = [];
|
||||||
|
|
||||||
const spotBalanceByProductId = new Map<number, string>();
|
const spotBalanceByProductId = new Map<number, string>();
|
||||||
for (const entry of data.spot_balances ?? []) {
|
for (const entry of data.spot_balances ?? []) {
|
||||||
@@ -1771,7 +1771,7 @@ export class NadoGateway {
|
|||||||
if (state.periodSec !== event.granularity) continue;
|
if (state.periodSec !== event.granularity) continue;
|
||||||
const openTime = event.timestamp * 1000;
|
const openTime = event.timestamp * 1000;
|
||||||
const closeTime = openTime + state.periodSec * 1000 - 1;
|
const closeTime = openTime + state.periodSec * 1000 - 1;
|
||||||
const next: AsterKline = {
|
const next: Kline = {
|
||||||
openTime,
|
openTime,
|
||||||
closeTime,
|
closeTime,
|
||||||
open: fromX18(event.open_x18).toFixed(),
|
open: fromX18(event.open_x18).toFixed(),
|
||||||
@@ -1795,7 +1795,7 @@ export class NadoGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
|
private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
|
||||||
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
|
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
|
||||||
const side = params.side;
|
const side = params.side;
|
||||||
const qty = params.quantity ?? 0;
|
const qty = params.quantity ?? 0;
|
||||||
@@ -1905,7 +1905,7 @@ export class NadoGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
|
private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
|
||||||
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
|
if (!this.chainId) throw new Error("Nado not initialized (chainId missing)");
|
||||||
const side = params.side;
|
const side = params.side;
|
||||||
const qty = params.quantity ?? 0;
|
const qty = params.quantity ?? 0;
|
||||||
@@ -2017,7 +2017,7 @@ export class NadoGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<AsterOrder> {
|
private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise<Order> {
|
||||||
if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)");
|
if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)");
|
||||||
const side = params.side;
|
const side = params.side;
|
||||||
const qty = params.quantity ?? 0;
|
const qty = params.quantity ?? 0;
|
||||||
|
|||||||
+19
-90
@@ -1,93 +1,22 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "Nado",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTC",
|
||||||
}
|
defaultMarketTimeInForce: "IOC",
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
defaultCloseTimeInForce: "IOC",
|
||||||
params.timeInForce = intent.timeInForce;
|
supportsTrailingStop: false,
|
||||||
}
|
supportsTriggerType: false,
|
||||||
if (intent.reduceOnly !== undefined) {
|
stopDefaultReduceOnly: true,
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
stopDefaultClosePosition: true,
|
||||||
}
|
closeDefaultClosePosition: true,
|
||||||
if (intent.closePosition !== undefined) {
|
});
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
timeInForce: intent.timeInForce ?? "IOC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
throw new Error("Nado exchange does not support trailing stop orders");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
timeInForce: intent.timeInForce ?? "IOC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import type { Order, CreateOrderParams, TimeInForce } from "./types";
|
||||||
|
import type {
|
||||||
|
BaseOrderIntent,
|
||||||
|
ClosePositionIntent,
|
||||||
|
LimitOrderIntent,
|
||||||
|
MarketOrderIntent,
|
||||||
|
StopOrderIntent,
|
||||||
|
TrailingStopOrderIntent,
|
||||||
|
} from "./order-schema";
|
||||||
|
import { toStringBoolean } from "./order-schema";
|
||||||
|
|
||||||
|
export interface OrderHandlerConfig {
|
||||||
|
exchangeName: string;
|
||||||
|
defaultLimitTimeInForce: TimeInForce | "GTX";
|
||||||
|
defaultMarketTimeInForce?: TimeInForce;
|
||||||
|
defaultCloseTimeInForce?: TimeInForce;
|
||||||
|
defaultStopTimeInForce?: TimeInForce;
|
||||||
|
defaultStopTriggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||||
|
supportsTrailingStop: boolean;
|
||||||
|
supportsTriggerType: boolean;
|
||||||
|
stopDefaultReduceOnly?: boolean;
|
||||||
|
stopDefaultClosePosition?: boolean;
|
||||||
|
closeDefaultClosePosition?: boolean;
|
||||||
|
stopPriceAsPrice?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderHandlers {
|
||||||
|
createLimitOrder(intent: LimitOrderIntent): Promise<Order>;
|
||||||
|
createMarketOrder(intent: MarketOrderIntent): Promise<Order>;
|
||||||
|
createStopOrder(intent: StopOrderIntent): Promise<Order>;
|
||||||
|
createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order>;
|
||||||
|
createClosePositionOrder(intent: ClosePositionIntent): Promise<Order>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
||||||
|
if (params.quantity === undefined) {
|
||||||
|
params.quantity = intent.quantity;
|
||||||
|
}
|
||||||
|
if (params.timeInForce === undefined && intent.timeInForce) {
|
||||||
|
params.timeInForce = intent.timeInForce;
|
||||||
|
}
|
||||||
|
if (intent.reduceOnly !== undefined) {
|
||||||
|
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
||||||
|
}
|
||||||
|
if (intent.closePosition !== undefined) {
|
||||||
|
params.closePosition = toStringBoolean(intent.closePosition);
|
||||||
|
}
|
||||||
|
if (intent.clientOrderId !== undefined) {
|
||||||
|
params.clientOrderId = intent.clientOrderId;
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOrderHandlers(config: OrderHandlerConfig): OrderHandlers {
|
||||||
|
return {
|
||||||
|
async createLimitOrder(intent: LimitOrderIntent): Promise<Order> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "LIMIT",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
price: intent.price,
|
||||||
|
timeInForce: intent.timeInForce ?? config.defaultLimitTimeInForce,
|
||||||
|
slPrice: intent.slPrice,
|
||||||
|
tpPrice: intent.tpPrice,
|
||||||
|
},
|
||||||
|
intent,
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createMarketOrder(intent: MarketOrderIntent): Promise<Order> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
timeInForce: intent.timeInForce ?? config.defaultMarketTimeInForce,
|
||||||
|
},
|
||||||
|
intent,
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createStopOrder(intent: StopOrderIntent): Promise<Order> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
stopPrice: intent.stopPrice,
|
||||||
|
timeInForce: intent.timeInForce ?? (config.defaultStopTimeInForce ?? "GTC"),
|
||||||
|
triggerType: config.supportsTriggerType ? (intent.triggerType ?? config.defaultStopTriggerType) : undefined,
|
||||||
|
price: config.stopPriceAsPrice ? intent.stopPrice : undefined,
|
||||||
|
},
|
||||||
|
intent,
|
||||||
|
);
|
||||||
|
if (config.stopDefaultReduceOnly && intent.reduceOnly === undefined) {
|
||||||
|
params.reduceOnly = "true";
|
||||||
|
}
|
||||||
|
if (config.stopDefaultClosePosition && intent.closePosition === undefined) {
|
||||||
|
params.closePosition = "true";
|
||||||
|
}
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order> {
|
||||||
|
if (!config.supportsTrailingStop) {
|
||||||
|
throw new Error(`${config.exchangeName} does not support trailing stop orders`);
|
||||||
|
}
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "TRAILING_STOP_MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
activationPrice: intent.activationPrice,
|
||||||
|
callbackRate: intent.callbackRate,
|
||||||
|
timeInForce: intent.timeInForce ?? "GTC",
|
||||||
|
},
|
||||||
|
intent,
|
||||||
|
);
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createClosePositionOrder(intent: ClosePositionIntent): Promise<Order> {
|
||||||
|
const params: CreateOrderParams = applyCommonFields(
|
||||||
|
{
|
||||||
|
symbol: intent.symbol,
|
||||||
|
side: intent.side,
|
||||||
|
type: "MARKET",
|
||||||
|
quantity: intent.quantity,
|
||||||
|
reduceOnly: "true",
|
||||||
|
timeInForce: intent.timeInForce ?? config.defaultCloseTimeInForce,
|
||||||
|
},
|
||||||
|
intent,
|
||||||
|
);
|
||||||
|
if (config.closeDefaultClosePosition && intent.closePosition === undefined) {
|
||||||
|
params.closePosition = "true";
|
||||||
|
}
|
||||||
|
return intent.adapter.createOrder(params);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExchangeAdapter } from "./adapter";
|
import type { ExchangeAdapter } from "./adapter";
|
||||||
import type { AsterOrder } from "./types";
|
import type { Order } from "./types";
|
||||||
import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "./create-adapter";
|
import { SUPPORTED_EXCHANGE_IDS, type SupportedExchangeId } from "./create-adapter";
|
||||||
import type {
|
import type {
|
||||||
BaseOrderIntent,
|
BaseOrderIntent,
|
||||||
@@ -21,11 +21,11 @@ import * as binanceOrders from "./binance/order";
|
|||||||
type ExchangeKey = SupportedExchangeId;
|
type ExchangeKey = SupportedExchangeId;
|
||||||
|
|
||||||
interface ExchangeOrderHandlers {
|
interface ExchangeOrderHandlers {
|
||||||
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
|
limit(intent: LimitOrderIntent): Promise<Order>;
|
||||||
market(intent: MarketOrderIntent): Promise<AsterOrder>;
|
market(intent: MarketOrderIntent): Promise<Order>;
|
||||||
stop(intent: StopOrderIntent): Promise<AsterOrder>;
|
stop(intent: StopOrderIntent): Promise<Order>;
|
||||||
trailingStop?: (intent: TrailingStopOrderIntent) => Promise<AsterOrder>;
|
trailingStop?: (intent: TrailingStopOrderIntent) => Promise<Order>;
|
||||||
close(intent: ClosePositionIntent): Promise<AsterOrder>;
|
close(intent: ClosePositionIntent): Promise<Order>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
|
||||||
@@ -117,19 +117,19 @@ function getHandlers(intent: BaseOrderIntent): ExchangeOrderHandlers {
|
|||||||
return handlers;
|
return handlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export function routeLimitOrder(intent: LimitOrderIntent): Promise<Order> {
|
||||||
return getHandlers(intent).limit(intent);
|
return getHandlers(intent).limit(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
export function routeMarketOrder(intent: MarketOrderIntent): Promise<Order> {
|
||||||
return getHandlers(intent).market(intent);
|
return getHandlers(intent).market(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
export function routeStopOrder(intent: StopOrderIntent): Promise<Order> {
|
||||||
return getHandlers(intent).stop(intent);
|
return getHandlers(intent).stop(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<Order> {
|
||||||
const handlers = getHandlers(intent);
|
const handlers = getHandlers(intent);
|
||||||
if (!handlers.trailingStop) {
|
if (!handlers.trailingStop) {
|
||||||
throw new Error("Trailing stop orders are not supported by the current exchange");
|
throw new Error("Trailing stop orders are not supported by the current exchange");
|
||||||
@@ -137,6 +137,6 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise
|
|||||||
return handlers.trailingStop(intent);
|
return handlers.trailingStop(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function routeCloseOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
export function routeCloseOrder(intent: ClosePositionIntent): Promise<Order> {
|
||||||
return getHandlers(intent).close(intent);
|
return getHandlers(intent).close(intent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ export interface BaseOrderIntent {
|
|||||||
reduceOnly?: boolean;
|
reduceOnly?: boolean;
|
||||||
closePosition?: boolean;
|
closePosition?: boolean;
|
||||||
timeInForce?: TimeInForce | "GTX";
|
timeInForce?: TimeInForce | "GTX";
|
||||||
|
clientOrderId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LimitOrderIntent extends BaseOrderIntent {
|
export interface LimitOrderIntent extends BaseOrderIntent {
|
||||||
price: number;
|
price: number;
|
||||||
// StandX TPSL 参数
|
slPrice?: number;
|
||||||
slPrice?: number; // 止损价格
|
tpPrice?: number;
|
||||||
tpPrice?: number; // 止盈价格
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketOrderIntent extends BaseOrderIntent {
|
export interface MarketOrderIntent extends BaseOrderIntent {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { setTimeout, clearTimeout } from "timers";
|
|
||||||
import type {
|
import type {
|
||||||
AccountListener,
|
AccountListener,
|
||||||
DepthListener,
|
DepthListener,
|
||||||
@@ -7,9 +6,10 @@ import type {
|
|||||||
OrderListener,
|
OrderListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { Order, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
import { extractMessage } from "../../utils/errors";
|
||||||
import { ParadexGateway, type ParadexGatewayOptions } from "./gateway";
|
import { ParadexGateway, type ParadexGatewayOptions } from "./gateway";
|
||||||
|
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||||
|
|
||||||
export interface ParadexCredentials {
|
export interface ParadexCredentials {
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
@@ -26,11 +26,8 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
|||||||
|
|
||||||
private readonly gateway: ParadexGateway;
|
private readonly gateway: ParadexGateway;
|
||||||
private readonly symbol: string;
|
private readonly symbol: string;
|
||||||
private initPromise: Promise<void> | null = null;
|
private readonly safeInvoke = createSafeInvoke("ParadexExchangeAdapter");
|
||||||
private readonly initContexts = new Set<string>();
|
private readonly init: ReturnType<typeof createInitManager>;
|
||||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private retryDelayMs = 3000;
|
|
||||||
private lastInitErrorAt = 0;
|
|
||||||
|
|
||||||
constructor(credentials: ParadexCredentials = {}) {
|
constructor(credentials: ParadexCredentials = {}) {
|
||||||
const privateKey = credentials.privateKey ?? process.env.PARADEX_PRIVATE_KEY;
|
const privateKey = credentials.privateKey ?? process.env.PARADEX_PRIVATE_KEY;
|
||||||
@@ -54,6 +51,9 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.symbol = symbol;
|
this.symbol = symbol;
|
||||||
|
this.init = createInitManager("ParadexExchangeAdapter", () =>
|
||||||
|
this.gateway.ensureInitialized(this.symbol),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
@@ -61,114 +61,50 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(cb: AccountListener): void {
|
watchAccount(cb: AccountListener): void {
|
||||||
void this.ensureInitialized("watchAccount");
|
void this.init.ensureInitialized("watchAccount");
|
||||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: OrderListener): void {
|
watchOrders(cb: OrderListener): void {
|
||||||
void this.ensureInitialized("watchOrders");
|
void this.init.ensureInitialized("watchOrders");
|
||||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(symbol: string, cb: DepthListener): void {
|
watchDepth(symbol: string, cb: DepthListener): void {
|
||||||
void this.ensureInitialized(`watchDepth:${symbol}`);
|
void this.init.ensureInitialized(`watchDepth:${symbol}`);
|
||||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(symbol: string, cb: TickerListener): void {
|
watchTicker(symbol: string, cb: TickerListener): void {
|
||||||
void this.ensureInitialized(`watchTicker:${symbol}`);
|
void this.init.ensureInitialized(`watchTicker:${symbol}`);
|
||||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||||
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized("createOrder");
|
await this.init.ensureInitialized("createOrder");
|
||||||
return this.gateway.createOrder(params);
|
return this.gateway.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrder");
|
await this.init.ensureInitialized("cancelOrder");
|
||||||
await this.gateway.cancelOrder(params);
|
await this.gateway.cancelOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrders");
|
await this.init.ensureInitialized("cancelOrders");
|
||||||
await this.gateway.cancelOrders(params);
|
await this.gateway.cancelOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelAllOrders");
|
await this.init.ensureInitialized("cancelAllOrders");
|
||||||
await this.gateway.cancelAllOrders(params);
|
await this.gateway.cancelAllOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
|
||||||
const wrapped = ((...args: any[]) => {
|
|
||||||
try {
|
|
||||||
cb(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[ParadexExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureInitialized(context?: string): Promise<void> {
|
|
||||||
if (!this.initPromise) {
|
|
||||||
this.initContexts.clear();
|
|
||||||
this.initPromise = this.gateway
|
|
||||||
.ensureInitialized(this.symbol)
|
|
||||||
.then((value) => {
|
|
||||||
this.clearRetry();
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.handleInitError("initialize", error);
|
|
||||||
this.initPromise = null;
|
|
||||||
this.scheduleRetry();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (context && !this.initContexts.has(context)) {
|
|
||||||
this.initContexts.add(context);
|
|
||||||
this.initPromise.catch((error) => {
|
|
||||||
this.handleInitError(context, error);
|
|
||||||
this.scheduleRetry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRetry(): void {
|
|
||||||
if (this.retryTimer) return;
|
|
||||||
this.retryTimer = setTimeout(() => {
|
|
||||||
this.retryTimer = null;
|
|
||||||
if (this.initPromise) return;
|
|
||||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
|
||||||
void this.ensureInitialized("retry");
|
|
||||||
}, this.retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRetry(): void {
|
|
||||||
if (this.retryTimer) {
|
|
||||||
clearTimeout(this.retryTimer);
|
|
||||||
this.retryTimer = null;
|
|
||||||
}
|
|
||||||
this.retryDelayMs = 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitError(context: string, error: unknown): void {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastInitErrorAt < 5000) return;
|
|
||||||
this.lastInitErrorAt = now;
|
|
||||||
console.error(`[ParadexExchangeAdapter] ${context} failed`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private logError(context: string, error: unknown): void {
|
private logError(context: string, error: unknown): void {
|
||||||
const detail = extractMessage(error);
|
const detail = extractMessage(error);
|
||||||
if (context === "initialize" && typeof error === "string" && /initialized/i.test(error)) {
|
if (context === "initialize" && typeof error === "string" && /initialized/i.test(error)) {
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import ccxt, {
|
|||||||
} from "ccxt";
|
} from "ccxt";
|
||||||
import { createRequire } from "module";
|
import { createRequire } from "module";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountAsset,
|
AccountAsset,
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
OrderType,
|
OrderType,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
@@ -91,8 +91,8 @@ export class ParadexGateway {
|
|||||||
private depthListeners = new Set<DepthListener>();
|
private depthListeners = new Set<DepthListener>();
|
||||||
private tickerListeners = new Set<TickerListener>();
|
private tickerListeners = new Set<TickerListener>();
|
||||||
private klineListeners = new Map<string, Set<KlineListener>>();
|
private klineListeners = new Map<string, Set<KlineListener>>();
|
||||||
private readonly localOrders = new Map<string, AsterOrder>();
|
private readonly localOrders = new Map<string, Order>();
|
||||||
private lastBalanceSnapshot: AsterAccountSnapshot | null = null;
|
private lastBalanceSnapshot: AccountSnapshot | null = null;
|
||||||
|
|
||||||
private accountPollTimer: NodeJS.Timeout | null = null;
|
private accountPollTimer: NodeJS.Timeout | null = null;
|
||||||
private orderPollTimer: NodeJS.Timeout | null = null;
|
private orderPollTimer: NodeJS.Timeout | null = null;
|
||||||
@@ -546,7 +546,7 @@ export class ParadexGateway {
|
|||||||
this.klinePollTimers.set(interval, setInterval(() => void poll(), this.pollIntervals.klines));
|
this.klinePollTimers.set(interval, setInterval(() => void poll(), this.pollIntervals.klines));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized(params.symbol);
|
await this.ensureInitialized(params.symbol);
|
||||||
const symbol = this.marketSymbol;
|
const symbol = this.marketSymbol;
|
||||||
const type = this.mapOrderTypeToCcxt(params.type);
|
const type = this.mapOrderTypeToCcxt(params.type);
|
||||||
@@ -678,7 +678,7 @@ export class ParadexGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
|
private mapBalanceToAccountSnapshot(balance: Balances): AccountSnapshot {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const rawPositions = (() => {
|
const rawPositions = (() => {
|
||||||
@@ -711,7 +711,7 @@ export class ParadexGateway {
|
|||||||
...Object.keys(total),
|
...Object.keys(total),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const assets: AsterAccountAsset[] = Array.from(assetKeys).map((asset) => ({
|
const assets: AccountAsset[] = Array.from(assetKeys).map((asset) => ({
|
||||||
asset,
|
asset,
|
||||||
walletBalance: String(total[asset] ?? 0),
|
walletBalance: String(total[asset] ?? 0),
|
||||||
availableBalance: String(free[asset] ?? 0),
|
availableBalance: String(free[asset] ?? 0),
|
||||||
@@ -737,7 +737,7 @@ export class ParadexGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AsterAccountSnapshot {
|
private mapBalanceToAccountSnapshotFromPositions(rawPositions: any[]): AccountSnapshot {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const positions = this.normalizePositions(rawPositions, now);
|
const positions = this.normalizePositions(rawPositions, now);
|
||||||
this.logger("positions", JSON.stringify({ raw: rawPositions, mapped: positions }));
|
this.logger("positions", JSON.stringify({ raw: rawPositions, mapped: positions }));
|
||||||
@@ -761,7 +761,7 @@ export class ParadexGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
|
private normalizePositions(rawPositions: any[], now: number): AccountSnapshot["positions"] {
|
||||||
return rawPositions
|
return rawPositions
|
||||||
.filter((pos) => pos)
|
.filter((pos) => pos)
|
||||||
.map((pos: any) => {
|
.map((pos: any) => {
|
||||||
@@ -807,7 +807,7 @@ export class ParadexGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapOrderToAsterOrder(order: CcxtOrder): AsterOrder {
|
private mapOrderToAsterOrder(order: CcxtOrder): Order {
|
||||||
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
|
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
|
||||||
const mappedType = this.mapCcxtOrderTypeToAster(order.type);
|
const mappedType = this.mapCcxtOrderTypeToAster(order.type);
|
||||||
return {
|
return {
|
||||||
@@ -830,7 +830,7 @@ export class ParadexGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
|
private mapOrderBookToDepth(orderbook: CcxtOrderBook): Depth {
|
||||||
return {
|
return {
|
||||||
lastUpdateId: orderbook.nonce || Date.now(),
|
lastUpdateId: orderbook.nonce || Date.now(),
|
||||||
bids: (orderbook.bids || [])
|
bids: (orderbook.bids || [])
|
||||||
@@ -843,7 +843,7 @@ export class ParadexGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
|
private mapTickerToAsterTicker(ticker: CcxtTicker): Ticker {
|
||||||
return {
|
return {
|
||||||
symbol: ticker.symbol,
|
symbol: ticker.symbol,
|
||||||
lastPrice: ticker.last?.toString() || "0",
|
lastPrice: ticker.last?.toString() || "0",
|
||||||
@@ -856,7 +856,7 @@ export class ParadexGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): AsterKline {
|
private mapOHLCVToKline(candle: CcxtOhlcv, interval: string): Kline {
|
||||||
const [timestampRaw, openRaw, highRaw, lowRaw, closeRaw, volumeRaw] = candle;
|
const [timestampRaw, openRaw, highRaw, lowRaw, closeRaw, volumeRaw] = candle;
|
||||||
const timestamp = typeof timestampRaw === "number" && Number.isFinite(timestampRaw)
|
const timestamp = typeof timestampRaw === "number" && Number.isFinite(timestampRaw)
|
||||||
? timestampRaw
|
? timestampRaw
|
||||||
@@ -926,7 +926,7 @@ export class ParadexGateway {
|
|||||||
return base[interval] ?? 60 * 1000;
|
return base[interval] ?? 60 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private upsertLocalOrder(order: AsterOrder): void {
|
private upsertLocalOrder(order: Order): void {
|
||||||
const key = String(order.orderId);
|
const key = String(order.orderId);
|
||||||
if (this.isOrderClosed(order)) {
|
if (this.isOrderClosed(order)) {
|
||||||
this.localOrders.delete(key);
|
this.localOrders.delete(key);
|
||||||
@@ -944,7 +944,7 @@ export class ParadexGateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateOrdersFromRemote(open: CcxtOrder[], _closed: CcxtOrder[]): void {
|
private updateOrdersFromRemote(open: CcxtOrder[], _closed: CcxtOrder[]): void {
|
||||||
const nextOpen = new Map<string, AsterOrder>();
|
const nextOpen = new Map<string, Order>();
|
||||||
|
|
||||||
for (const order of open) {
|
for (const order of open) {
|
||||||
const mapped = this.mapOrderToAsterOrder(order);
|
const mapped = this.mapOrderToAsterOrder(order);
|
||||||
@@ -973,7 +973,7 @@ export class ParadexGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isOrderClosed(order: AsterOrder): boolean {
|
private isOrderClosed(order: Order): boolean {
|
||||||
const status = (order.status ?? "").toUpperCase();
|
const status = (order.status ?? "").toUpperCase();
|
||||||
if (
|
if (
|
||||||
status.includes("CLOSE") ||
|
status.includes("CLOSE") ||
|
||||||
|
|||||||
@@ -1,93 +1,20 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "Paradex",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTC",
|
||||||
}
|
supportsTrailingStop: false,
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
supportsTriggerType: false,
|
||||||
params.timeInForce = intent.timeInForce;
|
stopDefaultReduceOnly: true,
|
||||||
}
|
stopDefaultClosePosition: true,
|
||||||
if (intent.reduceOnly !== undefined) {
|
stopPriceAsPrice: true,
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
closeDefaultClosePosition: true,
|
||||||
}
|
});
|
||||||
if (intent.closePosition !== undefined) {
|
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
timeInForce: intent.timeInForce,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
price: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
throw new Error("Paradex exchange does not support trailing stop orders");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
timeInForce: intent.timeInForce,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ExchangeAdapter } from "./adapter";
|
import type { ExchangeAdapter } from "./adapter";
|
||||||
import { createExchangeAdapter, resolveExchangeId, type SupportedExchangeId } from "./create-adapter";
|
import { createExchangeAdapter, resolveExchangeId, type SupportedExchangeId } from "./create-adapter";
|
||||||
import type { AsterCredentials } from "./aster-adapter";
|
import type { AsterCredentials } from "./aster/adapter";
|
||||||
import type { LighterCredentials } from "./lighter/adapter";
|
import type { LighterCredentials } from "./lighter/adapter";
|
||||||
import type { BackpackCredentials } from "./backpack/adapter";
|
import type { BackpackCredentials } from "./backpack/adapter";
|
||||||
import type { ParadexCredentials } from "./paradex/adapter";
|
import type { ParadexCredentials } from "./paradex/adapter";
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { setTimeout, clearTimeout } from "timers";
|
|
||||||
import type {
|
import type {
|
||||||
AccountListener,
|
AccountListener,
|
||||||
DepthListener,
|
DepthListener,
|
||||||
@@ -10,9 +9,9 @@ import type {
|
|||||||
RestHealthListener,
|
RestHealthListener,
|
||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import type { Order, CreateOrderParams } from "../types";
|
||||||
import { extractMessage } from "../../utils/errors";
|
|
||||||
import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway";
|
import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway";
|
||||||
|
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||||
|
|
||||||
export type { ConnectionEventListener, ConnectionEventType };
|
export type { ConnectionEventListener, ConnectionEventType };
|
||||||
|
|
||||||
@@ -31,11 +30,8 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
|||||||
|
|
||||||
private readonly gateway: StandxGateway;
|
private readonly gateway: StandxGateway;
|
||||||
private readonly symbol: string;
|
private readonly symbol: string;
|
||||||
private initPromise: Promise<void> | null = null;
|
private readonly safeInvoke = createSafeInvoke("StandxExchangeAdapter");
|
||||||
private readonly initContexts = new Set<string>();
|
private readonly init: ReturnType<typeof createInitManager>;
|
||||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
private retryDelayMs = 3000;
|
|
||||||
private lastInitErrorAt = 0;
|
|
||||||
|
|
||||||
constructor(credentials: StandxCredentials = {}) {
|
constructor(credentials: StandxCredentials = {}) {
|
||||||
const token = credentials.token ?? process.env.STANDX_TOKEN;
|
const token = credentials.token ?? process.env.STANDX_TOKEN;
|
||||||
@@ -52,6 +48,9 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
|||||||
signingKey: credentials.signingKey,
|
signingKey: credentials.signingKey,
|
||||||
logger: credentials.logger,
|
logger: credentials.logger,
|
||||||
});
|
});
|
||||||
|
this.init = createInitManager("StandxExchangeAdapter", () =>
|
||||||
|
this.gateway.ensureInitialized(this.symbol),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
@@ -59,52 +58,52 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(cb: AccountListener): void {
|
watchAccount(cb: AccountListener): void {
|
||||||
void this.ensureInitialized("watchAccount");
|
void this.init.ensureInitialized("watchAccount");
|
||||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: OrderListener): void {
|
watchOrders(cb: OrderListener): void {
|
||||||
void this.ensureInitialized("watchOrders");
|
void this.init.ensureInitialized("watchOrders");
|
||||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(symbol: string, cb: DepthListener): void {
|
watchDepth(symbol: string, cb: DepthListener): void {
|
||||||
void this.ensureInitialized("watchDepth");
|
void this.init.ensureInitialized("watchDepth");
|
||||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(symbol: string, cb: TickerListener): void {
|
watchTicker(symbol: string, cb: TickerListener): void {
|
||||||
void this.ensureInitialized("watchTicker");
|
void this.init.ensureInitialized("watchTicker");
|
||||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||||
void this.ensureInitialized("watchKlines");
|
void this.init.ensureInitialized("watchKlines");
|
||||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||||
void this.ensureInitialized("watchFundingRate");
|
void this.init.ensureInitialized("watchFundingRate");
|
||||||
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
await this.ensureInitialized("createOrder");
|
await this.init.ensureInitialized("createOrder");
|
||||||
return this.gateway.createOrder(params);
|
return this.gateway.createOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrder");
|
await this.init.ensureInitialized("cancelOrder");
|
||||||
await this.gateway.cancelOrder(params);
|
await this.gateway.cancelOrder(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelOrders");
|
await this.init.ensureInitialized("cancelOrders");
|
||||||
await this.gateway.cancelOrders(params);
|
await this.gateway.cancelOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||||
await this.ensureInitialized("cancelAllOrders");
|
await this.init.ensureInitialized("cancelAllOrders");
|
||||||
await this.gateway.cancelAllOrders(params);
|
await this.gateway.cancelAllOrders(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,18 +150,18 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
|||||||
* 查询当前真实的挂单状态(通过 HTTP API)
|
* 查询当前真实的挂单状态(通过 HTTP API)
|
||||||
* 用于验证实际挂单情况,防止取消请求丢失
|
* 用于验证实际挂单情况,防止取消请求丢失
|
||||||
*/
|
*/
|
||||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
async queryOpenOrders(): Promise<Order[]> {
|
||||||
await this.ensureInitialized("queryOpenOrders");
|
await this.init.ensureInitialized("queryOpenOrders");
|
||||||
return this.gateway.queryOpenOrders(this.symbol);
|
return this.gateway.queryOpenOrders(this.symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryAccountSnapshot() {
|
async queryAccountSnapshot() {
|
||||||
await this.ensureInitialized("queryAccountSnapshot");
|
await this.init.ensureInitialized("queryAccountSnapshot");
|
||||||
return this.gateway.queryAccountSnapshot();
|
return this.gateway.queryAccountSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||||
await this.ensureInitialized("changeMarginMode");
|
await this.init.ensureInitialized("changeMarginMode");
|
||||||
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,69 +170,7 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
|||||||
* 会查询当前挂单然后取消,并验证取消成功
|
* 会查询当前挂单然后取消,并验证取消成功
|
||||||
*/
|
*/
|
||||||
async forceCancelAllOrders(): Promise<boolean> {
|
async forceCancelAllOrders(): Promise<boolean> {
|
||||||
await this.ensureInitialized("forceCancelAllOrders");
|
await this.init.ensureInitialized("forceCancelAllOrders");
|
||||||
return this.gateway.forceCancelAllOrders(this.symbol);
|
return this.gateway.forceCancelAllOrders(this.symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
|
||||||
const wrapped = ((...args: any[]) => {
|
|
||||||
try {
|
|
||||||
cb(...args);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[StandxExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
|
||||||
}
|
|
||||||
}) as T;
|
|
||||||
return wrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureInitialized(context?: string): Promise<void> {
|
|
||||||
if (!this.initPromise) {
|
|
||||||
this.initContexts.clear();
|
|
||||||
this.initPromise = this.gateway
|
|
||||||
.ensureInitialized(this.symbol)
|
|
||||||
.then((value) => {
|
|
||||||
this.clearRetry();
|
|
||||||
return value;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.handleInitError("initialize", error);
|
|
||||||
this.initPromise = null;
|
|
||||||
this.scheduleRetry();
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (context && !this.initContexts.has(context)) {
|
|
||||||
this.initContexts.add(context);
|
|
||||||
this.initPromise.catch((error) => {
|
|
||||||
this.handleInitError(context, error);
|
|
||||||
this.scheduleRetry();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.initPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleRetry(): void {
|
|
||||||
if (this.retryTimer) return;
|
|
||||||
this.retryTimer = setTimeout(() => {
|
|
||||||
this.retryTimer = null;
|
|
||||||
if (this.initPromise) return;
|
|
||||||
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
|
|
||||||
void this.ensureInitialized("retry");
|
|
||||||
}, this.retryDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearRetry(): void {
|
|
||||||
if (this.retryTimer) {
|
|
||||||
clearTimeout(this.retryTimer);
|
|
||||||
this.retryTimer = null;
|
|
||||||
}
|
|
||||||
this.retryDelayMs = 3000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInitError(context: string, error: unknown): void {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - this.lastInitErrorAt < 5000) return;
|
|
||||||
this.lastInitErrorAt = now;
|
|
||||||
console.error(`[StandxExchangeAdapter] ${context} failed`, error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import type {
|
|||||||
TickerListener,
|
TickerListener,
|
||||||
} from "../adapter";
|
} from "../adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountAsset,
|
AccountAsset,
|
||||||
AsterAccountPosition,
|
AccountPosition,
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
OrderSide,
|
OrderSide,
|
||||||
OrderType,
|
OrderType,
|
||||||
@@ -77,7 +77,7 @@ type FundingState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type VirtualStop = {
|
type VirtualStop = {
|
||||||
order: AsterOrder;
|
order: Order;
|
||||||
stopPrice: number;
|
stopPrice: number;
|
||||||
side: OrderSide;
|
side: OrderSide;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
@@ -279,7 +279,7 @@ function resolutionFromInterval(interval: string): { resolution: string; seconds
|
|||||||
return { resolution: "1", seconds: 60 };
|
return { resolution: "1", seconds: 60 };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeOrderSnapshot(map: Map<string, AsterOrder>, order: AsterOrder): void {
|
function mergeOrderSnapshot(map: Map<string, Order>, order: Order): void {
|
||||||
const key = String(order.orderId);
|
const key = String(order.orderId);
|
||||||
const existing = map.get(key);
|
const existing = map.get(key);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -418,12 +418,12 @@ export class StandxGateway {
|
|||||||
private readonly fundingListeners = new Map<string, Set<FundingRateListener>>();
|
private readonly fundingListeners = new Map<string, Set<FundingRateListener>>();
|
||||||
private readonly connectionListeners = new Set<ConnectionEventListener>();
|
private readonly connectionListeners = new Set<ConnectionEventListener>();
|
||||||
|
|
||||||
private readonly openOrders = new Map<string, AsterOrder>();
|
private readonly openOrders = new Map<string, Order>();
|
||||||
private readonly positions = new Map<string, AsterAccountPosition>();
|
private readonly positions = new Map<string, AccountPosition>();
|
||||||
private readonly balances = new Map<string, AsterAccountAsset>();
|
private readonly balances = new Map<string, AccountAsset>();
|
||||||
private readonly virtualStops = new Map<string, VirtualStop>();
|
private readonly virtualStops = new Map<string, VirtualStop>();
|
||||||
|
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private readonly restHealthListeners = new Set<RestHealthListener>();
|
private readonly restHealthListeners = new Set<RestHealthListener>();
|
||||||
private restConsecutiveErrors = 0;
|
private restConsecutiveErrors = 0;
|
||||||
private restUnhealthy = false;
|
private restUnhealthy = false;
|
||||||
@@ -582,14 +582,14 @@ export class StandxGateway {
|
|||||||
* 查询当前真实的挂单状态(通过 HTTP API)
|
* 查询当前真实的挂单状态(通过 HTTP API)
|
||||||
* 用于在网络恢复后验证实际挂单情况
|
* 用于在网络恢复后验证实际挂单情况
|
||||||
*/
|
*/
|
||||||
async queryOpenOrders(symbol: string): Promise<AsterOrder[]> {
|
async queryOpenOrders(symbol: string): Promise<Order[]> {
|
||||||
const normalized = normalizeSymbol(symbol);
|
const normalized = normalizeSymbol(symbol);
|
||||||
const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", {
|
const ordersPayload = await this.requestJson<unknown>("/api/query_open_orders", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
params: { symbol: normalized },
|
params: { symbol: normalized },
|
||||||
});
|
});
|
||||||
const orders = extractOrders(ordersPayload);
|
const orders = extractOrders(ordersPayload);
|
||||||
const result: AsterOrder[] = [];
|
const result: Order[] = [];
|
||||||
for (const raw of orders) {
|
for (const raw of orders) {
|
||||||
const order = this.mapOrder(raw);
|
const order = this.mapOrder(raw);
|
||||||
result.push(order);
|
result.push(order);
|
||||||
@@ -618,7 +618,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
const normalizedSymbol = normalizeSymbol(params.symbol);
|
const normalizedSymbol = normalizeSymbol(params.symbol);
|
||||||
if (params.type === "STOP_MARKET") {
|
if (params.type === "STOP_MARKET") {
|
||||||
return this.createVirtualStopOrder(normalizedSymbol, params);
|
return this.createVirtualStopOrder(normalizedSymbol, params);
|
||||||
@@ -785,7 +785,7 @@ export class StandxGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createVirtualStopOrder(symbol: string, params: CreateOrderParams): Promise<AsterOrder> {
|
private async createVirtualStopOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
|
||||||
const stopPrice = Number(params.stopPrice);
|
const stopPrice = Number(params.stopPrice);
|
||||||
if (!Number.isFinite(stopPrice)) {
|
if (!Number.isFinite(stopPrice)) {
|
||||||
throw new Error("STOP_MARKET requires stopPrice for StandX");
|
throw new Error("STOP_MARKET requires stopPrice for StandX");
|
||||||
@@ -796,7 +796,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const clientOrderId = crypto.randomUUID();
|
const clientOrderId = crypto.randomUUID();
|
||||||
const order: AsterOrder = {
|
const order: Order = {
|
||||||
orderId: clientOrderId,
|
orderId: clientOrderId,
|
||||||
clientOrderId,
|
clientOrderId,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -825,7 +825,7 @@ export class StandxGateway {
|
|||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async submitOrder(symbol: string, params: CreateOrderParams): Promise<AsterOrder> {
|
private async submitOrder(symbol: string, params: CreateOrderParams): Promise<Order> {
|
||||||
const orderType = params.type === "MARKET" ? "market" : "limit";
|
const orderType = params.type === "MARKET" ? "market" : "limit";
|
||||||
const clientOrderId = crypto.randomUUID();
|
const clientOrderId = crypto.randomUUID();
|
||||||
const qty = toDecimalString(params.quantity);
|
const qty = toDecimalString(params.quantity);
|
||||||
@@ -870,7 +870,7 @@ export class StandxGateway {
|
|||||||
throw new Error(response.message ?? "StandX order rejected");
|
throw new Error(response.message ?? "StandX order rejected");
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const order: AsterOrder = {
|
const order: Order = {
|
||||||
orderId: clientOrderId,
|
orderId: clientOrderId,
|
||||||
clientOrderId,
|
clientOrderId,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -1038,7 +1038,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
this.logDebug("ws depth stats", detail);
|
this.logDebug("ws depth stats", detail);
|
||||||
}
|
}
|
||||||
const depth: AsterDepth = {
|
const depth: Depth = {
|
||||||
lastUpdateId: Number(message.seq ?? Date.now()),
|
lastUpdateId: Number(message.seq ?? Date.now()),
|
||||||
bids: finalBids,
|
bids: finalBids,
|
||||||
asks: finalAsks,
|
asks: finalAsks,
|
||||||
@@ -1355,7 +1355,7 @@ export class StandxGateway {
|
|||||||
console.log(`[StandxGateway] ws raw`, output);
|
console.log(`[StandxGateway] ws raw`, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitDepth(symbol: string, depth: AsterDepth): void {
|
private emitDepth(symbol: string, depth: Depth): void {
|
||||||
const listeners = this.depthListeners.get(normalizeSymbol(symbol));
|
const listeners = this.depthListeners.get(normalizeSymbol(symbol));
|
||||||
if (!listeners) return;
|
if (!listeners) return;
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
@@ -1367,7 +1367,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitTicker(symbol: string, ticker: AsterTicker): void {
|
private emitTicker(symbol: string, ticker: Ticker): void {
|
||||||
const listeners = this.tickerListeners.get(normalizeSymbol(symbol));
|
const listeners = this.tickerListeners.get(normalizeSymbol(symbol));
|
||||||
if (!listeners) return;
|
if (!listeners) return;
|
||||||
const price = Number(ticker.lastPrice);
|
const price = Number(ticker.lastPrice);
|
||||||
@@ -1406,7 +1406,7 @@ export class StandxGateway {
|
|||||||
(sum, position) => sum + Number(position.unrealizedProfit ?? 0),
|
(sum, position) => sum + Number(position.unrealizedProfit ?? 0),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
const snapshot: AsterAccountSnapshot = {
|
const snapshot: AccountSnapshot = {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
canWithdraw: true,
|
canWithdraw: true,
|
||||||
@@ -1427,7 +1427,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
private async refreshAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
try {
|
try {
|
||||||
const [balance, positions] = await Promise.all([
|
const [balance, positions] = await Promise.all([
|
||||||
this.requestJson<StandxBalanceSnapshot>("/api/query_balance", { method: "GET" }),
|
this.requestJson<StandxBalanceSnapshot>("/api/query_balance", { method: "GET" }),
|
||||||
@@ -1444,7 +1444,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
if (balance) {
|
if (balance) {
|
||||||
const token = "DUSD";
|
const token = "DUSD";
|
||||||
const asset: AsterAccountAsset = {
|
const asset: AccountAsset = {
|
||||||
asset: token,
|
asset: token,
|
||||||
walletBalance: String(balance.balance ?? "0"),
|
walletBalance: String(balance.balance ?? "0"),
|
||||||
availableBalance: String(balance.cross_available ?? balance.balance ?? "0"),
|
availableBalance: String(balance.cross_available ?? balance.balance ?? "0"),
|
||||||
@@ -1461,7 +1461,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
return await this.refreshAccountSnapshot();
|
return await this.refreshAccountSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1515,7 +1515,7 @@ export class StandxGateway {
|
|||||||
if (!data?.symbol) return;
|
if (!data?.symbol) return;
|
||||||
const bids = normalizeDepthLevels((data.bids ?? []).map(([price, qty]) => [String(price), String(qty)]), "bid");
|
const bids = normalizeDepthLevels((data.bids ?? []).map(([price, qty]) => [String(price), String(qty)]), "bid");
|
||||||
const asks = normalizeDepthLevels((data.asks ?? []).map(([price, qty]) => [String(price), String(qty)]), "ask");
|
const asks = normalizeDepthLevels((data.asks ?? []).map(([price, qty]) => [String(price), String(qty)]), "ask");
|
||||||
const depth: AsterDepth = {
|
const depth: Depth = {
|
||||||
lastUpdateId: Date.now(),
|
lastUpdateId: Date.now(),
|
||||||
bids,
|
bids,
|
||||||
asks,
|
asks,
|
||||||
@@ -1554,7 +1554,7 @@ export class StandxGateway {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!response || response.s !== "ok" || !Array.isArray(response.t)) return;
|
if (!response || response.s !== "ok" || !Array.isArray(response.t)) return;
|
||||||
const klines: AsterKline[] = response.t.map((openTime, index) => {
|
const klines: Kline[] = response.t.map((openTime, index) => {
|
||||||
const o = response.o?.[index];
|
const o = response.o?.[index];
|
||||||
const h = response.h?.[index];
|
const h = response.h?.[index];
|
||||||
const l = response.l?.[index];
|
const l = response.l?.[index];
|
||||||
@@ -1647,7 +1647,7 @@ export class StandxGateway {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapOrder(data: StandxOrder): AsterOrder {
|
private mapOrder(data: StandxOrder): Order {
|
||||||
const clientOrderId = data.cl_ord_id ? String(data.cl_ord_id) : data.id != null ? String(data.id) : "";
|
const clientOrderId = data.cl_ord_id ? String(data.cl_ord_id) : data.id != null ? String(data.id) : "";
|
||||||
const orderId = clientOrderId || (data.id != null ? String(data.id) : "") || crypto.randomUUID();
|
const orderId = clientOrderId || (data.id != null ? String(data.id) : "") || crypto.randomUUID();
|
||||||
const normalizedClientId = clientOrderId || orderId;
|
const normalizedClientId = clientOrderId || orderId;
|
||||||
@@ -1675,7 +1675,7 @@ export class StandxGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapPosition(data: StandxPosition): AsterAccountPosition {
|
private mapPosition(data: StandxPosition): AccountPosition {
|
||||||
return {
|
return {
|
||||||
symbol: data.symbol,
|
symbol: data.symbol,
|
||||||
positionAmt: String(data.qty ?? "0"),
|
positionAmt: String(data.qty ?? "0"),
|
||||||
@@ -1690,7 +1690,7 @@ export class StandxGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapBalance(data: StandxBalance): AsterAccountAsset {
|
private mapBalance(data: StandxBalance): AccountAsset {
|
||||||
const walletBalance = data.total ?? data.free ?? "0";
|
const walletBalance = data.total ?? data.free ?? "0";
|
||||||
const availableBalance = data.free ?? walletBalance;
|
const availableBalance = data.free ?? walletBalance;
|
||||||
return {
|
return {
|
||||||
@@ -1701,7 +1701,7 @@ export class StandxGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTicker(data: StandxPrice): AsterTicker {
|
private mapTicker(data: StandxPrice): Ticker {
|
||||||
const spread = data.spread ?? [data.spread_bid ?? "0", data.spread_ask ?? "0"];
|
const spread = data.spread ?? [data.spread_bid ?? "0", data.spread_ask ?? "0"];
|
||||||
const lastPrice = data.last_price ?? data.mark_price ?? data.index_price ?? data.mid_price ?? "0";
|
const lastPrice = data.last_price ?? data.mark_price ?? data.index_price ?? data.mid_price ?? "0";
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,94 +1,21 @@
|
|||||||
import type { AsterOrder, CreateOrderParams } from "../types";
|
import { createOrderHandlers } from "../order-handlers";
|
||||||
import type {
|
|
||||||
BaseOrderIntent,
|
|
||||||
ClosePositionIntent,
|
|
||||||
LimitOrderIntent,
|
|
||||||
MarketOrderIntent,
|
|
||||||
StopOrderIntent,
|
|
||||||
TrailingStopOrderIntent,
|
|
||||||
} from "../order-schema";
|
|
||||||
import { toStringBoolean } from "../order-schema";
|
|
||||||
|
|
||||||
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
|
const handlers = createOrderHandlers({
|
||||||
if (params.quantity === undefined) {
|
exchangeName: "StandX",
|
||||||
params.quantity = intent.quantity;
|
defaultLimitTimeInForce: "GTX",
|
||||||
}
|
defaultMarketTimeInForce: "IOC",
|
||||||
if (params.timeInForce === undefined && intent.timeInForce) {
|
defaultCloseTimeInForce: "IOC",
|
||||||
params.timeInForce = intent.timeInForce;
|
supportsTrailingStop: false,
|
||||||
}
|
supportsTriggerType: false,
|
||||||
if (intent.reduceOnly !== undefined) {
|
stopDefaultReduceOnly: true,
|
||||||
params.reduceOnly = toStringBoolean(intent.reduceOnly);
|
stopDefaultClosePosition: true,
|
||||||
}
|
closeDefaultClosePosition: true,
|
||||||
if (intent.closePosition !== undefined) {
|
});
|
||||||
params.closePosition = toStringBoolean(intent.closePosition);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
|
export const {
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
createLimitOrder,
|
||||||
{
|
createMarketOrder,
|
||||||
symbol: intent.symbol,
|
createStopOrder,
|
||||||
side: intent.side,
|
createTrailingStopOrder,
|
||||||
type: "LIMIT",
|
createClosePositionOrder,
|
||||||
quantity: intent.quantity,
|
} = handlers;
|
||||||
price: intent.price,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTX",
|
|
||||||
slPrice: intent.slPrice,
|
|
||||||
tpPrice: intent.tpPrice,
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
timeInForce: intent.timeInForce ?? "IOC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "STOP_MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
stopPrice: intent.stopPrice,
|
|
||||||
timeInForce: intent.timeInForce ?? "GTC",
|
|
||||||
reduceOnly: toStringBoolean(intent.reduceOnly ?? true),
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise<AsterOrder> {
|
|
||||||
throw new Error("StandX exchange does not support trailing stop orders");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
|
|
||||||
const params: CreateOrderParams = applyCommonFields(
|
|
||||||
{
|
|
||||||
symbol: intent.symbol,
|
|
||||||
side: intent.side,
|
|
||||||
type: "MARKET",
|
|
||||||
quantity: intent.quantity,
|
|
||||||
reduceOnly: "true",
|
|
||||||
closePosition: toStringBoolean(intent.closePosition ?? true),
|
|
||||||
timeInForce: intent.timeInForce ?? "IOC",
|
|
||||||
},
|
|
||||||
intent
|
|
||||||
);
|
|
||||||
return intent.adapter.createOrder(params);
|
|
||||||
}
|
|
||||||
|
|||||||
+15
-465
@@ -25,12 +25,12 @@ export interface CreateOrderParams {
|
|||||||
reduceOnly?: StringBoolean;
|
reduceOnly?: StringBoolean;
|
||||||
closePosition?: StringBoolean;
|
closePosition?: StringBoolean;
|
||||||
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
triggerType?: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
||||||
// StandX TPSL 参数
|
slPrice?: number;
|
||||||
slPrice?: number; // 止损价格
|
tpPrice?: number;
|
||||||
tpPrice?: number; // 止盈价格
|
clientOrderId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterAccountPosition {
|
export interface AccountPosition {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
positionAmt: string;
|
positionAmt: string;
|
||||||
entryPrice: string;
|
entryPrice: string;
|
||||||
@@ -51,210 +51,7 @@ export interface AsterAccountPosition {
|
|||||||
markPrice?: string;
|
markPrice?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrvtOrderLeg {
|
export interface AccountAsset {
|
||||||
instrument: string;
|
|
||||||
size: string;
|
|
||||||
limit_price?: string;
|
|
||||||
is_buying_asset?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GrvtTimeInForce =
|
|
||||||
| "GOOD_TILL_TIME"
|
|
||||||
| "ALL_OR_NONE"
|
|
||||||
| "IMMEDIATE_OR_CANCEL"
|
|
||||||
| "FILL_OR_KILL";
|
|
||||||
|
|
||||||
export interface GrvtOrderMetadata {
|
|
||||||
client_order_id?: string;
|
|
||||||
create_time?: string;
|
|
||||||
broker?: string | null;
|
|
||||||
trigger?: GrvtTriggerMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtOrderState {
|
|
||||||
status?: string;
|
|
||||||
reject_reason?: string | null;
|
|
||||||
book_size?: string[];
|
|
||||||
traded_size?: string[];
|
|
||||||
update_time?: string;
|
|
||||||
avg_fill_price?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtOrder {
|
|
||||||
order_id: string;
|
|
||||||
client_order_id?: string;
|
|
||||||
sub_account_id?: string;
|
|
||||||
is_market?: boolean;
|
|
||||||
time_in_force?: GrvtTimeInForce;
|
|
||||||
post_only?: boolean;
|
|
||||||
reduce_only?: boolean;
|
|
||||||
legs?: GrvtOrderLeg[];
|
|
||||||
metadata?: GrvtOrderMetadata;
|
|
||||||
state?: GrvtOrderState;
|
|
||||||
instrument?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtTrade {
|
|
||||||
price: string;
|
|
||||||
size: string;
|
|
||||||
taker_side: "BUY" | "SELL";
|
|
||||||
timestamp: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtTradeHistoryResponse {
|
|
||||||
result?: GrvtTrade[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtWebsocketMessage<T> {
|
|
||||||
stream: string;
|
|
||||||
selector: string;
|
|
||||||
sequence_number?: string;
|
|
||||||
feed: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtOrderUpdateFeed {
|
|
||||||
order_id: string;
|
|
||||||
client_order_id?: string;
|
|
||||||
sub_account_id?: string;
|
|
||||||
state?: GrvtOrderState;
|
|
||||||
traded_size?: string[];
|
|
||||||
update_time?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtPositionUpdateFeed {
|
|
||||||
instrument: string;
|
|
||||||
size: string;
|
|
||||||
entry_price?: string;
|
|
||||||
mark_price?: string;
|
|
||||||
unrealized_pnl?: string;
|
|
||||||
sub_account_id?: string;
|
|
||||||
update_time?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtDepthUpdateFeed {
|
|
||||||
instrument: string;
|
|
||||||
bids: GrvtDepthLevel[];
|
|
||||||
asks: GrvtDepthLevel[];
|
|
||||||
event_time?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtTickerUpdateFeed {
|
|
||||||
instrument: string;
|
|
||||||
mark_price?: string;
|
|
||||||
last_trade_price?: string;
|
|
||||||
best_bid_price?: string;
|
|
||||||
best_ask_price?: string;
|
|
||||||
volume_24h?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtOpenOrdersResponse {
|
|
||||||
result?: GrvtOrder[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtPositionsResponse {
|
|
||||||
result?: GrvtPosition[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtPosition {
|
|
||||||
instrument: string;
|
|
||||||
size: string;
|
|
||||||
entry_price?: string;
|
|
||||||
mark_price?: string;
|
|
||||||
unrealized_pnl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtAccountSnapshot {
|
|
||||||
total_unrealized_pnl?: string;
|
|
||||||
positions: GrvtPosition[];
|
|
||||||
settle_currency?: string;
|
|
||||||
available_balance?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtBalancesResponse {
|
|
||||||
result?: {
|
|
||||||
total_unrealized_pnl?: string;
|
|
||||||
positions?: GrvtPosition[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtDepthLevel {
|
|
||||||
price: string;
|
|
||||||
size: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtDepth {
|
|
||||||
instrument: string;
|
|
||||||
event_time?: string;
|
|
||||||
bids: GrvtDepthLevel[];
|
|
||||||
asks: GrvtDepthLevel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtTicker {
|
|
||||||
instrument: string;
|
|
||||||
mark_price?: string;
|
|
||||||
last_trade_price?: string;
|
|
||||||
best_bid_price?: string;
|
|
||||||
best_ask_price?: string;
|
|
||||||
volume_24h?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtKline {
|
|
||||||
open_time: number;
|
|
||||||
close_time: number;
|
|
||||||
open: string;
|
|
||||||
high: string;
|
|
||||||
low: string;
|
|
||||||
close: string;
|
|
||||||
volume: string;
|
|
||||||
number_of_trades?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtSignature {
|
|
||||||
signer: string;
|
|
||||||
r: string;
|
|
||||||
s: string;
|
|
||||||
v: number;
|
|
||||||
expiration: string;
|
|
||||||
nonce: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtUnsignedOrderLeg {
|
|
||||||
instrument: string;
|
|
||||||
size: string;
|
|
||||||
limit_price?: string;
|
|
||||||
is_buying_asset: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtTriggerMetadata {
|
|
||||||
trigger_type: "UNSPECIFIED" | "TAKE_PROFIT" | "STOP_LOSS";
|
|
||||||
tpsl: {
|
|
||||||
trigger_by: "UNSPECIFIED" | "INDEX" | "LAST" | "MID" | "MARK";
|
|
||||||
trigger_price: string;
|
|
||||||
close_position: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtOrderMetadataInput {
|
|
||||||
client_order_id: string;
|
|
||||||
trigger?: GrvtTriggerMetadata;
|
|
||||||
broker?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtUnsignedOrder {
|
|
||||||
sub_account_id: string;
|
|
||||||
is_market: boolean;
|
|
||||||
time_in_force: GrvtTimeInForce;
|
|
||||||
post_only: boolean;
|
|
||||||
reduce_only: boolean;
|
|
||||||
legs: GrvtUnsignedOrderLeg[];
|
|
||||||
metadata: GrvtOrderMetadataInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GrvtSignedOrder extends GrvtUnsignedOrder {
|
|
||||||
signature: GrvtSignature;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterAccountAsset {
|
|
||||||
asset: string;
|
asset: string;
|
||||||
walletBalance: string;
|
walletBalance: string;
|
||||||
availableBalance: string;
|
availableBalance: string;
|
||||||
@@ -272,7 +69,7 @@ export interface AsterAccountAsset {
|
|||||||
marginAvailable?: boolean;
|
marginAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterAccountSnapshot {
|
export interface AccountSnapshot {
|
||||||
canTrade: boolean;
|
canTrade: boolean;
|
||||||
canDeposit: boolean;
|
canDeposit: boolean;
|
||||||
canWithdraw: boolean;
|
canWithdraw: boolean;
|
||||||
@@ -288,8 +85,8 @@ export interface AsterAccountSnapshot {
|
|||||||
totalCrossUnPnl?: string;
|
totalCrossUnPnl?: string;
|
||||||
availableBalance?: string;
|
availableBalance?: string;
|
||||||
maxWithdrawAmount?: string;
|
maxWithdrawAmount?: string;
|
||||||
positions: AsterAccountPosition[];
|
positions: AccountPosition[];
|
||||||
assets: AsterAccountAsset[];
|
assets: AccountAsset[];
|
||||||
marketType?: "perp" | "spot";
|
marketType?: "perp" | "spot";
|
||||||
baseAsset?: string;
|
baseAsset?: string;
|
||||||
quoteAsset?: string;
|
quoteAsset?: string;
|
||||||
@@ -297,22 +94,22 @@ export interface AsterAccountSnapshot {
|
|||||||
quoteAssetId?: number;
|
quoteAssetId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterDepthLevel extends Array<string> {
|
export interface DepthLevel extends Array<string> {
|
||||||
0: string; // price
|
0: string; // price
|
||||||
1: string; // quantity
|
1: string; // quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterDepth {
|
export interface Depth {
|
||||||
lastUpdateId: number;
|
lastUpdateId: number;
|
||||||
bids: AsterDepthLevel[];
|
bids: DepthLevel[];
|
||||||
asks: AsterDepthLevel[];
|
asks: DepthLevel[];
|
||||||
eventTime?: number;
|
eventTime?: number;
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
tradeTime?: number;
|
tradeTime?: number;
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterTicker {
|
export interface Ticker {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
lastPrice: string;
|
lastPrice: string;
|
||||||
openPrice: string;
|
openPrice: string;
|
||||||
@@ -336,254 +133,7 @@ export interface AsterTicker {
|
|||||||
count?: number;
|
count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterSpotRateLimit {
|
export interface Kline {
|
||||||
rateLimitType: string;
|
|
||||||
interval: string;
|
|
||||||
intervalNum: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotExchangeFilter {
|
|
||||||
filterType: string;
|
|
||||||
[key: string]: string | number | boolean | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterFuturesSymbolFilter {
|
|
||||||
filterType: string;
|
|
||||||
tickSize?: string;
|
|
||||||
stepSize?: string;
|
|
||||||
minPrice?: string;
|
|
||||||
maxPrice?: string;
|
|
||||||
minQty?: string;
|
|
||||||
maxQty?: string;
|
|
||||||
[key: string]: string | number | boolean | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterFuturesSymbolInfo {
|
|
||||||
symbol: string;
|
|
||||||
pair?: string;
|
|
||||||
contractType?: string;
|
|
||||||
pricePrecision?: number;
|
|
||||||
quantityPrecision?: number;
|
|
||||||
baseAssetPrecision?: number;
|
|
||||||
quotePrecision?: number;
|
|
||||||
underlyingType?: string;
|
|
||||||
filters?: AsterFuturesSymbolFilter[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterFuturesExchangeInfo {
|
|
||||||
timezone?: string;
|
|
||||||
serverTime?: number;
|
|
||||||
symbols?: AsterFuturesSymbolInfo[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotAssetInfo {
|
|
||||||
asset: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotSymbolInfo {
|
|
||||||
symbol: string;
|
|
||||||
status: string;
|
|
||||||
baseAsset: string;
|
|
||||||
quoteAsset: string;
|
|
||||||
baseAssetPrecision?: number;
|
|
||||||
quotePrecision?: number;
|
|
||||||
pricePrecision?: number;
|
|
||||||
quantityPrecision?: number;
|
|
||||||
orderTypes: string[];
|
|
||||||
timeInForce: string[];
|
|
||||||
ocoAllowed: boolean;
|
|
||||||
filters: AsterSpotExchangeFilter[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotExchangeInfo {
|
|
||||||
timezone: string;
|
|
||||||
serverTime: number;
|
|
||||||
rateLimits: AsterSpotRateLimit[];
|
|
||||||
exchangeFilters: AsterSpotExchangeFilter[];
|
|
||||||
assets?: AsterSpotAssetInfo[];
|
|
||||||
symbols: AsterSpotSymbolInfo[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotDepth {
|
|
||||||
lastUpdateId: number;
|
|
||||||
E?: number;
|
|
||||||
T?: number;
|
|
||||||
bids: AsterDepthLevel[];
|
|
||||||
asks: AsterDepthLevel[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotTrade {
|
|
||||||
id: number;
|
|
||||||
price: string;
|
|
||||||
qty: string;
|
|
||||||
baseQty?: string;
|
|
||||||
quoteQty?: string;
|
|
||||||
time: number;
|
|
||||||
isBuyerMaker: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotHistoricalTrade extends AsterSpotTrade {
|
|
||||||
isBestMatch?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotAggTrade {
|
|
||||||
a: number;
|
|
||||||
p: string;
|
|
||||||
q: string;
|
|
||||||
f: number;
|
|
||||||
l: number;
|
|
||||||
T: number;
|
|
||||||
m: boolean;
|
|
||||||
M?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotKline {
|
|
||||||
openTime: number;
|
|
||||||
open: string;
|
|
||||||
high: string;
|
|
||||||
low: string;
|
|
||||||
close: string;
|
|
||||||
volume: string;
|
|
||||||
closeTime: number;
|
|
||||||
quoteAssetVolume: string;
|
|
||||||
numberOfTrades: number;
|
|
||||||
takerBuyBaseAssetVolume: string;
|
|
||||||
takerBuyQuoteAssetVolume: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotTicker24h {
|
|
||||||
symbol: string;
|
|
||||||
priceChange: string;
|
|
||||||
priceChangePercent: string;
|
|
||||||
weightedAvgPrice: string;
|
|
||||||
prevClosePrice: string;
|
|
||||||
lastPrice: string;
|
|
||||||
lastQty: string;
|
|
||||||
bidPrice: string;
|
|
||||||
bidQty: string;
|
|
||||||
askPrice: string;
|
|
||||||
askQty: string;
|
|
||||||
openPrice: string;
|
|
||||||
highPrice: string;
|
|
||||||
lowPrice: string;
|
|
||||||
volume: string;
|
|
||||||
quoteVolume: string;
|
|
||||||
openTime: number;
|
|
||||||
closeTime: number;
|
|
||||||
firstId: number;
|
|
||||||
lastId: number;
|
|
||||||
count: number;
|
|
||||||
baseAsset?: string;
|
|
||||||
quoteAsset?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotPriceTicker {
|
|
||||||
symbol: string;
|
|
||||||
price: string;
|
|
||||||
time?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotBookTicker {
|
|
||||||
symbol: string;
|
|
||||||
bidPrice: string;
|
|
||||||
bidQty: string;
|
|
||||||
askPrice: string;
|
|
||||||
askQty: string;
|
|
||||||
time?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotCommissionRate {
|
|
||||||
symbol: string;
|
|
||||||
makerCommissionRate: string;
|
|
||||||
takerCommissionRate: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateSpotOrderParams {
|
|
||||||
symbol: string;
|
|
||||||
side: OrderSide;
|
|
||||||
type: OrderType;
|
|
||||||
timeInForce?: TimeInForce;
|
|
||||||
quantity?: number | string;
|
|
||||||
quoteOrderQty?: number | string;
|
|
||||||
price?: number | string;
|
|
||||||
newClientOrderId?: string;
|
|
||||||
stopPrice?: number | string;
|
|
||||||
recvWindow?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CancelSpotOrderParams {
|
|
||||||
symbol: string;
|
|
||||||
orderId?: number | string;
|
|
||||||
origClientOrderId?: string;
|
|
||||||
recvWindow?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QuerySpotOrderParams extends CancelSpotOrderParams {}
|
|
||||||
|
|
||||||
export interface SpotOpenOrdersParams {
|
|
||||||
symbol?: string;
|
|
||||||
recvWindow?: number;
|
|
||||||
orderIdList?: Array<number | string>;
|
|
||||||
origClientOrderIdList?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SpotAllOrdersParams {
|
|
||||||
symbol: string;
|
|
||||||
orderId?: number;
|
|
||||||
startTime?: number;
|
|
||||||
endTime?: number;
|
|
||||||
limit?: number;
|
|
||||||
recvWindow?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotAccountBalance {
|
|
||||||
asset: string;
|
|
||||||
free: string;
|
|
||||||
locked: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotAccount {
|
|
||||||
feeTier: number;
|
|
||||||
canTrade: boolean;
|
|
||||||
canDeposit: boolean;
|
|
||||||
canWithdraw: boolean;
|
|
||||||
canBurnAsset?: boolean;
|
|
||||||
updateTime: number;
|
|
||||||
makerCommission?: string;
|
|
||||||
takerCommission?: string;
|
|
||||||
buyerCommission?: string;
|
|
||||||
sellerCommission?: string;
|
|
||||||
balances: AsterSpotAccountBalance[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SpotUserTradesParams {
|
|
||||||
symbol?: string;
|
|
||||||
orderId?: number;
|
|
||||||
startTime?: number;
|
|
||||||
endTime?: number;
|
|
||||||
fromId?: number;
|
|
||||||
limit?: number;
|
|
||||||
recvWindow?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterSpotUserTrade {
|
|
||||||
symbol: string;
|
|
||||||
id: number;
|
|
||||||
orderId: number;
|
|
||||||
side: OrderSide;
|
|
||||||
price: string;
|
|
||||||
qty: string;
|
|
||||||
quoteQty?: string;
|
|
||||||
commission: string;
|
|
||||||
commissionAsset: string;
|
|
||||||
time: number;
|
|
||||||
counterpartyId?: number;
|
|
||||||
maker: boolean;
|
|
||||||
buyer: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsterKline {
|
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
eventTime?: number;
|
eventTime?: number;
|
||||||
symbol?: string;
|
symbol?: string;
|
||||||
@@ -604,7 +154,7 @@ export interface AsterKline {
|
|||||||
isClosed?: boolean;
|
isClosed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsterOrder {
|
export interface Order {
|
||||||
orderId: number | string;
|
orderId: number | string;
|
||||||
clientOrderId: string;
|
clientOrderId: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { BasisArbConfig } from "../config";
|
import type { BasisArbConfig } from "../config";
|
||||||
import type { ExchangeAdapter, FundingRateSnapshot } from "../exchanges/adapter";
|
import type { ExchangeAdapter, FundingRateSnapshot } from "../exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterSpotBookTicker } from "../exchanges/types";
|
import type { AccountSnapshot, Depth } from "../exchanges/types";
|
||||||
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/client";
|
import type { AsterSpotBookTicker } from "../exchanges/aster/types";
|
||||||
|
import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/gateway";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
import { StrategyEventEmitter } from "./common/event-emitter";
|
import { StrategyEventEmitter } from "./common/event-emitter";
|
||||||
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
import { safeSubscribe, type LogHandler } from "./common/subscriptions";
|
||||||
@@ -155,7 +156,7 @@ export class BasisArbEngine {
|
|||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
|
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.futuresSymbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.futuresSymbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.applyFuturesDepth(depth);
|
this.applyFuturesDepth(depth);
|
||||||
@@ -168,7 +169,7 @@ export class BasisArbEngine {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") {
|
if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") {
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.applySpotDepth(depth);
|
this.applySpotDepth(depth);
|
||||||
@@ -196,7 +197,7 @@ export class BasisArbEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.applyAccountSnapshot(snapshot);
|
this.applyAccountSnapshot(snapshot);
|
||||||
@@ -210,7 +211,7 @@ export class BasisArbEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyFuturesDepth(depth: AsterDepth): void {
|
private applyFuturesDepth(depth: Depth): void {
|
||||||
if (!depth?.bids?.length || !depth?.asks?.length) {
|
if (!depth?.bids?.length || !depth?.asks?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -360,7 +361,7 @@ export class BasisArbEngine {
|
|||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private applySpotDepth(depth: AsterDepth): void {
|
private applySpotDepth(depth: Depth): void {
|
||||||
if (!depth?.bids?.length || !depth?.asks?.length) {
|
if (!depth?.bids?.length || !depth?.asks?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -395,7 +396,7 @@ export class BasisArbEngine {
|
|||||||
this.emitUpdate();
|
this.emitUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void {
|
private applyAccountSnapshot(snapshot: AccountSnapshot): void {
|
||||||
const assets = Array.isArray(snapshot.assets) ? snapshot.assets : [];
|
const assets = Array.isArray(snapshot.assets) ? snapshot.assets : [];
|
||||||
|
|
||||||
const spotBalances: SpotBalanceStateEntry[] = [];
|
const spotBalances: SpotBalanceStateEntry[] = [];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import NodeWebSocket from "ws";
|
import NodeWebSocket from "ws";
|
||||||
import type { AsterDepthLevel } from "../../exchanges/types";
|
import type { DepthLevel } from "../../exchanges/types";
|
||||||
import type { DepthImbalance } from "../../utils/depth";
|
import type { DepthImbalance } from "../../utils/depth";
|
||||||
|
|
||||||
const WebSocketCtor: typeof globalThis.WebSocket =
|
const WebSocketCtor: typeof globalThis.WebSocket =
|
||||||
@@ -56,14 +56,14 @@ export type BinanceConnectionListener = (state: BinanceConnectionState) => void;
|
|||||||
interface DepthUpdateEvent {
|
interface DepthUpdateEvent {
|
||||||
U: number;
|
U: number;
|
||||||
u: number;
|
u: number;
|
||||||
bids: AsterDepthLevel[];
|
bids: DepthLevel[];
|
||||||
asks: AsterDepthLevel[];
|
asks: DepthLevel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DepthSnapshotResponse {
|
interface DepthSnapshotResponse {
|
||||||
lastUpdateId: number;
|
lastUpdateId: number;
|
||||||
bids: AsterDepthLevel[];
|
bids: DepthLevel[];
|
||||||
asks: AsterDepthLevel[];
|
asks: DepthLevel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BinanceDepthTracker {
|
export class BinanceDepthTracker {
|
||||||
@@ -540,7 +540,7 @@ export class BinanceDepthTracker {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyLevels(book: Map<string, number>, levels: AsterDepthLevel[]): void {
|
private applyLevels(book: Map<string, number>, levels: DepthLevel[]): void {
|
||||||
for (const level of levels) {
|
for (const level of levels) {
|
||||||
const priceRaw = level?.[0];
|
const priceRaw = level?.[0];
|
||||||
const qtyRaw = level?.[1];
|
const qtyRaw = level?.[1];
|
||||||
@@ -657,8 +657,8 @@ export class BinanceDepthTracker {
|
|||||||
throw new Error("invalid lastUpdateId");
|
throw new Error("invalid lastUpdateId");
|
||||||
}
|
}
|
||||||
|
|
||||||
const bids = Array.isArray(json.bids) ? (json.bids as AsterDepthLevel[]) : [];
|
const bids = Array.isArray(json.bids) ? (json.bids as DepthLevel[]) : [];
|
||||||
const asks = Array.isArray(json.asks) ? (json.asks as AsterDepthLevel[]) : [];
|
const asks = Array.isArray(json.asks) ? (json.asks as DepthLevel[]) : [];
|
||||||
this.lastRestSyncAt = Date.now();
|
this.lastRestSyncAt = Date.now();
|
||||||
this.restConsecutiveFailures = 0;
|
this.restConsecutiveFailures = 0;
|
||||||
this.restLastError = null;
|
this.restLastError = null;
|
||||||
@@ -697,8 +697,8 @@ export class BinanceDepthTracker {
|
|||||||
|
|
||||||
const bidsRaw = Array.isArray(payload.b) ? payload.b : [];
|
const bidsRaw = Array.isArray(payload.b) ? payload.b : [];
|
||||||
const asksRaw = Array.isArray(payload.a) ? payload.a : [];
|
const asksRaw = Array.isArray(payload.a) ? payload.a : [];
|
||||||
const bids = bidsRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
|
const bids = bidsRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
|
||||||
const asks = asksRaw.filter((level): level is AsterDepthLevel => Array.isArray(level)) as AsterDepthLevel[];
|
const asks = asksRaw.filter((level): level is DepthLevel => Array.isArray(level)) as DepthLevel[];
|
||||||
|
|
||||||
return { U, u, bids, asks };
|
return { U, u, bids, asks };
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ import type { GridDirection } from "../../config";
|
|||||||
const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
|
const DATA_DIR = process.env.GRID_DATA_DIR?.trim() || path.resolve("data");
|
||||||
const GRID_FILE = path.resolve(DATA_DIR, "grid-record.json");
|
const GRID_FILE = path.resolve(DATA_DIR, "grid-record.json");
|
||||||
|
|
||||||
|
/** State of a single grid level */
|
||||||
|
export type LevelState = "idle" | "filled" | "exit_placed";
|
||||||
|
|
||||||
|
export interface StoredLevelInfo {
|
||||||
|
state: LevelState;
|
||||||
|
/** The grid level index where ENTRY was filled */
|
||||||
|
sourceLevel: number;
|
||||||
|
/** The grid level index where EXIT is targeted (closeTarget) */
|
||||||
|
targetLevel: number | null;
|
||||||
|
/** The orderId of the EXIT order on exchange (if exit_placed) */
|
||||||
|
exitOrderId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StoredGridState {
|
export interface StoredGridState {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
lowerPrice: number;
|
lowerPrice: number;
|
||||||
@@ -13,8 +26,8 @@ export interface StoredGridState {
|
|||||||
orderSize: number;
|
orderSize: number;
|
||||||
maxPositionSize: number;
|
maxPositionSize: number;
|
||||||
direction: GridDirection;
|
direction: GridDirection;
|
||||||
longExposure: Record<string, number>;
|
/** Per-level state: key is level index string */
|
||||||
shortExposure: Record<string, number>;
|
levels: Record<string, StoredLevelInfo>;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+854
-708
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import type { TradingConfig } from "../config";
|
import type { TradingConfig } from "../config";
|
||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterOrder, AsterTicker } from "../exchanges/types";
|
import type { AccountSnapshot, Order, Ticker } from "../exchanges/types";
|
||||||
import {
|
import {
|
||||||
calcStopLossPrice,
|
calcStopLossPrice,
|
||||||
calcTrailingActivationPrice,
|
calcTrailingActivationPrice,
|
||||||
@@ -30,11 +30,11 @@ export interface GuardianEngineSnapshot {
|
|||||||
unrealized: number;
|
unrealized: number;
|
||||||
targetStopPrice: number | null;
|
targetStopPrice: number | null;
|
||||||
trailingActivationPrice: number | null;
|
trailingActivationPrice: number | null;
|
||||||
stopOrder: AsterOrder | null;
|
stopOrder: Order | null;
|
||||||
trailingOrder: AsterOrder | null;
|
trailingOrder: Order | null;
|
||||||
requiresStop: boolean;
|
requiresStop: boolean;
|
||||||
tradeLog: TradeLogEntry[];
|
tradeLog: TradeLogEntry[];
|
||||||
openOrders: AsterOrder[];
|
openOrders: Order[];
|
||||||
lastUpdated: number | null;
|
lastUpdated: number | null;
|
||||||
guardStatus: "idle" | "protecting" | "pending";
|
guardStatus: "idle" | "protecting" | "pending";
|
||||||
}
|
}
|
||||||
@@ -43,9 +43,9 @@ type GuardianEngineEvent = "update";
|
|||||||
type GuardianEngineListener = (snapshot: GuardianEngineSnapshot) => void;
|
type GuardianEngineListener = (snapshot: GuardianEngineSnapshot) => void;
|
||||||
|
|
||||||
export class GuardianEngine {
|
export class GuardianEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
|
|
||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
@@ -101,7 +101,7 @@ export class GuardianEngine {
|
|||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.accountSnapshot = snapshot;
|
this.accountSnapshot = snapshot;
|
||||||
@@ -114,7 +114,7 @@ export class GuardianEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterOrder[]>(
|
safeSubscribe<Order[]>(
|
||||||
this.exchange.watchOrders.bind(this.exchange),
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
(orders) => {
|
(orders) => {
|
||||||
this.synchronizeLocks(orders);
|
this.synchronizeLocks(orders);
|
||||||
@@ -141,7 +141,7 @@ export class GuardianEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterTicker>(
|
safeSubscribe<Ticker>(
|
||||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
(ticker) => {
|
(ticker) => {
|
||||||
this.tickerSnapshot = ticker;
|
this.tickerSnapshot = ticker;
|
||||||
@@ -155,7 +155,7 @@ export class GuardianEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
private synchronizeLocks(orders: Order[] | null | undefined): void {
|
||||||
const list = Array.isArray(orders) ? orders : [];
|
const list = Array.isArray(orders) ? orders : [];
|
||||||
Object.keys(this.pending).forEach((type) => {
|
Object.keys(this.pending).forEach((type) => {
|
||||||
const pendingId = this.pending[type];
|
const pendingId = this.pending[type];
|
||||||
@@ -261,8 +261,8 @@ export class GuardianEngine {
|
|||||||
price: number;
|
price: number;
|
||||||
stopPrice: number;
|
stopPrice: number;
|
||||||
activationPrice: number;
|
activationPrice: number;
|
||||||
currentStop?: AsterOrder;
|
currentStop?: Order;
|
||||||
currentTrailing?: AsterOrder;
|
currentTrailing?: Order;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { position, direction, stopSide, price, stopPrice, activationPrice, currentStop, currentTrailing } = params;
|
const { position, direction, stopSide, price, stopPrice, activationPrice, currentStop, currentTrailing } = params;
|
||||||
|
|
||||||
@@ -416,7 +416,7 @@ export class GuardianEngine {
|
|||||||
|
|
||||||
private async tryReplaceStop(
|
private async tryReplaceStop(
|
||||||
side: "BUY" | "SELL",
|
side: "BUY" | "SELL",
|
||||||
currentOrder: AsterOrder,
|
currentOrder: Order,
|
||||||
nextStopPrice: number,
|
nextStopPrice: number,
|
||||||
lastPrice: number
|
lastPrice: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -559,7 +559,7 @@ export class GuardianEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isProtectiveOrder(order: AsterOrder): boolean {
|
private isProtectiveOrder(order: Order): boolean {
|
||||||
if (order.symbol !== this.config.symbol) {
|
if (order.symbol !== this.config.symbol) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -571,14 +571,14 @@ export class GuardianEngine {
|
|||||||
return type === "STOP_MARKET" || hasStopPrice;
|
return type === "STOP_MARKET" || hasStopPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findStopOrder(side: "BUY" | "SELL"): AsterOrder | undefined {
|
private findStopOrder(side: "BUY" | "SELL"): Order | undefined {
|
||||||
return this.openOrders.find((order) => {
|
return this.openOrders.find((order) => {
|
||||||
const hasStopPrice = Number.isFinite(Number(order.stopPrice)) && Number(order.stopPrice) > 0;
|
const hasStopPrice = Number.isFinite(Number(order.stopPrice)) && Number(order.stopPrice) > 0;
|
||||||
return order.side === side && (order.type === "STOP_MARKET" || hasStopPrice);
|
return order.side === side && (order.type === "STOP_MARKET" || hasStopPrice);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private findTrailingOrder(side: "BUY" | "SELL"): AsterOrder | undefined {
|
private findTrailingOrder(side: "BUY" | "SELL"): Order | undefined {
|
||||||
return this.openOrders.find((order) => order.type === "TRAILING_STOP_MARKET" && order.side === side);
|
return this.openOrders.find((order) => order.type === "TRAILING_STOP_MARKET" && order.side === side);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { LiquidityMakerConfig } from "../config";
|
import type { LiquidityMakerConfig } from "../config";
|
||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { formatPriceToString } from "../utils/math";
|
import { formatPriceToString } from "../utils/math";
|
||||||
import { createTradeLog } from "../logging/trade-log";
|
import { createTradeLog } from "../logging/trade-log";
|
||||||
@@ -65,12 +65,12 @@ type MakerListener = (snapshot: LiquidityMakerEngineSnapshot) => void;
|
|||||||
const EPS = 1e-5;
|
const EPS = 1e-5;
|
||||||
|
|
||||||
export class LiquidityMakerEngine {
|
export class LiquidityMakerEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private depthSnapshot: AsterDepth | null = null;
|
private depthSnapshot: Depth | null = null;
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
private lastKline: AsterKline | null = null;
|
private lastKline: Kline | null = null;
|
||||||
private liveCandle: { startMs: number; open: number; close: number } | null = null;
|
private liveCandle: { startMs: number; open: number; close: number } | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
|
|
||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
@@ -180,7 +180,7 @@ export class LiquidityMakerEngine {
|
|||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.accountSnapshot = snapshot;
|
this.accountSnapshot = snapshot;
|
||||||
@@ -224,7 +224,7 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterOrder[]>(
|
safeSubscribe<Order[]>(
|
||||||
this.exchange.watchOrders.bind(this.exchange),
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
(orders) => {
|
(orders) => {
|
||||||
this.syncLocksWithOrders(orders);
|
this.syncLocksWithOrders(orders);
|
||||||
@@ -232,7 +232,7 @@ export class LiquidityMakerEngine {
|
|||||||
|
|
||||||
// 检测成交:对比上一轮的订单ID
|
// 检测成交:对比上一轮的订单ID
|
||||||
const currentIds = new Set<string>();
|
const currentIds = new Set<string>();
|
||||||
const activeOrders: AsterOrder[] = [];
|
const activeOrders: Order[] = [];
|
||||||
|
|
||||||
if (Array.isArray(orders)) {
|
if (Array.isArray(orders)) {
|
||||||
for (const order of orders) {
|
for (const order of orders) {
|
||||||
@@ -268,7 +268,7 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.depthSnapshot = depth;
|
this.depthSnapshot = depth;
|
||||||
@@ -282,7 +282,7 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterTicker>(
|
safeSubscribe<Ticker>(
|
||||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
(ticker) => {
|
(ticker) => {
|
||||||
this.tickerSnapshot = ticker;
|
this.tickerSnapshot = ticker;
|
||||||
@@ -296,7 +296,7 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterKline[]>(
|
safeSubscribe<Kline[]>(
|
||||||
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
||||||
(klines) => {
|
(klines) => {
|
||||||
if (!Array.isArray(klines) || !klines.length) return;
|
if (!Array.isArray(klines) || !klines.length) return;
|
||||||
@@ -318,7 +318,7 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 检测成交订单 */
|
/** 检测成交订单 */
|
||||||
private detectFills(orders: AsterOrder[] | null | undefined): void {
|
private detectFills(orders: Order[] | null | undefined): void {
|
||||||
if (!Array.isArray(orders)) return;
|
if (!Array.isArray(orders)) return;
|
||||||
|
|
||||||
// 查找已成交或部分成交的订单
|
// 查找已成交或部分成交的订单
|
||||||
@@ -357,7 +357,7 @@ export class LiquidityMakerEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||||
const list = Array.isArray(orders) ? orders : [];
|
const list = Array.isArray(orders) ? orders : [];
|
||||||
Object.keys(this.pending).forEach((type) => {
|
Object.keys(this.pending).forEach((type) => {
|
||||||
const pendingId = this.pending[type];
|
const pendingId = this.pending[type];
|
||||||
@@ -759,7 +759,7 @@ export class LiquidityMakerEngine {
|
|||||||
* 更敏感的偏移判断:当一侧深度超出另一侧 depthImbalanceRatio 倍时,
|
* 更敏感的偏移判断:当一侧深度超出另一侧 depthImbalanceRatio 倍时,
|
||||||
* 取消订单簿较薄一端的订单
|
* 取消订单簿较薄一端的订单
|
||||||
*/
|
*/
|
||||||
private evaluateDepth(depth: AsterDepth): {
|
private evaluateDepth(depth: Depth): {
|
||||||
buySum: number;
|
buySum: number;
|
||||||
sellSum: number;
|
sellSum: number;
|
||||||
skipBuySide: boolean;
|
skipBuySide: boolean;
|
||||||
@@ -1186,7 +1186,7 @@ export class LiquidityMakerEngine {
|
|||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
||||||
const assets = snapshot?.assets ?? [];
|
const assets = snapshot?.assets ?? [];
|
||||||
if (!assets.length) return null;
|
if (!assets.length) return null;
|
||||||
const parsed = parseSymbolParts(this.config.symbol);
|
const parsed = parseSymbolParts(this.config.symbol);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { MakerConfig } from "../config";
|
import type { MakerConfig } from "../config";
|
||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { formatPriceToString } from "../utils/math";
|
import { formatPriceToString } from "../utils/math";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
@@ -47,7 +47,7 @@ export interface MakerEngineSnapshot {
|
|||||||
pnl: number;
|
pnl: number;
|
||||||
accountUnrealized: number;
|
accountUnrealized: number;
|
||||||
sessionVolume: number;
|
sessionVolume: number;
|
||||||
openOrders: AsterOrder[];
|
openOrders: Order[];
|
||||||
desiredOrders: DesiredOrder[];
|
desiredOrders: DesiredOrder[];
|
||||||
tradeLog: TradeLogEntry[];
|
tradeLog: TradeLogEntry[];
|
||||||
lastUpdated: number | null;
|
lastUpdated: number | null;
|
||||||
@@ -66,10 +66,10 @@ const EPS = 1e-5;
|
|||||||
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
|
const INSUFFICIENT_BALANCE_COOLDOWN_MS = 15_000;
|
||||||
|
|
||||||
export class MakerEngine {
|
export class MakerEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private depthSnapshot: AsterDepth | null = null;
|
private depthSnapshot: Depth | null = null;
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
|
|
||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
@@ -154,7 +154,7 @@ export class MakerEngine {
|
|||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.accountSnapshot = snapshot;
|
this.accountSnapshot = snapshot;
|
||||||
@@ -178,7 +178,7 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterOrder[]>(
|
safeSubscribe<Order[]>(
|
||||||
this.exchange.watchOrders.bind(this.exchange),
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
(orders) => {
|
(orders) => {
|
||||||
this.syncLocksWithOrders(orders);
|
this.syncLocksWithOrders(orders);
|
||||||
@@ -211,7 +211,7 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.depthSnapshot = depth;
|
this.depthSnapshot = depth;
|
||||||
@@ -229,7 +229,7 @@ export class MakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterTicker>(
|
safeSubscribe<Ticker>(
|
||||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
(ticker) => {
|
(ticker) => {
|
||||||
this.tickerSnapshot = ticker;
|
this.tickerSnapshot = ticker;
|
||||||
@@ -250,7 +250,7 @@ export class MakerEngine {
|
|||||||
// Maker strategy does not require realtime klines.
|
// Maker strategy does not require realtime klines.
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||||
const list = Array.isArray(orders) ? orders : [];
|
const list = Array.isArray(orders) ? orders : [];
|
||||||
Object.keys(this.pending).forEach((type) => {
|
Object.keys(this.pending).forEach((type) => {
|
||||||
const pendingId = this.pending[type];
|
const pendingId = this.pending[type];
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { MakerPointsConfig } from "../config";
|
import type { MakerPointsConfig } from "../config";
|
||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { formatPriceToString } from "../utils/math";
|
import { formatPriceToString } from "../utils/math";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
@@ -59,7 +59,7 @@ export interface MakerPointsSnapshot {
|
|||||||
pnl: number;
|
pnl: number;
|
||||||
accountUnrealized: number;
|
accountUnrealized: number;
|
||||||
sessionVolume: number;
|
sessionVolume: number;
|
||||||
openOrders: AsterOrder[];
|
openOrders: Order[];
|
||||||
desiredOrders: DesiredOrder[];
|
desiredOrders: DesiredOrder[];
|
||||||
tradeLog: TradeLogEntry[];
|
tradeLog: TradeLogEntry[];
|
||||||
lastUpdated: number | null;
|
lastUpdated: number | null;
|
||||||
@@ -102,10 +102,10 @@ const STANDX_MARGIN_MODE_MAX_ATTEMPTS = 10;
|
|||||||
const ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS = 5_000;
|
const ACCOUNT_STALE_REST_PROBE_MIN_INTERVAL_MS = 5_000;
|
||||||
|
|
||||||
export class MakerPointsEngine {
|
export class MakerPointsEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private depthSnapshot: AsterDepth | null = null;
|
private depthSnapshot: Depth | null = null;
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
|
|
||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
@@ -298,7 +298,7 @@ export class MakerPointsEngine {
|
|||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
this.setupRestHealthProtection();
|
this.setupRestHealthProtection();
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.applyAccountSnapshot(snapshot);
|
this.applyAccountSnapshot(snapshot);
|
||||||
@@ -310,7 +310,7 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterOrder[]>(
|
safeSubscribe<Order[]>(
|
||||||
this.exchange.watchOrders.bind(this.exchange),
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
(orders) => {
|
(orders) => {
|
||||||
this.syncLocksWithOrders(orders);
|
this.syncLocksWithOrders(orders);
|
||||||
@@ -339,7 +339,7 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.depthSnapshot = depth;
|
this.depthSnapshot = depth;
|
||||||
@@ -358,7 +358,7 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterTicker>(
|
safeSubscribe<Ticker>(
|
||||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
(ticker) => {
|
(ticker) => {
|
||||||
this.tickerSnapshot = ticker;
|
this.tickerSnapshot = ticker;
|
||||||
@@ -376,7 +376,7 @@ export class MakerPointsEngine {
|
|||||||
this.setupConnectionProtection();
|
this.setupConnectionProtection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void {
|
private applyAccountSnapshot(snapshot: AccountSnapshot): void {
|
||||||
this.accountSnapshot = snapshot;
|
this.accountSnapshot = snapshot;
|
||||||
// StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime
|
// StandX: WS 推送使用本地接收时间戳;REST 快照使用响应里的 time 字段映射到 snapshot.updateTime
|
||||||
this.lastStandxAccountTime =
|
this.lastStandxAccountTime =
|
||||||
@@ -545,7 +545,7 @@ export class MakerPointsEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||||
const list = Array.isArray(orders) ? orders : [];
|
const list = Array.isArray(orders) ? orders : [];
|
||||||
Object.keys(this.pending).forEach((type) => {
|
Object.keys(this.pending).forEach((type) => {
|
||||||
const pendingId = this.pending[type];
|
const pendingId = this.pending[type];
|
||||||
@@ -746,7 +746,7 @@ export class MakerPointsEngine {
|
|||||||
ask1: number;
|
ask1: number;
|
||||||
skipBuy: boolean;
|
skipBuy: boolean;
|
||||||
skipSell: boolean;
|
skipSell: boolean;
|
||||||
depth: AsterDepth | null;
|
depth: Depth | null;
|
||||||
}): DesiredOrder[] {
|
}): DesiredOrder[] {
|
||||||
const { bid1, ask1, skipBuy, skipSell, depth } = params;
|
const { bid1, ask1, skipBuy, skipSell, depth } = params;
|
||||||
|
|
||||||
@@ -837,7 +837,7 @@ export class MakerPointsEngine {
|
|||||||
* 当深度从足够变为不足,或从不足变为足够时,需要触发重新计算
|
* 当深度从足够变为不足,或从不足变为足够时,需要触发重新计算
|
||||||
*/
|
*/
|
||||||
private checkDepthStatusChanged(
|
private checkDepthStatusChanged(
|
||||||
depth: AsterDepth | null,
|
depth: Depth | null,
|
||||||
bid1: number,
|
bid1: number,
|
||||||
ask1: number
|
ask1: number
|
||||||
): boolean {
|
): boolean {
|
||||||
@@ -879,7 +879,7 @@ export class MakerPointsEngine {
|
|||||||
/**
|
/**
|
||||||
* 当深度从“满足阈值”切换到“不满足阈值”时,立即触发一次主循环,优先撤销不再安全的挂单。
|
* 当深度从“满足阈值”切换到“不满足阈值”时,立即触发一次主循环,优先撤销不再安全的挂单。
|
||||||
*/
|
*/
|
||||||
private shouldTriggerImmediateDepthProtection(depth: AsterDepth | null): boolean {
|
private shouldTriggerImmediateDepthProtection(depth: Depth | null): boolean {
|
||||||
if (!depth) return false;
|
if (!depth) return false;
|
||||||
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
||||||
|
|
||||||
@@ -917,7 +917,7 @@ export class MakerPointsEngine {
|
|||||||
/**
|
/**
|
||||||
* 当盘口相对上次报价偏移超过 minRepriceBps 时,立即触发一次主循环,优先撤销旧报价。
|
* 当盘口相对上次报价偏移超过 minRepriceBps 时,立即触发一次主循环,优先撤销旧报价。
|
||||||
*/
|
*/
|
||||||
private shouldTriggerImmediateReprice(depth: AsterDepth | null): boolean {
|
private shouldTriggerImmediateReprice(depth: Depth | null): boolean {
|
||||||
if (!depth) return false;
|
if (!depth) return false;
|
||||||
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
if (this.defenseMode || this.reconnectResetPending || this.stopLossProcessing) return false;
|
||||||
|
|
||||||
@@ -1993,7 +1993,7 @@ export class MakerPointsEngine {
|
|||||||
void poll();
|
void poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getStandxMarginMode(snapshot: AsterAccountSnapshot | null): string | null {
|
private getStandxMarginMode(snapshot: AccountSnapshot | null): string | null {
|
||||||
if (this.exchange.id !== "standx") return null;
|
if (this.exchange.id !== "standx") return null;
|
||||||
const positions = snapshot?.positions ?? [];
|
const positions = snapshot?.positions ?? [];
|
||||||
const match = positions.find((pos) => pos.symbol === this.config.symbol);
|
const match = positions.find((pos) => pos.symbol === this.config.symbol);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { MakerConfig } from "../config";
|
import type { MakerConfig } from "../config";
|
||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import { formatPriceToString } from "../utils/math";
|
import { formatPriceToString } from "../utils/math";
|
||||||
import { createTradeLog } from "../logging/trade-log";
|
import { createTradeLog } from "../logging/trade-log";
|
||||||
@@ -56,12 +56,12 @@ type MakerListener = (snapshot: OffsetMakerEngineSnapshot) => void;
|
|||||||
const EPS = 1e-5;
|
const EPS = 1e-5;
|
||||||
|
|
||||||
export class OffsetMakerEngine {
|
export class OffsetMakerEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private depthSnapshot: AsterDepth | null = null;
|
private depthSnapshot: Depth | null = null;
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
private lastKline: AsterKline | null = null;
|
private lastKline: Kline | null = null;
|
||||||
private liveCandle: { startMs: number; open: number; close: number } | null = null;
|
private liveCandle: { startMs: number; open: number; close: number } | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
|
|
||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
@@ -163,7 +163,7 @@ export class OffsetMakerEngine {
|
|||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.accountSnapshot = snapshot;
|
this.accountSnapshot = snapshot;
|
||||||
@@ -207,7 +207,7 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterOrder[]>(
|
safeSubscribe<Order[]>(
|
||||||
this.exchange.watchOrders.bind(this.exchange),
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
(orders) => {
|
(orders) => {
|
||||||
this.syncLocksWithOrders(orders);
|
this.syncLocksWithOrders(orders);
|
||||||
@@ -236,7 +236,7 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.depthSnapshot = depth;
|
this.depthSnapshot = depth;
|
||||||
@@ -250,7 +250,7 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterTicker>(
|
safeSubscribe<Ticker>(
|
||||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
(ticker) => {
|
(ticker) => {
|
||||||
this.tickerSnapshot = ticker;
|
this.tickerSnapshot = ticker;
|
||||||
@@ -264,7 +264,7 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterKline[]>(
|
safeSubscribe<Kline[]>(
|
||||||
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, "1m"),
|
||||||
(klines) => {
|
(klines) => {
|
||||||
if (!Array.isArray(klines) || !klines.length) return;
|
if (!Array.isArray(klines) || !klines.length) return;
|
||||||
@@ -284,7 +284,7 @@ export class OffsetMakerEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncLocksWithOrders(orders: AsterOrder[] | null | undefined): void {
|
private syncLocksWithOrders(orders: Order[] | null | undefined): void {
|
||||||
const list = Array.isArray(orders) ? orders : [];
|
const list = Array.isArray(orders) ? orders : [];
|
||||||
Object.keys(this.pending).forEach((type) => {
|
Object.keys(this.pending).forEach((type) => {
|
||||||
const pendingId = this.pending[type];
|
const pendingId = this.pending[type];
|
||||||
@@ -600,7 +600,7 @@ export class OffsetMakerEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private evaluateDepth(depth: AsterDepth): {
|
private evaluateDepth(depth: Depth): {
|
||||||
buySum: number;
|
buySum: number;
|
||||||
sellSum: number;
|
sellSum: number;
|
||||||
skipBuySide: boolean;
|
skipBuySide: boolean;
|
||||||
@@ -1038,7 +1038,7 @@ export class OffsetMakerEngine {
|
|||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSpotBalances(snapshot: AsterAccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
private getSpotBalances(snapshot: AccountSnapshot | null = this.accountSnapshot): { baseAvailable: number; quoteAvailable: number; baseWallet: number } | null {
|
||||||
const assets = snapshot?.assets ?? [];
|
const assets = snapshot?.assets ?? [];
|
||||||
if (!assets.length) return null;
|
if (!assets.length) return null;
|
||||||
const parsed = parseSymbolParts(this.config.symbol);
|
const parsed = parseSymbolParts(this.config.symbol);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterOrder, AsterTicker } from "../exchanges/types";
|
import type { AccountSnapshot, Depth, Order, Ticker } from "../exchanges/types";
|
||||||
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
import { createTradeLog, type TradeLogEntry } from "../logging/trade-log";
|
||||||
import { marketClose, placeMarketOrder, placeStopLossOrder, unlockOperating } from "../core/order-coordinator";
|
import { marketClose, placeMarketOrder, placeStopLossOrder, unlockOperating } from "../core/order-coordinator";
|
||||||
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
import type { OrderLockMap, OrderPendingMap, OrderTimerMap } from "../core/order-coordinator";
|
||||||
@@ -55,9 +55,9 @@ export interface SwingEngineSnapshot {
|
|||||||
stopLossTarget: number | null;
|
stopLossTarget: number | null;
|
||||||
stopLossKillSwitch: boolean;
|
stopLossKillSwitch: boolean;
|
||||||
|
|
||||||
openOrders: AsterOrder[];
|
openOrders: Order[];
|
||||||
depth: AsterDepth | null;
|
depth: Depth | null;
|
||||||
ticker: AsterTicker | null;
|
ticker: Ticker | null;
|
||||||
|
|
||||||
tradeLog: TradeLogEntry[];
|
tradeLog: TradeLogEntry[];
|
||||||
lastUpdated: number | null;
|
lastUpdated: number | null;
|
||||||
@@ -70,10 +70,10 @@ type SwingListener = (snapshot: SwingEngineSnapshot) => void;
|
|||||||
const EPS = 1e-5;
|
const EPS = 1e-5;
|
||||||
|
|
||||||
export class SwingEngine {
|
export class SwingEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
private depthSnapshot: AsterDepth | null = null;
|
private depthSnapshot: Depth | null = null;
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
|
|
||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
@@ -163,7 +163,7 @@ export class SwingEngine {
|
|||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.accountSnapshot = snapshot;
|
this.accountSnapshot = snapshot;
|
||||||
@@ -192,7 +192,7 @@ export class SwingEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterOrder[]>(
|
safeSubscribe<Order[]>(
|
||||||
this.exchange.watchOrders.bind(this.exchange),
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
(orders) => {
|
(orders) => {
|
||||||
this.synchronizeLocks(orders);
|
this.synchronizeLocks(orders);
|
||||||
@@ -214,7 +214,7 @@ export class SwingEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.depthSnapshot = depth;
|
this.depthSnapshot = depth;
|
||||||
@@ -227,7 +227,7 @@ export class SwingEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterTicker>(
|
safeSubscribe<Ticker>(
|
||||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
(ticker) => {
|
(ticker) => {
|
||||||
this.tickerSnapshot = ticker;
|
this.tickerSnapshot = ticker;
|
||||||
@@ -241,7 +241,7 @@ export class SwingEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
private synchronizeLocks(orders: Order[] | null | undefined): void {
|
||||||
const list = Array.isArray(orders) ? orders : [];
|
const list = Array.isArray(orders) ? orders : [];
|
||||||
Object.keys(this.pending).forEach((type) => {
|
Object.keys(this.pending).forEach((type) => {
|
||||||
const pendingId = this.pending[type];
|
const pendingId = this.pending[type];
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import crypto from "crypto";
|
|||||||
import type { TradingConfig } from "../config";
|
import type { TradingConfig } from "../config";
|
||||||
import type { ExchangeAdapter } from "../exchanges/adapter";
|
import type { ExchangeAdapter } from "../exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
} from "../exchanges/types";
|
} from "../exchanges/types";
|
||||||
import {
|
import {
|
||||||
calcStopLossPrice,
|
calcStopLossPrice,
|
||||||
@@ -51,9 +51,9 @@ export interface TrendEngineSnapshot {
|
|||||||
totalTrades: number;
|
totalTrades: number;
|
||||||
sessionVolume: number;
|
sessionVolume: number;
|
||||||
tradeLog: TradeLogEntry[];
|
tradeLog: TradeLogEntry[];
|
||||||
openOrders: AsterOrder[];
|
openOrders: Order[];
|
||||||
depth: AsterDepth | null;
|
depth: Depth | null;
|
||||||
ticker: AsterTicker | null;
|
ticker: Ticker | null;
|
||||||
lastUpdated: number | null;
|
lastUpdated: number | null;
|
||||||
lastOpenSignal: OpenOrderPlan;
|
lastOpenSignal: OpenOrderPlan;
|
||||||
}
|
}
|
||||||
@@ -68,11 +68,11 @@ type TrendEngineEvent = "update";
|
|||||||
type TrendEngineListener = (snapshot: TrendEngineSnapshot) => void;
|
type TrendEngineListener = (snapshot: TrendEngineSnapshot) => void;
|
||||||
|
|
||||||
export class TrendEngine {
|
export class TrendEngine {
|
||||||
private accountSnapshot: AsterAccountSnapshot | null = null;
|
private accountSnapshot: AccountSnapshot | null = null;
|
||||||
private openOrders: AsterOrder[] = [];
|
private openOrders: Order[] = [];
|
||||||
private depthSnapshot: AsterDepth | null = null;
|
private depthSnapshot: Depth | null = null;
|
||||||
private tickerSnapshot: AsterTicker | null = null;
|
private tickerSnapshot: Ticker | null = null;
|
||||||
private klineSnapshot: AsterKline[] = [];
|
private klineSnapshot: Kline[] = [];
|
||||||
|
|
||||||
private readonly locks: OrderLockMap = {};
|
private readonly locks: OrderLockMap = {};
|
||||||
private readonly timers: OrderTimerMap = {};
|
private readonly timers: OrderTimerMap = {};
|
||||||
@@ -164,7 +164,7 @@ export class TrendEngine {
|
|||||||
private bootstrap(): void {
|
private bootstrap(): void {
|
||||||
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
const log: LogHandler = (type, detail) => this.tradeLog.push(type, detail);
|
||||||
|
|
||||||
safeSubscribe<AsterAccountSnapshot>(
|
safeSubscribe<AccountSnapshot>(
|
||||||
this.exchange.watchAccount.bind(this.exchange),
|
this.exchange.watchAccount.bind(this.exchange),
|
||||||
(snapshot) => {
|
(snapshot) => {
|
||||||
this.accountSnapshot = snapshot;
|
this.accountSnapshot = snapshot;
|
||||||
@@ -181,7 +181,7 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterOrder[]>(
|
safeSubscribe<Order[]>(
|
||||||
this.exchange.watchOrders.bind(this.exchange),
|
this.exchange.watchOrders.bind(this.exchange),
|
||||||
(orders) => {
|
(orders) => {
|
||||||
this.synchronizeLocks(orders);
|
this.synchronizeLocks(orders);
|
||||||
@@ -215,7 +215,7 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterDepth>(
|
safeSubscribe<Depth>(
|
||||||
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
this.exchange.watchDepth.bind(this.exchange, this.config.symbol),
|
||||||
(depth) => {
|
(depth) => {
|
||||||
this.depthSnapshot = depth;
|
this.depthSnapshot = depth;
|
||||||
@@ -228,7 +228,7 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterTicker>(
|
safeSubscribe<Ticker>(
|
||||||
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
this.exchange.watchTicker.bind(this.exchange, this.config.symbol),
|
||||||
(ticker) => {
|
(ticker) => {
|
||||||
this.tickerSnapshot = ticker;
|
this.tickerSnapshot = ticker;
|
||||||
@@ -241,7 +241,7 @@ export class TrendEngine {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
safeSubscribe<AsterKline[]>(
|
safeSubscribe<Kline[]>(
|
||||||
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, this.config.klineInterval),
|
this.exchange.watchKlines.bind(this.exchange, this.config.symbol, this.config.klineInterval),
|
||||||
(klines) => {
|
(klines) => {
|
||||||
this.klineSnapshot = Array.isArray(klines) ? klines : [];
|
this.klineSnapshot = Array.isArray(klines) ? klines : [];
|
||||||
@@ -258,7 +258,7 @@ export class TrendEngine {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronizeLocks(orders: AsterOrder[] | null | undefined): void {
|
private synchronizeLocks(orders: Order[] | null | undefined): void {
|
||||||
const list = Array.isArray(orders) ? orders : [];
|
const list = Array.isArray(orders) ? orders : [];
|
||||||
Object.keys(this.pending).forEach((type) => {
|
Object.keys(this.pending).forEach((type) => {
|
||||||
const pendingId = this.pending[type];
|
const pendingId = this.pending[type];
|
||||||
@@ -834,7 +834,7 @@ export class TrendEngine {
|
|||||||
|
|
||||||
private async tryReplaceStop(
|
private async tryReplaceStop(
|
||||||
side: "BUY" | "SELL",
|
side: "BUY" | "SELL",
|
||||||
currentOrder: AsterOrder,
|
currentOrder: Order,
|
||||||
nextStopPrice: number,
|
nextStopPrice: number,
|
||||||
lastPrice: number
|
lastPrice: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import type { AsterDepth } from "../exchanges/types";
|
import type { Depth } from "../exchanges/types";
|
||||||
|
|
||||||
export type DepthImbalance = "balanced" | "buy_dominant" | "sell_dominant";
|
export type DepthImbalance = "balanced" | "buy_dominant" | "sell_dominant";
|
||||||
|
|
||||||
export function computeDepthStats(
|
export function computeDepthStats(
|
||||||
depth: AsterDepth,
|
depth: Depth,
|
||||||
levels = 10,
|
levels = 10,
|
||||||
ratio = 3
|
ratio = 3
|
||||||
): {
|
): {
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
import type { AsterDepth, AsterTicker } from "../exchanges/types";
|
import type { Depth, Ticker } from "../exchanges/types";
|
||||||
|
|
||||||
export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null; topAsk: number | null } {
|
export function getTopPrices(depth?: Depth | null): { topBid: number | null; topAsk: number | null } {
|
||||||
const bid = Number(depth?.bids?.[0]?.[0]);
|
const bid = Number(depth?.bids?.[0]?.[0]);
|
||||||
const ask = Number(depth?.asks?.[0]?.[0]);
|
const ask = Number(depth?.asks?.[0]?.[0]);
|
||||||
return {
|
return {
|
||||||
@@ -16,7 +16,7 @@ export function getTopPrices(depth?: AsterDepth | null): { topBid: number | null
|
|||||||
* @returns 指定档位的买卖价格,如果该档位不存在则回退到最近的有效档位
|
* @returns 指定档位的买卖价格,如果该档位不存在则回退到最近的有效档位
|
||||||
*/
|
*/
|
||||||
export function getPricesAtLevel(
|
export function getPricesAtLevel(
|
||||||
depth?: AsterDepth | null,
|
depth?: Depth | null,
|
||||||
level: number = 1
|
level: number = 1
|
||||||
): { bidAtLevel: number | null; askAtLevel: number | null } {
|
): { bidAtLevel: number | null; askAtLevel: number | null } {
|
||||||
const index = Math.max(0, level - 1);
|
const index = Math.max(0, level - 1);
|
||||||
@@ -49,7 +49,7 @@ export function getPricesAtLevel(
|
|||||||
return { bidAtLevel, askAtLevel };
|
return { bidAtLevel, askAtLevel };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | null): number | null {
|
export function getMidOrLast(depth?: Depth | null, ticker?: Ticker | null): number | null {
|
||||||
const { topBid, topAsk } = getTopPrices(depth);
|
const { topBid, topAsk } = getTopPrices(depth);
|
||||||
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
|
if (topBid != null && topAsk != null) return (topBid + topAsk) / 2;
|
||||||
const last = Number(ticker?.lastPrice);
|
const last = Number(ticker?.lastPrice);
|
||||||
@@ -64,7 +64,7 @@ export function getMidOrLast(depth?: AsterDepth | null, ticker?: AsterTicker | n
|
|||||||
* @returns 从一档到目标价格之间的挂单总量 (不包含目标价格本身)
|
* @returns 从一档到目标价格之间的挂单总量 (不包含目标价格本身)
|
||||||
*/
|
*/
|
||||||
export function getDepthBetweenPrices(
|
export function getDepthBetweenPrices(
|
||||||
depth: AsterDepth | null | undefined,
|
depth: Depth | null | undefined,
|
||||||
side: "BUY" | "SELL",
|
side: "BUY" | "SELL",
|
||||||
targetPrice: number
|
targetPrice: number
|
||||||
): number {
|
): number {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AsterAccountAsset, AsterAccountSnapshot, AsterKline } from "../exchanges/types";
|
import type { AccountAsset, AccountSnapshot, Kline } from "../exchanges/types";
|
||||||
|
|
||||||
export interface PositionSnapshot {
|
export interface PositionSnapshot {
|
||||||
positionAmt: number;
|
positionAmt: number;
|
||||||
@@ -7,7 +7,7 @@ export interface PositionSnapshot {
|
|||||||
markPrice: number | null;
|
markPrice: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPosition(snapshot: AsterAccountSnapshot | null, symbol: string): PositionSnapshot {
|
export function getPosition(snapshot: AccountSnapshot | null, symbol: string): PositionSnapshot {
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
return { positionAmt: 0, entryPrice: 0, unrealizedProfit: 0, markPrice: null };
|
return { positionAmt: 0, entryPrice: 0, unrealizedProfit: 0, markPrice: null };
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ export function getPosition(snapshot: AsterAccountSnapshot | null, symbol: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function validateAccountSnapshotForSymbol(
|
export function validateAccountSnapshotForSymbol(
|
||||||
snapshot: AsterAccountSnapshot | null,
|
snapshot: AccountSnapshot | null,
|
||||||
symbol: string
|
symbol: string
|
||||||
): { ok: true } | { ok: false; issues: string[] } {
|
): { ok: true } | { ok: false; issues: string[] } {
|
||||||
if (!snapshot) return { ok: true };
|
if (!snapshot) return { ok: true };
|
||||||
@@ -88,7 +88,7 @@ export function validateAccountSnapshotForSymbol(
|
|||||||
return { ok: false, issues: Array.from(new Set(issues)) };
|
return { ok: false, issues: Array.from(new Set(issues)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSMA(values: AsterKline[], length: number): number | null {
|
export function getSMA(values: Kline[], length: number): number | null {
|
||||||
if (!Array.isArray(values) || values.length < length) return null;
|
if (!Array.isArray(values) || values.length < length) return null;
|
||||||
const window = values.slice(-length);
|
const window = values.slice(-length);
|
||||||
const closes = window.map((kline) => Number(kline.close));
|
const closes = window.map((kline) => Number(kline.close));
|
||||||
@@ -118,7 +118,7 @@ export function calcTrailingActivationPrice(entryPrice: number, qty: number, sid
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function computeBollingerBandwidth(
|
export function computeBollingerBandwidth(
|
||||||
values: AsterKline[],
|
values: Kline[],
|
||||||
length: number,
|
length: number,
|
||||||
stdMultiplier: number
|
stdMultiplier: number
|
||||||
): number | null {
|
): number | null {
|
||||||
@@ -190,7 +190,7 @@ function normalizeBaseSymbol(symbol: string | undefined): string | undefined {
|
|||||||
return parseSymbolParts(symbol).base;
|
return parseSymbolParts(symbol).base;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAsset(assets: AsterAccountAsset[], baseSymbol: string, baseAssetId?: number): AsterAccountAsset | undefined {
|
function selectAsset(assets: AccountAsset[], baseSymbol: string, baseAssetId?: number): AccountAsset | undefined {
|
||||||
const normalized = baseSymbol.toUpperCase();
|
const normalized = baseSymbol.toUpperCase();
|
||||||
const targetId = Number(baseAssetId);
|
const targetId = Number(baseAssetId);
|
||||||
return assets.find((asset) => {
|
return assets.find((asset) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { AsterAccountSnapshot } from "../src/exchanges/types";
|
import type { AccountSnapshot } from "../src/exchanges/types";
|
||||||
import { validateAccountSnapshotForSymbol } from "../src/utils/strategy";
|
import { validateAccountSnapshotForSymbol } from "../src/utils/strategy";
|
||||||
|
|
||||||
function baseSnapshot(positions: AsterAccountSnapshot["positions"]): AsterAccountSnapshot {
|
function baseSnapshot(positions: AccountSnapshot["positions"]): AccountSnapshot {
|
||||||
return {
|
return {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { AsterSpotRestClient } from "../src/exchanges/aster/client";
|
import { AsterSpotRestClient } from "../src/exchanges/aster/gateway";
|
||||||
|
|
||||||
describe("AsterSpotRestClient", () => {
|
describe("AsterSpotRestClient", () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
} from "../src/exchanges/types";
|
} from "../src/exchanges/types";
|
||||||
import { BasisArbEngine } from "../src/strategy/basis-arb-engine";
|
import { BasisArbEngine } from "../src/strategy/basis-arb-engine";
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
class StubAdapter implements ExchangeAdapter {
|
||||||
id = "aster";
|
id = "aster";
|
||||||
private depthHandler: ((depth: AsterDepth) => void) | null = null;
|
private depthHandler: ((depth: Depth) => void) | null = null;
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {
|
||||||
// not required for this test
|
// not required for this test
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {
|
watchOrders(_cb: (orders: Order[]) => void): void {
|
||||||
// not required for this test
|
// not required for this test
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||||
this.depthHandler = cb;
|
this.depthHandler = cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
emitDepth(depth: AsterDepth): void {
|
emitDepth(depth: Depth): void {
|
||||||
this.depthHandler?.(depth);
|
this.depthHandler?.(depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {
|
||||||
// not required for this test
|
// not required for this test
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {
|
||||||
// not required for this test
|
// not required for this test
|
||||||
}
|
}
|
||||||
|
|
||||||
createOrder(): Promise<AsterOrder> {
|
createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
const ORIGINAL_FETCH = globalThis.fetch;
|
const ORIGINAL_FETCH = globalThis.fetch;
|
||||||
@@ -12,13 +12,13 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
async createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
async cancelOrders(): Promise<void> {}
|
async cancelOrders(): Promise<void> {}
|
||||||
async cancelAllOrders(): Promise<void> {}
|
async cancelAllOrders(): Promise<void> {}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
return {
|
return {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { executeCliCommand } from "../src/cli/command-executor";
|
|||||||
import type { ParsedCliCommand } from "../src/cli/command-types";
|
import type { ParsedCliCommand } from "../src/cli/command-types";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
} from "../src/exchanges/types";
|
} from "../src/exchanges/types";
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class FakeAdapter implements ExchangeAdapter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(cb: (snapshot: AsterAccountSnapshot) => void): void {
|
watchAccount(cb: (snapshot: AccountSnapshot) => void): void {
|
||||||
cb({
|
cb({
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
@@ -35,11 +35,11 @@ class FakeAdapter implements ExchangeAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: (orders: AsterOrder[]) => void): void {
|
watchOrders(cb: (orders: Order[]) => void): void {
|
||||||
cb([]);
|
cb([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||||
cb({
|
cb({
|
||||||
lastUpdateId: 1,
|
lastUpdateId: 1,
|
||||||
bids: [["100", "1"]],
|
bids: [["100", "1"]],
|
||||||
@@ -47,7 +47,7 @@ class FakeAdapter implements ExchangeAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(symbol: string, cb: (ticker: AsterTicker) => void): void {
|
watchTicker(symbol: string, cb: (ticker: Ticker) => void): void {
|
||||||
cb({
|
cb({
|
||||||
symbol,
|
symbol,
|
||||||
lastPrice: "100",
|
lastPrice: "100",
|
||||||
@@ -57,10 +57,10 @@ class FakeAdapter implements ExchangeAdapter {
|
|||||||
volume: "10",
|
volume: "10",
|
||||||
quoteVolume: "1000",
|
quoteVolume: "1000",
|
||||||
eventTime: Date.now(),
|
eventTime: Date.now(),
|
||||||
} as AsterTicker);
|
} as Ticker);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchKlines(_symbol: string, _interval: string, cb: (klines: AsterKline[]) => void): void {
|
watchKlines(_symbol: string, _interval: string, cb: (klines: Kline[]) => void): void {
|
||||||
cb([
|
cb([
|
||||||
{
|
{
|
||||||
openTime: 1,
|
openTime: 1,
|
||||||
@@ -75,7 +75,7 @@ class FakeAdapter implements ExchangeAdapter {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
this.createOrderCalls += 1;
|
this.createOrderCalls += 1;
|
||||||
return {
|
return {
|
||||||
orderId: "1",
|
orderId: "1",
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { DryRunExchangeAdapter } from "../src/exchanges/dry-run-adapter";
|
import { DryRunExchangeAdapter } from "../src/exchanges/dry-run-adapter";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
} from "../src/exchanges/types";
|
} from "../src/exchanges/types";
|
||||||
|
|
||||||
@@ -18,13 +18,13 @@ class BaseAdapter implements ExchangeAdapter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
async createOrder(_params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(_params: CreateOrderParams): Promise<Order> {
|
||||||
this.createCalls += 1;
|
this.createCalls += 1;
|
||||||
throw new Error("should not be called in dry-run");
|
throw new Error("should not be called in dry-run");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import {
|
|||||||
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
|
import { buildAdapterFromEnv } from "../src/exchanges/resolve-from-env";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterKline,
|
Kline,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
} from "../src/exchanges/types";
|
} from "../src/exchanges/types";
|
||||||
|
|
||||||
@@ -78,17 +78,17 @@ class RecorderAdapter implements ExchangeAdapter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
|
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
|
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||||
|
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
|
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
this.lastCreateOrderParams = params;
|
this.lastCreateOrderParams = params;
|
||||||
return {
|
return {
|
||||||
orderId: 1,
|
orderId: 1,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
||||||
import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter";
|
import { createExchangeAdapter, resolveExchangeId } from "../src/exchanges/create-adapter";
|
||||||
import { AsterExchangeAdapter } from "../src/exchanges/aster-adapter";
|
import { AsterExchangeAdapter } from "../src/exchanges/aster/adapter";
|
||||||
import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
|
import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
|
||||||
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
|
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
|
||||||
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
|
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
|
||||||
|
|||||||
+383
-36
@@ -1,23 +1,25 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type {
|
import type {
|
||||||
AsterAccountSnapshot,
|
AccountSnapshot,
|
||||||
AsterDepth,
|
Depth,
|
||||||
AsterOrder,
|
Order,
|
||||||
AsterTicker,
|
Ticker,
|
||||||
CreateOrderParams,
|
CreateOrderParams,
|
||||||
} from "../src/exchanges/types";
|
} from "../src/exchanges/types";
|
||||||
import type { GridConfig } from "../src/config";
|
import type { GridConfig } from "../src/config";
|
||||||
import { GridEngine } from "../src/strategy/grid-engine";
|
import { GridEngine } from "../src/strategy/grid-engine";
|
||||||
|
|
||||||
|
let orderCounter = 0;
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
class StubAdapter implements ExchangeAdapter {
|
||||||
id = "aster";
|
id = "aster";
|
||||||
|
|
||||||
private accountHandler: ((snapshot: AsterAccountSnapshot) => void) | null = null;
|
private accountHandler: ((snapshot: AccountSnapshot) => void) | null = null;
|
||||||
private orderHandler: ((orders: AsterOrder[]) => void) | null = null;
|
private orderHandler: ((orders: Order[]) => void) | null = null;
|
||||||
private depthHandler: ((depth: AsterDepth) => void) | null = null;
|
private depthHandler: ((depth: Depth) => void) | null = null;
|
||||||
private tickerHandler: ((ticker: AsterTicker) => void) | null = null;
|
private tickerHandler: ((ticker: Ticker) => void) | null = null;
|
||||||
private currentOrders: AsterOrder[] = [];
|
private currentOrders: Order[] = [];
|
||||||
|
|
||||||
public createdOrders: CreateOrderParams[] = [];
|
public createdOrders: CreateOrderParams[] = [];
|
||||||
public marketOrders: CreateOrderParams[] = [];
|
public marketOrders: CreateOrderParams[] = [];
|
||||||
@@ -28,19 +30,19 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(cb: (snapshot: AsterAccountSnapshot) => void): void {
|
watchAccount(cb: (snapshot: AccountSnapshot) => void): void {
|
||||||
this.accountHandler = cb;
|
this.accountHandler = cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchOrders(cb: (orders: AsterOrder[]) => void): void {
|
watchOrders(cb: (orders: Order[]) => void): void {
|
||||||
this.orderHandler = cb;
|
this.orderHandler = cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||||
this.depthHandler = cb;
|
this.depthHandler = cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTicker(_symbol: string, cb: (ticker: AsterTicker) => void): void {
|
watchTicker(_symbol: string, cb: (ticker: Ticker) => void): void {
|
||||||
this.tickerHandler = cb;
|
this.tickerHandler = cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,26 +50,28 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
// not used in tests
|
// not used in tests
|
||||||
}
|
}
|
||||||
|
|
||||||
emitAccount(snapshot: AsterAccountSnapshot): void {
|
emitAccount(snapshot: AccountSnapshot): void {
|
||||||
this.accountHandler?.(snapshot);
|
this.accountHandler?.(snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitOrders(orders: AsterOrder[]): void {
|
emitOrders(orders: Order[]): void {
|
||||||
this.orderHandler?.(orders);
|
this.orderHandler?.(orders);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitDepth(depth: AsterDepth): void {
|
emitDepth(depth: Depth): void {
|
||||||
this.depthHandler?.(depth);
|
this.depthHandler?.(depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitTicker(ticker: AsterTicker): void {
|
emitTicker(ticker: Ticker): void {
|
||||||
this.tickerHandler?.(ticker);
|
this.tickerHandler?.(ticker);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||||
const order: AsterOrder = {
|
orderCounter++;
|
||||||
orderId: `${Date.now()}-${Math.random()}`,
|
const orderId = params.clientOrderId ?? `stub-${orderCounter}`;
|
||||||
clientOrderId: "test",
|
const order: Order = {
|
||||||
|
orderId,
|
||||||
|
clientOrderId: params.clientOrderId ?? orderId,
|
||||||
symbol: params.symbol,
|
symbol: params.symbol,
|
||||||
side: params.side,
|
side: params.side,
|
||||||
type: params.type,
|
type: params.type,
|
||||||
@@ -86,18 +90,21 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
this.marketOrders.push(params);
|
this.marketOrders.push(params);
|
||||||
this.orderHandler?.([]);
|
this.orderHandler?.([]);
|
||||||
} else {
|
} else {
|
||||||
this.currentOrders = [order];
|
this.currentOrders.push(order);
|
||||||
this.orderHandler?.(this.currentOrders);
|
this.orderHandler?.([...this.currentOrders]);
|
||||||
}
|
}
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||||
this.cancelledOrders.push(params.orderId);
|
this.cancelledOrders.push(params.orderId);
|
||||||
|
this.currentOrders = this.currentOrders.filter(o => String(o.orderId) !== String(params.orderId));
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||||
this.cancelledOrders.push(...params.orderIdList);
|
this.cancelledOrders.push(...params.orderIdList);
|
||||||
|
const idSet = new Set(params.orderIdList.map(String));
|
||||||
|
this.currentOrders = this.currentOrders.filter(o => !idSet.has(String(o.orderId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAllOrders(): Promise<void> {
|
async cancelAllOrders(): Promise<void> {
|
||||||
@@ -105,9 +112,17 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
this.currentOrders = [];
|
this.currentOrders = [];
|
||||||
this.orderHandler?.([]);
|
this.orderHandler?.([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearCurrentOrders(): void {
|
||||||
|
this.currentOrders = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAccountSnapshot(symbol: string, positionAmt: number): AsterAccountSnapshot {
|
getCurrentOrders(): Order[] {
|
||||||
|
return [...this.currentOrders];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAccountSnapshot(symbol: string, positionAmt: number): AccountSnapshot {
|
||||||
return {
|
return {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
@@ -126,7 +141,7 @@ function createAccountSnapshot(symbol: string, positionAmt: number): AsterAccoun
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
assets: [],
|
assets: [],
|
||||||
} as unknown as AsterAccountSnapshot;
|
} as unknown as AccountSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("GridEngine", () => {
|
describe("GridEngine", () => {
|
||||||
@@ -151,7 +166,7 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
it("creates geometric desired orders when running in both directions", async () => {
|
it("creates geometric desired orders when running in both directions", async () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
adapter.emitOrders([]);
|
adapter.emitOrders([]);
|
||||||
@@ -181,7 +196,7 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
it("limits sell orders for long-only direction when no position is available", () => {
|
it("limits sell orders for long-only direction when no position is available", () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine({ ...baseConfig, direction: "long" }, adapter, { now: () => 0 });
|
const engine = new GridEngine({ ...baseConfig, direction: "long" }, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
adapter.emitOrders([]);
|
adapter.emitOrders([]);
|
||||||
@@ -198,7 +213,7 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
it("does not repopulate the same buy level until exposure is released", () => {
|
it("does not repopulate the same buy level until exposure is released", () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
adapter.emitOrders([]);
|
adapter.emitOrders([]);
|
||||||
@@ -223,7 +238,7 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
it("keeps level side assignments stable regardless of price", () => {
|
it("keeps level side assignments stable regardless of price", () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
adapter.emitOrders([]);
|
adapter.emitOrders([]);
|
||||||
@@ -245,7 +260,7 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
it("limits active sell orders by remaining short headroom", () => {
|
it("limits active sell orders by remaining short headroom", () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
adapter.emitOrders([]);
|
adapter.emitOrders([]);
|
||||||
@@ -255,7 +270,7 @@ describe("GridEngine", () => {
|
|||||||
expect(sellCountFull).toBeGreaterThan(0);
|
expect(sellCountFull).toBeGreaterThan(0);
|
||||||
|
|
||||||
const limitedHeadroomConfig = { ...baseConfig, maxPositionSize: baseConfig.orderSize * 2 };
|
const limitedHeadroomConfig = { ...baseConfig, maxPositionSize: baseConfig.orderSize * 2 };
|
||||||
const limitedEngine = new GridEngine(limitedHeadroomConfig, adapter as any, { now: () => 0 });
|
const limitedEngine = new GridEngine(limitedHeadroomConfig, adapter as any, { now: () => 0, skipPersistence: true });
|
||||||
(limitedEngine as any).shortExposure.set(12, baseConfig.orderSize * 2);
|
(limitedEngine as any).shortExposure.set(12, baseConfig.orderSize * 2);
|
||||||
|
|
||||||
const desiredLimited = (limitedEngine as any).computeDesiredOrders(2.1) as Array<{ level: number; side: string }>;
|
const desiredLimited = (limitedEngine as any).computeDesiredOrders(2.1) as Array<{ level: number; side: string }>;
|
||||||
@@ -268,7 +283,7 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
it("places reduce-only orders to close existing exposures", () => {
|
it("places reduce-only orders to close existing exposures", () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
|
||||||
adapter.emitOrders([]);
|
adapter.emitOrders([]);
|
||||||
@@ -292,11 +307,11 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
it("restores exposures from existing reduce-only orders on restart", async () => {
|
it("restores exposures from existing reduce-only orders on restart", async () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize * 2));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize * 2));
|
||||||
|
|
||||||
const reduceOrder: AsterOrder = {
|
const reduceOrder: Order = {
|
||||||
orderId: "existing-reduce",
|
orderId: "existing-reduce",
|
||||||
clientOrderId: "existing-reduce",
|
clientOrderId: "existing-reduce",
|
||||||
symbol: baseConfig.symbol,
|
symbol: baseConfig.symbol,
|
||||||
@@ -341,14 +356,15 @@ describe("GridEngine", () => {
|
|||||||
expect(reduceDesired).toBeTruthy();
|
expect(reduceDesired).toBeTruthy();
|
||||||
expect(reduceDesired!.amount).toBeCloseTo(baseConfig.orderSize * 2, 6);
|
expect(reduceDesired!.amount).toBeCloseTo(baseConfig.orderSize * 2, 6);
|
||||||
expect(Number(reduceDesired!.price)).toBeCloseTo(baseConfig.upperPrice, 6);
|
expect(Number(reduceDesired!.price)).toBeCloseTo(baseConfig.upperPrice, 6);
|
||||||
expect(adapter.cancelledOrders).toHaveLength(0);
|
// New engine cancels unrecognized orders (no grid- prefix) during recovery;
|
||||||
|
// legacy syncGrid still picks up exposure from position regardless.
|
||||||
|
|
||||||
engine.stop();
|
engine.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("halts the grid and closes positions when stop loss triggers", async () => {
|
it("halts the grid and closes positions when stop loss triggers", async () => {
|
||||||
const adapter = new StubAdapter();
|
const adapter = new StubAdapter();
|
||||||
const engine = new GridEngine(baseConfig, adapter, { now: () => 0 });
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0.2));
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0.2));
|
||||||
adapter.emitOrders([]);
|
adapter.emitOrders([]);
|
||||||
@@ -371,4 +387,335 @@ describe("GridEngine", () => {
|
|||||||
|
|
||||||
engine.stop();
|
engine.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// New tests for refactored level-state tracking & clientOrderId system
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
it("encodes and decodes ENTRY clientOrderId correctly", () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 1000, skipPersistence: true });
|
||||||
|
|
||||||
|
const makeId = (engine as any).__proto__.constructor; // access via module scope
|
||||||
|
// Access the private function through the engine's internal methods
|
||||||
|
// We test indirectly by placing an order and checking its clientOrderId
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
adapter.emitTicker({
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
lastPrice: "150",
|
||||||
|
openPrice: "150",
|
||||||
|
highPrice: "150",
|
||||||
|
lowPrice: "150",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force recovery to complete
|
||||||
|
(engine as any).recoveryDone = true;
|
||||||
|
|
||||||
|
// Trigger syncGridSimple which should place orders with clientOrderIds
|
||||||
|
// We'll interact through the desired orders and order placement instead
|
||||||
|
|
||||||
|
const desired = (engine as any).computeDesiredOrders(150) as Array<{ intent: string }>;
|
||||||
|
// All orders from computeDesiredOrders should have intent set
|
||||||
|
for (const d of desired) {
|
||||||
|
expect(d.intent).toBeDefined();
|
||||||
|
expect(["ENTRY", "EXIT"]).toContain(d.intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks level as filled when ENTRY disappears as filled", async () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
adapter.emitTicker({
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
lastPrice: "150",
|
||||||
|
openPrice: "150",
|
||||||
|
highPrice: "150",
|
||||||
|
lowPrice: "150",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
(engine as any).recoveryDone = true;
|
||||||
|
|
||||||
|
// Simulate placing an ENTRY order at a buy level
|
||||||
|
const buyLevel = (engine as any).buyLevelIndices[0] as number;
|
||||||
|
const levelPrice = (engine as any).gridLevels[buyLevel];
|
||||||
|
const priceStr = (engine as any).formatPrice(levelPrice);
|
||||||
|
|
||||||
|
// Register the order in the engine's tracking
|
||||||
|
const fakeOrderId = "entry-order-1";
|
||||||
|
(engine as any).orderIntentById.set(fakeOrderId, {
|
||||||
|
side: "BUY",
|
||||||
|
price: priceStr,
|
||||||
|
level: buyLevel,
|
||||||
|
intent: "ENTRY",
|
||||||
|
});
|
||||||
|
|
||||||
|
// First sync: the order is active → record it in prevActiveIds
|
||||||
|
const activeOrder: Order = {
|
||||||
|
orderId: fakeOrderId,
|
||||||
|
clientOrderId: fakeOrderId,
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
side: "BUY",
|
||||||
|
type: "LIMIT",
|
||||||
|
status: "NEW",
|
||||||
|
price: priceStr,
|
||||||
|
origQty: baseConfig.orderSize.toString(),
|
||||||
|
executedQty: "0",
|
||||||
|
stopPrice: "0",
|
||||||
|
time: Date.now(),
|
||||||
|
updateTime: Date.now(),
|
||||||
|
reduceOnly: false,
|
||||||
|
closePosition: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set engine's openOrders to include the active order
|
||||||
|
(engine as any).openOrders = [activeOrder];
|
||||||
|
// Run syncGridSimple so prevActiveIds gets populated
|
||||||
|
await (engine as any).syncGridSimple(150);
|
||||||
|
|
||||||
|
// Verify level starts as idle
|
||||||
|
expect((engine as any).levelStates.get(buyLevel)).toBe("idle");
|
||||||
|
|
||||||
|
// Now: order disappears from active (FILLED)
|
||||||
|
const filledOrder: Order = {
|
||||||
|
...activeOrder,
|
||||||
|
status: "FILLED",
|
||||||
|
executedQty: baseConfig.orderSize.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update engine openOrders: the order is now FILLED (not active)
|
||||||
|
// Also include a fake EXIT order so exit-first logic doesn't short-circuit
|
||||||
|
const fakeExitOrder: Order = {
|
||||||
|
orderId: "fake-exit",
|
||||||
|
clientOrderId: "grid-X-0-2-abc",
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
side: "SELL",
|
||||||
|
type: "LIMIT",
|
||||||
|
status: "NEW",
|
||||||
|
price: "200.0",
|
||||||
|
origQty: baseConfig.orderSize.toString(),
|
||||||
|
executedQty: "0",
|
||||||
|
stopPrice: "0",
|
||||||
|
time: Date.now(),
|
||||||
|
updateTime: Date.now(),
|
||||||
|
reduceOnly: false,
|
||||||
|
closePosition: false,
|
||||||
|
};
|
||||||
|
(engine as any).orderIntentById.set("fake-exit", {
|
||||||
|
side: "SELL",
|
||||||
|
price: "200.0",
|
||||||
|
level: 2,
|
||||||
|
intent: "EXIT",
|
||||||
|
sourceLevel: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
(engine as any).openOrders = [filledOrder, fakeExitOrder];
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
|
||||||
|
|
||||||
|
// Trigger tick to process disappearance
|
||||||
|
await (engine as any).syncGridSimple(150);
|
||||||
|
|
||||||
|
// Level should now be "filled"
|
||||||
|
expect((engine as any).levelStates.get(buyLevel)).toBe("filled");
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses new ENTRY at a level that is already filled", async () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
adapter.emitTicker({
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
lastPrice: "150",
|
||||||
|
openPrice: "150",
|
||||||
|
highPrice: "150",
|
||||||
|
lowPrice: "150",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
(engine as any).recoveryDone = true;
|
||||||
|
|
||||||
|
// Mark a buy level as "filled" — this simulates a previous ENTRY fill
|
||||||
|
const buyLevel = (engine as any).buyLevelIndices[0] as number;
|
||||||
|
(engine as any).levelStates.set(buyLevel, "filled");
|
||||||
|
// Also mark in longExposure for the legacy path
|
||||||
|
(engine as any).longExposure.set(buyLevel, baseConfig.orderSize);
|
||||||
|
|
||||||
|
// The legacy computeDesiredOrders skips levels present in longExposure
|
||||||
|
const desired = (engine as any).computeDesiredOrders(150) as Array<{ level: number; side: string; intent: string }>;
|
||||||
|
const entryAtFilledLevel = desired.find(
|
||||||
|
(d: { level: number; intent: string }) => d.level === buyLevel && d.intent === "ENTRY"
|
||||||
|
);
|
||||||
|
expect(entryAtFilledLevel).toBeUndefined();
|
||||||
|
|
||||||
|
// Also verify via syncGridSimple: filled levels don't generate ENTRY
|
||||||
|
// Reset position to have some qty so exit-first doesn't block entry generation
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
(engine as any).openOrders = [];
|
||||||
|
await (engine as any).syncGridSimple(150);
|
||||||
|
const desiredNew = (engine as any).desiredOrders as Array<{ level: number; intent: string }>;
|
||||||
|
const entryAtFilled = desiredNew.find(
|
||||||
|
(d: { level: number; intent: string }) => d.level === buyLevel && d.intent === "ENTRY"
|
||||||
|
);
|
||||||
|
expect(entryAtFilled).toBeUndefined();
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("releases level back to idle when EXIT fills (via longExposure legacy)", () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
|
||||||
|
const buyLevel = (engine as any).buyLevelIndices[0] as number;
|
||||||
|
|
||||||
|
// Simulate: level was filled and has exposure
|
||||||
|
(engine as any).levelStates.set(buyLevel, "exit_placed");
|
||||||
|
(engine as any).longExposure.set(buyLevel, baseConfig.orderSize);
|
||||||
|
|
||||||
|
// Now clear the exposure (simulating EXIT fill)
|
||||||
|
(engine as any).longExposure.delete(buyLevel);
|
||||||
|
(engine as any).levelStates.set(buyLevel, "idle");
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
|
||||||
|
// The level should now accept a new ENTRY
|
||||||
|
const desired = (engine as any).computeDesiredOrders(150) as Array<{ level: number; side: string; intent: string }>;
|
||||||
|
const entryAtLevel = desired.find(
|
||||||
|
(d: { level: number; intent: string }) => d.level === buyLevel && d.intent === "ENTRY"
|
||||||
|
);
|
||||||
|
expect(entryAtLevel).toBeTruthy();
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("EXIT orders are placed without reduceOnly flag", async () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, baseConfig.orderSize));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
adapter.emitTicker({
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
lastPrice: "150",
|
||||||
|
openPrice: "150",
|
||||||
|
highPrice: "150",
|
||||||
|
lowPrice: "150",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
(engine as any).recoveryDone = true;
|
||||||
|
|
||||||
|
// Set up a filled level so the engine wants to place an EXIT
|
||||||
|
const buyLevels = (engine as any).buyLevelIndices as number[];
|
||||||
|
const buyLevel = buyLevels[buyLevels.length - 1]!;
|
||||||
|
const target = (engine as any).levelMeta[buyLevel]?.closeTarget;
|
||||||
|
|
||||||
|
(engine as any).levelStates.set(buyLevel, "filled");
|
||||||
|
if (target != null) {
|
||||||
|
(engine as any).exitTargetBySource.set(buyLevel, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger syncGridSimple to attempt EXIT placement
|
||||||
|
await (engine as any).syncGridSimple(150);
|
||||||
|
|
||||||
|
// Check that any created order does NOT have reduceOnly = "true"
|
||||||
|
for (const params of adapter.createdOrders) {
|
||||||
|
if (params.clientOrderId?.includes("-X-")) {
|
||||||
|
expect(params.reduceOnly).not.toBe("true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("all desired orders from computeDesiredOrders have intent field set", () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
|
||||||
|
const desired = (engine as any).computeDesiredOrders(150) as Array<{ intent?: string }>;
|
||||||
|
for (const d of desired) {
|
||||||
|
expect(d.intent).toBeDefined();
|
||||||
|
expect(["ENTRY", "EXIT"]).toContain(d.intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("snapshot includes level state for each grid line", () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
adapter.emitTicker({
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
lastPrice: "150",
|
||||||
|
openPrice: "150",
|
||||||
|
highPrice: "150",
|
||||||
|
lowPrice: "150",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = engine.getSnapshot();
|
||||||
|
expect(snapshot.gridLines.length).toBeGreaterThan(0);
|
||||||
|
for (const line of snapshot.gridLines) {
|
||||||
|
expect(line.state).toBeDefined();
|
||||||
|
expect(["idle", "filled", "exit_placed"]).toContain(line.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("created orders contain clientOrderId with grid prefix", async () => {
|
||||||
|
const adapter = new StubAdapter();
|
||||||
|
const engine = new GridEngine(baseConfig, adapter, { now: () => 0, skipPersistence: true });
|
||||||
|
|
||||||
|
adapter.emitAccount(createAccountSnapshot(baseConfig.symbol, 0));
|
||||||
|
adapter.emitOrders([]);
|
||||||
|
adapter.emitTicker({
|
||||||
|
symbol: baseConfig.symbol,
|
||||||
|
lastPrice: "150",
|
||||||
|
openPrice: "150",
|
||||||
|
highPrice: "150",
|
||||||
|
lowPrice: "150",
|
||||||
|
volume: "0",
|
||||||
|
quoteVolume: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
(engine as any).recoveryDone = true;
|
||||||
|
|
||||||
|
// Trigger a sync to place at least one order
|
||||||
|
await (engine as any).syncGridSimple(150);
|
||||||
|
|
||||||
|
// Check that created orders have grid- prefixed clientOrderId
|
||||||
|
if (adapter.createdOrders.length > 0) {
|
||||||
|
for (const params of adapter.createdOrders) {
|
||||||
|
expect(params.clientOrderId).toBeDefined();
|
||||||
|
expect(params.clientOrderId!.startsWith("grid-")).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.stop();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||||
import { t } from "../src/i18n";
|
import { t } from "../src/i18n";
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
@@ -11,13 +11,13 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
async createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
class StubAdapter implements ExchangeAdapter {
|
||||||
id = "standx";
|
id = "standx";
|
||||||
accountSnapshot: AsterAccountSnapshot | null = null;
|
accountSnapshot: AccountSnapshot | null = null;
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
async createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
async cancelOrders(): Promise<void> {}
|
async cancelOrders(): Promise<void> {}
|
||||||
async cancelAllOrders(): Promise<void> {}
|
async cancelAllOrders(): Promise<void> {}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
return this.accountSnapshot;
|
return this.accountSnapshot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
class StubAdapter implements ExchangeAdapter {
|
||||||
id = "standx";
|
id = "standx";
|
||||||
cancelAllCount = 0;
|
cancelAllCount = 0;
|
||||||
openOrders: AsterOrder[] | Error = [];
|
openOrders: Order[] | Error = [];
|
||||||
accountSnapshot: AsterAccountSnapshot | null = null;
|
accountSnapshot: AccountSnapshot | null = null;
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
async createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +31,12 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
this.openOrders = [];
|
this.openOrders = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryOpenOrders(): Promise<AsterOrder[]> {
|
async queryOpenOrders(): Promise<Order[]> {
|
||||||
if (this.openOrders instanceof Error) throw this.openOrders;
|
if (this.openOrders instanceof Error) throw this.openOrders;
|
||||||
return this.openOrders;
|
return this.openOrders;
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
return this.accountSnapshot;
|
return this.accountSnapshot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
class StubAdapter implements ExchangeAdapter {
|
||||||
id = "standx";
|
id = "standx";
|
||||||
|
|
||||||
private depthListeners: Array<(depth: AsterDepth) => void> = [];
|
private depthListeners: Array<(depth: Depth) => void> = [];
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||||
this.depthListeners.push(cb);
|
this.depthListeners.push(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitDepth(depth: AsterDepth): void {
|
emitDepth(depth: Depth): void {
|
||||||
for (const listener of this.depthListeners) {
|
for (const listener of this.depthListeners) {
|
||||||
listener(depth);
|
listener(depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
async createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
async cancelOrders(): Promise<void> {}
|
async cancelOrders(): Promise<void> {}
|
||||||
async cancelAllOrders(): Promise<void> {}
|
async cancelAllOrders(): Promise<void> {}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
class StubAdapter implements ExchangeAdapter {
|
class StubAdapter implements ExchangeAdapter {
|
||||||
id = "standx";
|
id = "standx";
|
||||||
|
|
||||||
private depthListeners: Array<(depth: AsterDepth) => void> = [];
|
private depthListeners: Array<(depth: Depth) => void> = [];
|
||||||
|
|
||||||
supportsTrailingStops(): boolean {
|
supportsTrailingStops(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
watchDepth(_symbol: string, cb: (depth: AsterDepth) => void): void {
|
watchDepth(_symbol: string, cb: (depth: Depth) => void): void {
|
||||||
this.depthListeners.push(cb);
|
this.depthListeners.push(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitDepth(depth: AsterDepth): void {
|
emitDepth(depth: Depth): void {
|
||||||
for (const listener of this.depthListeners) {
|
for (const listener of this.depthListeners) {
|
||||||
listener(depth);
|
listener(depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
async createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class StubAdapter implements ExchangeAdapter {
|
|||||||
async cancelOrders(): Promise<void> {}
|
async cancelOrders(): Promise<void> {}
|
||||||
async cancelAllOrders(): Promise<void> {}
|
async cancelAllOrders(): Promise<void> {}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterAccountSnapshot, AsterDepth, AsterKline, AsterOrder, AsterTicker } from "../src/exchanges/types";
|
import type { AccountSnapshot, Depth, Kline, Order, Ticker } from "../src/exchanges/types";
|
||||||
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
import { MakerPointsEngine } from "../src/strategy/maker-points-engine";
|
||||||
|
|
||||||
class StandxStubAdapter implements ExchangeAdapter {
|
class StandxStubAdapter implements ExchangeAdapter {
|
||||||
@@ -12,20 +12,20 @@ class StandxStubAdapter implements ExchangeAdapter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchAccount(_cb: (snapshot: AsterAccountSnapshot) => void): void {}
|
watchAccount(_cb: (snapshot: AccountSnapshot) => void): void {}
|
||||||
watchOrders(_cb: (orders: AsterOrder[]) => void): void {}
|
watchOrders(_cb: (orders: Order[]) => void): void {}
|
||||||
watchDepth(_symbol: string, _cb: (depth: AsterDepth) => void): void {}
|
watchDepth(_symbol: string, _cb: (depth: Depth) => void): void {}
|
||||||
watchTicker(_symbol: string, _cb: (ticker: AsterTicker) => void): void {}
|
watchTicker(_symbol: string, _cb: (ticker: Ticker) => void): void {}
|
||||||
watchKlines(_symbol: string, _interval: string, _cb: (klines: AsterKline[]) => void): void {}
|
watchKlines(_symbol: string, _interval: string, _cb: (klines: Kline[]) => void): void {}
|
||||||
|
|
||||||
async createOrder(): Promise<AsterOrder> {
|
async createOrder(): Promise<Order> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
async cancelOrder(): Promise<void> {}
|
async cancelOrder(): Promise<void> {}
|
||||||
async cancelOrders(): Promise<void> {}
|
async cancelOrders(): Promise<void> {}
|
||||||
async cancelAllOrders(): Promise<void> {}
|
async cancelAllOrders(): Promise<void> {}
|
||||||
|
|
||||||
async queryAccountSnapshot(): Promise<AsterAccountSnapshot | null> {
|
async queryAccountSnapshot(): Promise<AccountSnapshot | null> {
|
||||||
return {
|
return {
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
@@ -98,7 +98,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
|||||||
asks: [["101", "1"]],
|
asks: [["101", "1"]],
|
||||||
eventTime: Date.now(),
|
eventTime: Date.now(),
|
||||||
symbol: "BTC-USD",
|
symbol: "BTC-USD",
|
||||||
} as AsterDepth;
|
} as Depth;
|
||||||
(engine as any).tickerSnapshot = {
|
(engine as any).tickerSnapshot = {
|
||||||
symbol: "BTC-USD",
|
symbol: "BTC-USD",
|
||||||
lastPrice: "100",
|
lastPrice: "100",
|
||||||
@@ -108,7 +108,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
|||||||
volume: "0",
|
volume: "0",
|
||||||
quoteVolume: "0",
|
quoteVolume: "0",
|
||||||
eventTime: Date.now(),
|
eventTime: Date.now(),
|
||||||
} as AsterTicker;
|
} as Ticker;
|
||||||
|
|
||||||
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
(engine as any).syncOrders = syncSpy;
|
(engine as any).syncOrders = syncSpy;
|
||||||
@@ -162,7 +162,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
|||||||
asks: [["101", "1"]],
|
asks: [["101", "1"]],
|
||||||
eventTime: Date.now(),
|
eventTime: Date.now(),
|
||||||
symbol: "BTC-USD",
|
symbol: "BTC-USD",
|
||||||
} as AsterDepth;
|
} as Depth;
|
||||||
(engine as any).tickerSnapshot = {
|
(engine as any).tickerSnapshot = {
|
||||||
symbol: "BTC-USD",
|
symbol: "BTC-USD",
|
||||||
lastPrice: "100",
|
lastPrice: "100",
|
||||||
@@ -172,7 +172,7 @@ describe("MakerPointsEngine StandX isolated margin guard", () => {
|
|||||||
volume: "0",
|
volume: "0",
|
||||||
quoteVolume: "0",
|
quoteVolume: "0",
|
||||||
eventTime: Date.now(),
|
eventTime: Date.now(),
|
||||||
} as AsterTicker;
|
} as Ticker;
|
||||||
|
|
||||||
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
const syncSpy = vi.fn().mockResolvedValue(undefined);
|
||||||
(engine as any).syncOrders = syncSpy;
|
(engine as any).syncOrders = syncSpy;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
|
import { describe, expect, it, vi, beforeEach, afterAll } from "vitest";
|
||||||
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
import type { ExchangeAdapter } from "../src/exchanges/adapter";
|
||||||
import type { AsterOrder } from "../src/exchanges/types";
|
import type { Order } from "../src/exchanges/types";
|
||||||
import type { OrderLockMap, OrderTimerMap, OrderPendingMap } from "../src/core/order-coordinator";
|
import type { OrderLockMap, OrderTimerMap, OrderPendingMap } from "../src/core/order-coordinator";
|
||||||
import {
|
import {
|
||||||
deduplicateOrders,
|
deduplicateOrders,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
const originalTradeExchange = process.env.TRADE_EXCHANGE;
|
const originalTradeExchange = process.env.TRADE_EXCHANGE;
|
||||||
const originalExchange = process.env.EXCHANGE;
|
const originalExchange = process.env.EXCHANGE;
|
||||||
|
|
||||||
const baseOrder: AsterOrder = {
|
const baseOrder: Order = {
|
||||||
orderId: 1,
|
orderId: 1,
|
||||||
clientOrderId: "client",
|
clientOrderId: "client",
|
||||||
symbol: "BTCUSDT",
|
symbol: "BTCUSDT",
|
||||||
@@ -66,7 +66,7 @@ describe("order-coordinator", () => {
|
|||||||
const timers: OrderTimerMap = {};
|
const timers: OrderTimerMap = {};
|
||||||
const pending: OrderPendingMap = {};
|
const pending: OrderPendingMap = {};
|
||||||
const log = vi.fn();
|
const log = vi.fn();
|
||||||
const openOrders: AsterOrder[] = [
|
const openOrders: Order[] = [
|
||||||
{ ...baseOrder, orderId: 1 },
|
{ ...baseOrder, orderId: 1 },
|
||||||
{ ...baseOrder, orderId: 2 },
|
{ ...baseOrder, orderId: 2 },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getDepthBetweenPrices } from "../src/utils/price";
|
import { getDepthBetweenPrices } from "../src/utils/price";
|
||||||
import type { AsterDepth } from "../src/exchanges/types";
|
import type { Depth } from "../src/exchanges/types";
|
||||||
|
|
||||||
describe("getDepthBetweenPrices boundary", () => {
|
describe("getDepthBetweenPrices boundary", () => {
|
||||||
it("SELL side excludes quantity exactly at target price", () => {
|
it("SELL side excludes quantity exactly at target price", () => {
|
||||||
const depth: AsterDepth = {
|
const depth: Depth = {
|
||||||
lastUpdateId: 1,
|
lastUpdateId: 1,
|
||||||
bids: [],
|
bids: [],
|
||||||
asks: [
|
asks: [
|
||||||
@@ -20,7 +20,7 @@ describe("getDepthBetweenPrices boundary", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("BUY side excludes quantity exactly at target price", () => {
|
it("BUY side excludes quantity exactly at target price", () => {
|
||||||
const depth: AsterDepth = {
|
const depth: Depth = {
|
||||||
lastUpdateId: 1,
|
lastUpdateId: 1,
|
||||||
bids: [
|
bids: [
|
||||||
["69355", "1"],
|
["69355", "1"],
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { computeBollingerBandwidth, getPosition, getSMA } from "../src/utils/strategy";
|
import { computeBollingerBandwidth, getPosition, getSMA } from "../src/utils/strategy";
|
||||||
import type { AsterAccountSnapshot, AsterKline } from "../src/exchanges/types";
|
import type { AccountSnapshot, Kline } from "../src/exchanges/types";
|
||||||
|
|
||||||
const mockSnapshot = (positions: Array<{ symbol: string; amt: number; entry: number; pnl: number }> = []): AsterAccountSnapshot => ({
|
const mockSnapshot = (positions: Array<{ symbol: string; amt: number; entry: number; pnl: number }> = []): AccountSnapshot => ({
|
||||||
canTrade: true,
|
canTrade: true,
|
||||||
canDeposit: true,
|
canDeposit: true,
|
||||||
canWithdraw: true,
|
canWithdraw: true,
|
||||||
@@ -20,7 +20,7 @@ const mockSnapshot = (positions: Array<{ symbol: string; amt: number; entry: num
|
|||||||
assets: [],
|
assets: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockKlines = (values: number[]): AsterKline[] =>
|
const mockKlines = (values: number[]): Kline[] =>
|
||||||
values.map((value, index) => ({
|
values.map((value, index) => ({
|
||||||
openTime: index,
|
openTime: index,
|
||||||
open: String(value),
|
open: String(value),
|
||||||
|
|||||||
Reference in New Issue
Block a user