Skip to content

Commit 778e36e

Browse files
committed
test: cover 5m target-triggered ratchet
1 parent adbce76 commit 778e36e

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
applyTargetTriggeredRatchet,
4+
TARGET_TRIGGERED_RATCHET_CANDIDATES
5+
} from "../research/regime-trend-v1/target-triggered-ratchet-tools.mjs";
6+
7+
const FIVE = 5 * 60 * 1000;
8+
9+
function trade(overrides = {}) {
10+
return {
11+
strategy_id: "regime-trend-v1",
12+
implementation_version: "test",
13+
dataset_hash: "test",
14+
symbol: "TESTUSDT",
15+
timeframe: "4h",
16+
direction: "long",
17+
signal_timestamp: -4 * 60 * 60 * 1000,
18+
entry_timestamp: 0,
19+
raw_entry_open: 100,
20+
entry_fill: 100,
21+
entry_atr: 10,
22+
initial_stop: 90,
23+
exit_timestamp: 3 * FIVE,
24+
raw_exit_reference: 100,
25+
exit_fill: 99.95,
26+
exit_reason: "trend_exit",
27+
quantity: 0.01,
28+
entry_fee: 0.001,
29+
exit_fee: 0.0009995,
30+
gross_pnl: -0.0005,
31+
net_pnl: -0.0024995,
32+
net_return: -0.0024995,
33+
bars_held: 0,
34+
...overrides
35+
};
36+
}
37+
38+
function candle(timestamp: number, overrides = {}) {
39+
return {
40+
timestamp,
41+
open: 100,
42+
high: 102,
43+
low: 98,
44+
close: 100,
45+
volume: 1,
46+
...overrides
47+
};
48+
}
49+
50+
function map(candles: ReturnType<typeof candle>[]) {
51+
return new Map(candles.map((item) => [item.timestamp, item]));
52+
}
53+
54+
describe("Regime Trend v1 target-triggered ratchet", () => {
55+
it("freezes exactly five candidates", () => {
56+
expect(TARGET_TRIGGERED_RATCHET_CANDIDATES).toHaveLength(5);
57+
});
58+
59+
it("activates after target touch and applies the floor only on the next 5m candle", () => {
60+
const candidate = TARGET_TRIGGERED_RATCHET_CANDIDATES.find((item) => item.id === "touch-1.50-lock-0.00")!;
61+
const candles = map([
62+
candle(0, { high: 116, low: 95 }),
63+
candle(FIVE, { open: 101, high: 102, low: 99 }),
64+
candle(2 * FIVE)
65+
]);
66+
const result = applyTargetTriggeredRatchet(trade(), [], candles, candidate);
67+
expect(result.activated).toBe(true);
68+
expect(result.classification).toBe("RATCHET_EXIT");
69+
expect(result.timestamp).toBe(FIVE);
70+
});
71+
72+
it("does not retroactively apply a ratchet inside the activation candle", () => {
73+
const candidate = TARGET_TRIGGERED_RATCHET_CANDIDATES[0];
74+
const candles = map([
75+
candle(0, { high: 116, low: 89 })
76+
]);
77+
const result = applyTargetTriggeredRatchet(
78+
trade({ exit_timestamp: FIVE, exit_reason: "initial_stop" }),
79+
[],
80+
candles,
81+
candidate
82+
);
83+
expect(result.classification).toBe("BASELINE_STOP");
84+
expect(result.activated).toBe(false);
85+
});
86+
87+
it("keeps the baseline trend exit when activated but the floor is not touched", () => {
88+
const candidate = TARGET_TRIGGERED_RATCHET_CANDIDATES[2];
89+
const candles = map([
90+
candle(0, { high: 121, low: 95 }),
91+
candle(FIVE, { high: 125, low: 101 }),
92+
candle(2 * FIVE, { high: 130, low: 102 })
93+
]);
94+
const result = applyTargetTriggeredRatchet(trade(), [], candles, candidate);
95+
expect(result.classification).toBe("ACTIVATED_BASELINE_EXIT");
96+
expect(result.trade.net_pnl).toBe(trade().net_pnl);
97+
});
98+
99+
it("returns DATA_GAP instead of inventing a missing 5m candle", () => {
100+
const candidate = TARGET_TRIGGERED_RATCHET_CANDIDATES[0];
101+
const result = applyTargetTriggeredRatchet(trade(), [], map([candle(0)]), candidate);
102+
expect(result.classification).toBe("DATA_GAP");
103+
});
104+
});

0 commit comments

Comments
 (0)