Files
2026-05-31 13:49:36 +08:00

136 lines
5.0 KiB
TypeScript

/**
* Strategy 5 · Prob Chase — when diff crosses, the probability is too low; hold to window settlement
*
* Core logic: at the moment diff breaks the threshold, if the probability has not caught up yet (deviates from the historical fair probability),
* it means the market is reacting slowly, so enter and buy.
*
* Exit: no take profit, no timeout; only stop loss when diff crosses to the reverse ±5; otherwise hold to window-end settlement.
*/
import type {
IStrategy, StrategyKey, StrategyNumber, StrategyDirection,
StrategyTickContext, EntrySignal, ExitSignal, StrategyDescription,
} from "./types.js";
import { getFairProb } from "./_core/fair-prob.js";
// ── Entry parameters ────────────────────────────────────────────────
const ENTRY_DIFF = 25; // check the deviation when diff crosses this threshold
const ENTRY_BIAS_MIN = 10; // enter only when the probability deviation is at least this many percentage points (fair probability - actual probability ≥ 10)
const WINDOW_MAX_REMAINING = 90; // entry scan start: remaining ≤90s
const WINDOW_MIN_REMAINING = 30; // entry scan end: remaining ≤30s
// ── Exit parameters ────────────────────────────────────────────────
const STOP_LOSS_DIFF = 5; // cross -5 stop loss: buy up diff≤-5 / buy down diff≥5
interface S5State {
lastDiff: number | null;
entryBias: number; // deviation value at entry
entryTs: number; // entry timestamp
}
function createState(): S5State {
return {
lastDiff: null,
entryBias: 0,
entryTs: 0,
};
}
export class P1ProbChase implements IStrategy {
readonly key: StrategyKey = "p1";
readonly number: StrategyNumber = 1;
readonly name = "Prob Chase";
private s: S5State = createState();
getDescription(): StrategyDescription {
return {
key: this.key,
number: this.number,
name: this.name,
title: "Prob Chase 1",
category: { id: "prob-chase", label: "Prob Chase", color: "#f0a500" },
supportedMarkets: ["btc-5m"],
lines: [
{ text: `⏱ Checked when remaining ${WINDOW_MAX_REMAINING}s~${WINDOW_MIN_REMAINING}s` },
{ text: `📈 Enter when diff crosses ±${ENTRY_DIFF} and the probability deviation ≥${ENTRY_BIAS_MIN}%` },
{ text: "deviation = historical fair probability - current probability (probability has not caught up with diff)" },
{ text: `Stop loss: buy up diff≤-${STOP_LOSS_DIFF} / buy down diff≥${STOP_LOSS_DIFF}`, color: "#f85149", marginTop: true },
{ text: "No take profit, no timeout; hold to window end and let settlement decide", color: "#f0a500" },
{ text: "Fair probability judged from a diff+rem 2D mapping table", color: "#888", marginTop: true },
],
};
}
updateGuards(_ctx: StrategyTickContext): void {}
checkEntry(ctx: StrategyTickContext): EntrySignal | null {
const { rem, upPct, dnPct, diff } = ctx;
if (upPct == null || dnPct == null || diff == null) return null;
if (rem > WINDOW_MAX_REMAINING || rem <= WINDOW_MIN_REMAINING) return null;
const lastDiff = this.s.lastDiff;
if (lastDiff == null) return null;
// buy-up crossing
if (lastDiff <= ENTRY_DIFF && diff > ENTRY_DIFF) {
const fair = getFairProb(diff, rem);
if (fair != null && fair - upPct >= ENTRY_BIAS_MIN) {
this.s.entryBias = fair - upPct;
return { direction: "up" };
}
}
// buy-down crossing
if (lastDiff >= -ENTRY_DIFF && diff < -ENTRY_DIFF) {
const fair = getFairProb(diff, rem);
if (fair != null) {
const fairDn = 100 - fair;
const bias = fairDn - dnPct;
if (bias >= ENTRY_BIAS_MIN) {
this.s.entryBias = bias;
return { direction: "down" };
}
}
}
return null;
}
onEntryFilled(ctx: StrategyTickContext, _direction: StrategyDirection): void {
this.s.entryTs = ctx.now;
}
checkExit(ctx: StrategyTickContext, direction: StrategyDirection): ExitSignal {
const { diff } = ctx;
if (diff == null) return null;
// cross -5 stop loss: buy up diff≤-5 / buy down diff≥5
if (direction === "up" && diff <= -STOP_LOSS_DIFF) {
return { signal: "sl", reason: `reverse-cross stop loss diff ${Math.round(diff)}≤-${STOP_LOSS_DIFF}` };
}
if (direction === "down" && diff >= STOP_LOSS_DIFF) {
return { signal: "sl", reason: `reverse-cross stop loss diff ${Math.round(diff)}${STOP_LOSS_DIFF}` };
}
// no take profit, no timeout, no forced close → return null, hold to window-end settlement
return null;
}
finalizeTick(diff: number | null): void {
this.s.lastDiff = diff;
}
resetState(): void {
this.s = createState();
}
getStatePayload(): Record<string, unknown> {
return {
lastDiff: this.s.lastDiff,
entryBias: this.s.entryBias,
entryTs: this.s.entryTs,
};
}
}