Files
ritmex-bot/src/strategy/maker-points-defense.test.ts
T
discountry 22b9c5a39d i18n: route the remaining 300 hardcoded strings through the table
Grid, offset-maker, liquidity-maker and maker-points wrote their trade log and
Telegram alerts as Chinese literals, so LANG=en changed the menu but not a
single runtime message. src/ now holds zero user-facing Chinese literals.

Reuses rather than duplicates: the four engines' subscription boilerplate maps
onto the existing log.subscribe.* / log.process.* keys, and the spot-maker
wording shared by offset-maker and liquidity-maker became one log.spotMaker.*
set instead of two.

Exchange gateways were treated differently on purpose: aster's exception and
console.error text is developer diagnostics, not UI, and its eight sibling
gateways already used English — so those were translated to English rather than
added to the table.

Adds tests/i18n-coverage.test.ts to hold the line: it fails on any new CJK
literal in src/, on a key missing either language, and on a duplicate key.
Several existing tests asserted the Chinese literals, which is what let the gap
persist; they now assert the resolved key and hold in either language.

214 new keys (300 -> 514). 275 pass; tsc and oxlint clean.
2026-07-29 21:35:19 +08:00

168 lines
5.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
ACCOUNT_DATA_STALE_THRESHOLD_MS,
DATA_STALE_THRESHOLD_MS,
REST_ERROR_DEFENSE_THRESHOLD,
defenseReasonsFor,
describeDefenseReasons,
evaluateDefense,
type DefenseInputs,
} from "./maker-points-defense";
import { t } from "../i18n";
const NOW = 1_700_000_000_000;
function inputs(overrides: Partial<DefenseInputs> = {}): DefenseInputs {
return {
now: NOW,
lastDepthTime: NOW,
lastAccountTime: NOW,
lastBinanceDepthTime: NOW,
binanceHealth: { healthy: true },
accountHealth: { ok: true },
hasAccountSnapshot: true,
accountProbeFailures: 0,
accountProbeInFlight: false,
restUnhealthy: false,
restConsecutiveErrors: 0,
restLastError: null,
marginMode: "isolated",
enforceIsolatedMargin: true,
...overrides,
};
}
describe("evaluateDefense", () => {
it("stays out of defense when every feed is fresh", () => {
expect(evaluateDefense(inputs()).shouldDefend).toBe(false);
});
it("treats a feed that has never reported as fresh, not stale", () => {
// Startup: age 0 must not be read as "infinitely old".
const verdict = evaluateDefense(
inputs({ lastDepthTime: 0, lastAccountTime: 0, lastBinanceDepthTime: 0 })
);
expect(verdict.shouldDefend).toBe(false);
expect(verdict.reasons.depthAge).toBe(0);
});
it("defends on a stale venue depth feed", () => {
const verdict = evaluateDefense(
inputs({ lastDepthTime: NOW - DATA_STALE_THRESHOLD_MS - 1 })
);
expect(verdict.shouldDefend).toBe(true);
expect(verdict.reasons.depthStale).toBe(true);
});
it("does not defend exactly at the staleness threshold", () => {
expect(evaluateDefense(inputs({ lastDepthTime: NOW - DATA_STALE_THRESHOLD_MS })).shouldDefend).toBe(
false
);
});
it("defends on a stale Binance depth feed or an unhealthy book", () => {
expect(
evaluateDefense(inputs({ lastBinanceDepthTime: NOW - DATA_STALE_THRESHOLD_MS - 1 })).shouldDefend
).toBe(true);
expect(
evaluateDefense(inputs({ binanceHealth: { healthy: false, reason: "gap" } })).shouldDefend
).toBe(true);
});
it("probes but does not defend when the account feed first goes quiet", () => {
const verdict = evaluateDefense(
inputs({ lastAccountTime: NOW - ACCOUNT_DATA_STALE_THRESHOLD_MS - 1 })
);
expect(verdict.needsAccountProbe).toBe(true);
expect(verdict.shouldDefend).toBe(false);
});
it("holds off while the account REST probe is still in flight", () => {
const verdict = evaluateDefense(
inputs({
lastAccountTime: NOW - ACCOUNT_DATA_STALE_THRESHOLD_MS - 1,
accountProbeFailures: 2,
accountProbeInFlight: true,
})
);
expect(verdict.shouldDefend).toBe(false);
});
it("defends once the account REST probe has failed", () => {
const verdict = evaluateDefense(
inputs({
lastAccountTime: NOW - ACCOUNT_DATA_STALE_THRESHOLD_MS - 1,
accountProbeFailures: 1,
accountProbeInFlight: false,
})
);
expect(verdict.shouldDefend).toBe(true);
expect(verdict.reasons.accountStale).toBe(true);
});
it("defends on an invalid account snapshot and carries its issues", () => {
const verdict = evaluateDefense(
inputs({ accountHealth: { ok: false, issues: ["missing position"] } })
);
expect(verdict.shouldDefend).toBe(true);
expect(verdict.reasons.accountIssues).toEqual(["missing position"]);
});
it("ignores account validity before any snapshot has arrived", () => {
const verdict = evaluateDefense(
inputs({ hasAccountSnapshot: false, accountHealth: { ok: false, issues: ["x"] } })
);
expect(verdict.shouldDefend).toBe(false);
});
it("defends only after REST failures reach the threshold", () => {
const below = evaluateDefense(
inputs({ restUnhealthy: true, restConsecutiveErrors: REST_ERROR_DEFENSE_THRESHOLD - 1 })
);
expect(below.shouldDefend).toBe(false);
const at = evaluateDefense(
inputs({ restUnhealthy: true, restConsecutiveErrors: REST_ERROR_DEFENSE_THRESHOLD })
);
expect(at.shouldDefend).toBe(true);
expect(at.reasons.restUnhealthy).toBe(true);
});
it("defends on a non-isolated margin mode only where it is enforced", () => {
expect(evaluateDefense(inputs({ marginMode: "cross" })).shouldDefend).toBe(true);
expect(
evaluateDefense(inputs({ marginMode: "cross", enforceIsolatedMargin: false })).shouldDefend
).toBe(false);
});
it("does not defend on an unknown margin mode", () => {
expect(evaluateDefense(inputs({ marginMode: null })).shouldDefend).toBe(false);
});
});
describe("describeDefenseReasons", () => {
it("names every active cause", () => {
const summary = describeDefenseReasons(
defenseReasonsFor({
depthStale: true,
depthAge: 7_000,
restUnhealthy: true,
restConsecutiveErrors: 4,
})
);
expect(summary).toContain(t("defense.reason.depth", { seconds: 7 }));
expect(summary).toContain(t("defense.reason.rest", { count: 4 }));
});
it("falls back to unknown when nothing is flagged", () => {
expect(describeDefenseReasons(defenseReasonsFor({}))).toBe(t("defense.reason.unknown"));
});
it("omits the Binance book reason when there is none", () => {
const summary = describeDefenseReasons(
defenseReasonsFor({ binanceUnhealthy: true, binanceHealthReason: null })
);
expect(summary).toBe(t("defense.reason.unknown"));
});
});