mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
add test
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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