mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 17:28:08 +00:00
add test
This commit is contained in:
@@ -3,6 +3,12 @@
|
|||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run index.ts",
|
||||||
|
"start": "bun run index.ts",
|
||||||
|
"test": "bun x vitest run",
|
||||||
|
"test:watch": "bun x vitest"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user