mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 16:28:06 +00:00
* feat(grid): add grid shift, exchange stops, and reconcile safeguards * docs(grid): document smart-follow shift and new grid env vars
146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { promises as fs } from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
import {
|
|
loadGridState,
|
|
saveGridState,
|
|
clearGridState,
|
|
migrateV1ToV2,
|
|
type StoredGridStateV2,
|
|
} from "../src/strategy/common/grid-storage";
|
|
|
|
let tmpDir: string;
|
|
let prevDataDir: string | undefined;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "grid-storage-test-"));
|
|
prevDataDir = process.env.GRID_DATA_DIR;
|
|
process.env.GRID_DATA_DIR = tmpDir;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (prevDataDir == null) delete process.env.GRID_DATA_DIR;
|
|
else process.env.GRID_DATA_DIR = prevDataDir;
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
function makeV2(symbol: string): StoredGridStateV2 {
|
|
return {
|
|
schemaVersion: 2,
|
|
symbol,
|
|
exchangeId: "aster",
|
|
gridVersion: 3,
|
|
anchorPrice: 150,
|
|
lowerPrice: 100,
|
|
upperPrice: 200,
|
|
gridLevels: 5,
|
|
orderSize: 0.1,
|
|
maxPositionSize: 0.4,
|
|
direction: "neutral",
|
|
gridMode: "geometric",
|
|
levels: {
|
|
"1": { phase: "holding", exitTarget: 2, holdQty: 0.1 },
|
|
"3": { phase: "exit_placed", exitTarget: 2, holdQty: 0.1, exitOrderId: "o-9" },
|
|
},
|
|
intents: [
|
|
{
|
|
orderId: "o-9",
|
|
clientOrderId: "grid-3-X-3-2-abc",
|
|
intent: "EXIT",
|
|
side: "BUY",
|
|
price: "141.4",
|
|
qty: 0.1,
|
|
level: 3,
|
|
target: 2,
|
|
gridVersion: 3,
|
|
createdAt: 1000,
|
|
},
|
|
],
|
|
inflight: null,
|
|
shift: { phase: "closing", targetAnchor: 210, startedAt: 2000 },
|
|
exchangeStop: { orderId: "stop-1", side: "SELL", stopPrice: 99 },
|
|
updatedAt: 3000,
|
|
};
|
|
}
|
|
|
|
describe("grid-storage v2", () => {
|
|
it("round-trips a v2 snapshot", async () => {
|
|
const snapshot = makeV2("BTCUSDT");
|
|
await saveGridState(snapshot);
|
|
const loaded = await loadGridState("BTCUSDT");
|
|
expect(loaded).toEqual(snapshot);
|
|
});
|
|
|
|
it("returns null for unknown symbols", async () => {
|
|
expect(await loadGridState("NONE")).toBeNull();
|
|
});
|
|
|
|
it("keeps entries for other symbols on clear", async () => {
|
|
await saveGridState(makeV2("BTCUSDT"));
|
|
await saveGridState(makeV2("ETHUSDT"));
|
|
await clearGridState("BTCUSDT");
|
|
expect(await loadGridState("BTCUSDT")).toBeNull();
|
|
expect(await loadGridState("ETHUSDT")).not.toBeNull();
|
|
});
|
|
|
|
it("removes the file when the last symbol is cleared", async () => {
|
|
await saveGridState(makeV2("BTCUSDT"));
|
|
await clearGridState("BTCUSDT");
|
|
await expect(fs.stat(path.join(tmpDir, "grid-record.json"))).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("grid-storage v1 migration", () => {
|
|
const v1Entry = {
|
|
symbol: "BTCUSDT",
|
|
lowerPrice: 100,
|
|
upperPrice: 200,
|
|
gridLevels: 5,
|
|
orderSize: 0.1,
|
|
maxPositionSize: 0.4,
|
|
direction: "both",
|
|
levels: {
|
|
"0": { state: "filled", sourceLevel: 0, targetLevel: 1 },
|
|
"1": { state: "exit_placed", sourceLevel: 1, targetLevel: 2, exitOrderId: "legacy-1" },
|
|
"2": { state: "idle", sourceLevel: 2, targetLevel: null },
|
|
},
|
|
updatedAt: 1234,
|
|
};
|
|
|
|
it("loads v1 entries as migrated v2", async () => {
|
|
await fs.writeFile(
|
|
path.join(tmpDir, "grid-record.json"),
|
|
JSON.stringify({ BTCUSDT: v1Entry }),
|
|
"utf8"
|
|
);
|
|
const loaded = await loadGridState("BTCUSDT");
|
|
expect(loaded).not.toBeNull();
|
|
expect(loaded!.schemaVersion).toBe(2);
|
|
expect(loaded!.gridVersion).toBe(1);
|
|
expect(loaded!.anchorPrice).toBeNull();
|
|
expect(loaded!.levels["0"]).toEqual({ phase: "holding", exitTarget: 1, holdQty: 0.1 });
|
|
expect(loaded!.levels["1"]).toEqual({
|
|
phase: "exit_placed",
|
|
exitTarget: 2,
|
|
holdQty: 0.1,
|
|
exitOrderId: "legacy-1",
|
|
});
|
|
expect(loaded!.levels["2"]).toBeUndefined();
|
|
expect(loaded!.intents).toEqual([]);
|
|
});
|
|
|
|
it("migrateV1ToV2 preserves config fingerprint fields", () => {
|
|
const migrated = migrateV1ToV2(v1Entry as any);
|
|
expect(migrated.symbol).toBe("BTCUSDT");
|
|
expect(migrated.direction).toBe("both");
|
|
expect(migrated.orderSize).toBe(0.1);
|
|
expect(migrated.gridLevels).toBe(5);
|
|
expect(migrated.gridMode).toBe("geometric");
|
|
expect(migrated.lowerPrice).toBe(100);
|
|
expect(migrated.upperPrice).toBe(200);
|
|
});
|
|
});
|