This commit is contained in:
discountry
2025-09-23 02:10:27 +08:00
parent 1386455870
commit c93d731382
3 changed files with 252 additions and 0 deletions
+6
View File
@@ -3,6 +3,12 @@
"module": "index.ts",
"type": "module",
"private": true,
"scripts": {
"dev": "bun run index.ts",
"start": "bun run index.ts",
"test": "bun x vitest run",
"test:watch": "bun x vitest"
},
"devDependencies": {
"@types/bun": "latest",
"vitest": "^3.2.4"
+188
View File
@@ -0,0 +1,188 @@
import { describe, expect, it, vi } from "vitest";
import type { ExchangeAdapter } from "../src/exchanges/adapter";
import type { AsterOrder } from "../src/exchanges/types";
import {
deduplicateOrders,
placeOrder,
placeMarketOrder,
placeStopLossOrder,
placeTrailingStopOrder,
marketClose,
unlockOperating,
OrderLockMap,
OrderTimerMap,
OrderPendingMap,
} from "../src/core/order-coordinator";
const baseOrder: AsterOrder = {
orderId: 1,
clientOrderId: "client",
symbol: "BTCUSDT",
side: "BUY",
type: "LIMIT",
status: "NEW",
price: "100",
origQty: "1",
executedQty: "0",
stopPrice: "0",
time: Date.now(),
updateTime: Date.now(),
reduceOnly: false,
closePosition: false,
};
function createMockExchange(overrides: Partial<ExchangeAdapter> = {}): ExchangeAdapter {
return {
id: "mock",
watchAccount: () => undefined,
watchOrders: () => undefined,
watchDepth: () => undefined,
watchTicker: () => undefined,
watchKlines: () => undefined,
createOrder: vi.fn(async () => baseOrder),
cancelOrder: vi.fn(async () => undefined),
cancelOrders: vi.fn(async () => undefined),
cancelAllOrders: vi.fn(async () => undefined),
...overrides,
};
}
describe("order-coordinator", () => {
it("deduplicates orders by type and side", async () => {
const adapter = createMockExchange();
const locks: OrderLockMap = {};
const timers: OrderTimerMap = {};
const pending: OrderPendingMap = {};
const log = vi.fn();
const openOrders: AsterOrder[] = [
{ ...baseOrder, orderId: 1 },
{ ...baseOrder, orderId: 2 },
];
await deduplicateOrders(adapter, "BTCUSDT", openOrders, locks, timers, pending, "LIMIT", "BUY", log);
expect(adapter.cancelOrders).toHaveBeenCalledWith({ symbol: "BTCUSDT", orderIdList: [2] });
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("去重撤销重复"));
});
it("places limit orders and records pending id", async () => {
const adapter = createMockExchange();
const locks: OrderLockMap = {};
const timers: OrderTimerMap = {};
const pending: OrderPendingMap = {};
const log = vi.fn();
await placeOrder(
adapter,
"BTCUSDT",
[],
locks,
timers,
pending,
"BUY",
100,
1,
log,
false
);
expect(adapter.createOrder).toHaveBeenCalled();
expect(pending.MARKET).toBeUndefined();
expect(pending.LIMIT).toBe(String(baseOrder.orderId));
});
it("places market order and unlocks after completion", async () => {
const adapter = createMockExchange();
const locks: OrderLockMap = {};
const timers: OrderTimerMap = {};
const pending: OrderPendingMap = {};
const log = vi.fn();
await placeMarketOrder(
adapter,
"BTCUSDT",
[],
locks,
timers,
pending,
"SELL",
1,
log,
true
);
expect(adapter.createOrder).toHaveBeenCalled();
expect(pending.MARKET).toBe(String(baseOrder.orderId));
});
it("places stop loss order only when valid", async () => {
const adapter = createMockExchange();
const locks: OrderLockMap = {};
const timers: OrderTimerMap = {};
const pending: OrderPendingMap = {};
const log = vi.fn();
await placeStopLossOrder(
adapter,
"BTCUSDT",
[],
locks,
timers,
pending,
"SELL",
99,
1,
100,
log
);
expect(adapter.createOrder).toHaveBeenCalled();
expect(log).toHaveBeenCalledWith("stop", expect.stringContaining("STOP_MARKET"));
});
it("places trailing stop order", async () => {
const adapter = createMockExchange();
const locks: OrderLockMap = {};
const timers: OrderTimerMap = {};
const pending: OrderPendingMap = {};
const log = vi.fn();
await placeTrailingStopOrder(
adapter,
"BTCUSDT",
[],
locks,
timers,
pending,
"SELL",
101,
1,
0.2,
log
);
expect(adapter.createOrder).toHaveBeenCalled();
expect(log).toHaveBeenCalledWith("order", expect.stringContaining("挂动态止盈单"));
});
it("market close cancels open orders before placing close order", async () => {
const adapter = createMockExchange();
const locks: OrderLockMap = {};
const timers: OrderTimerMap = {};
const pending: OrderPendingMap = {};
const log = vi.fn();
await marketClose(
adapter,
"BTCUSDT",
[{ ...baseOrder, orderId: 2 }],
locks,
timers,
pending,
"SELL",
"BUY",
log
);
expect(adapter.createOrder).toHaveBeenCalled();
expect(log).toHaveBeenCalledWith("close", expect.stringContaining("市价平仓"));
});
it("unlockOperating clears timers and pending", () => {
const locks: OrderLockMap = { LIMIT: true };
const timers: OrderTimerMap = { LIMIT: setTimeout(() => undefined, 0) };
const pending: OrderPendingMap = { LIMIT: "123" };
unlockOperating(locks, timers, pending, "LIMIT");
expect(locks.LIMIT).toBe(false);
expect(pending.LIMIT).toBeNull();
expect(timers.LIMIT).toBeNull();
});
});
+58
View File
@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import { getPosition, getSMA } from "../src/utils/strategy";
import type { AsterAccountSnapshot, AsterKline } from "../src/exchanges/types";
const mockSnapshot = (positions: Array<{ symbol: string; amt: number; entry: number; pnl: number }> = []): AsterAccountSnapshot => ({
canTrade: true,
canDeposit: true,
canWithdraw: true,
updateTime: Date.now(),
totalWalletBalance: "0",
totalUnrealizedProfit: "0",
positions: positions.map((p) => ({
symbol: p.symbol,
positionAmt: String(p.amt),
entryPrice: String(p.entry),
unrealizedProfit: String(p.pnl),
positionSide: "BOTH",
updateTime: Date.now(),
})),
assets: [],
});
const mockKlines = (values: number[]): AsterKline[] =>
values.map((value, index) => ({
openTime: index,
open: String(value),
high: String(value),
low: String(value),
close: String(value),
volume: "0",
closeTime: index + 1,
numberOfTrades: 0,
}));
describe("strategy utils", () => {
it("returns default position when snapshot missing", () => {
expect(getPosition(null, "BTCUSDT")).toEqual({ positionAmt: 0, entryPrice: 0, unrealizedProfit: 0 });
});
it("extracts position for symbol", () => {
const snapshot = mockSnapshot([{ symbol: "BTCUSDT", amt: 1, entry: 100, pnl: 5 }]);
expect(getPosition(snapshot, "BTCUSDT")).toEqual({ positionAmt: 1, entryPrice: 100, unrealizedProfit: 5 });
});
it("returns zero position when symbol not found", () => {
const snapshot = mockSnapshot([{ symbol: "ETHUSDT", amt: 2, entry: 200, pnl: 10 }]);
expect(getPosition(snapshot, "BTCUSDT")).toEqual({ positionAmt: 0, entryPrice: 0, unrealizedProfit: 0 });
});
it("returns null when not enough klines", () => {
expect(getSMA(mockKlines([1, 2, 3]), 5)).toBeNull();
});
it("computes SMA for latest closes", () => {
const data = mockKlines(Array.from({ length: 30 }, (_, i) => i + 1));
expect(getSMA(data, 30)).toBe(15.5);
});
});