mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
feat(grid): add grid shift, exchange stops, and reconcile safeguards
This commit is contained in:
+604
-535
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user