Files
polymarket-terminal/src/oneshot/StateMachine.js
T
direkturcrypto f074ca9ecb feat(oneshot): add Anti-Flip 5m microstructure execution engine
Introduces a complete, event-driven execution engine for 5-minute
Polymarket UP/DOWN markets, implementing the Anti-Flip strategy spec.

Architecture
────────────
• EventBus          — central pub/sub bus connecting all services
• MarketFeedService — discovers 5m/15m markets via Gamma API, polls
                      CLOB orderbooks every 200–500ms, emits snapshots
• FeatureEngine     — maintains a 15s rolling buffer per market and
                      computes midSlope6s, retrace3s, imbalance, spread,
                      depthTop3 for both UP and DOWN sides
• SignalEngine      — hard gate checks (TTE, spread, depth, stale) then
                      weighted score (imbalance 35%, slope 35%,
                      spread 20%, retrace 10%) + trend confirmation
• ExecutionEngine   — limit-marketable FOK buy, market-sell FOK exit,
                      GTC limit-sell for TP; dry-run short-circuits
• RiskEngine        — consecutive loss cooldown, daily USDC loss cap,
                      session halt; all via explicit canTrade() gate
• PositionEngine    — per-market position state, TP/adverse/slope/time
                      exit evaluation on every snapshot tick
• StateMachine      — explicit state graph with guarded transitions:
                      IDLE → SETUP_READY → ORDER_PENDING → POSITION_OPEN
                      → REDUCE_ONLY → IDLE | COOLDOWN | HALTED
• Telemetry         — structured JSONL logger (data/oneshot_telemetry.jsonl)
                      recording decisions, orders, exits, and transitions

Runtime sequence (per market, per tick)
────────────────────────────────────────
A  Ingest snapshot (MarketFeedService)
B  Build features — rolling slope, retrace, imbalance (FeatureEngine)
C  Hard gate check — TTE [25,120]s, spread ≤ 0.02, depth ≥ minTopSize
D  Score + trend confirm → emit ENTER_LONG / ENTER_SHORT / NO_TRADE
E  Submit FOK limit-marketable at bestAsk
F  Fill handling — full fill / partial (reduce if TTE ≤ 25s) / timeout
G  Position management — TP, adverse (2-tick), slope drop (4s), time exits
H  Risk enforcement — P&L accounting, cooldown, daily halt

New scripts
───────────
  npm run oneshot      — live trading  (DRY_RUN=false)
  npm run oneshot-sim  — simulation    (DRY_RUN=true)
  npm run oneshot-dev  — sim + nodemon

New .env variables
──────────────────
  ONESHOT_ASSETS, ONESHOT_DURATION, ONESHOT_POLL_INTERVAL_MS,
  ONESHOT_BASE_RISK_USDC, ONESHOT_TP_TICKS, ONESHOT_SCORE_THRESHOLD,
  ONESHOT_MIN_TOP_SIZE, ONESHOT_MAX_CONSEC_LOSSES,
  ONESHOT_COOLDOWN_ROUNDS, ONESHOT_DAILY_LOSS_CAP, ONESHOT_FILL_TIMEOUT_MS

Co-Authored-By: direkturcrypto <direkturcrypto.x@mail3.me>
2026-02-24 13:00:19 +07:00

70 lines
2.0 KiB
JavaScript

/**
* StateMachine.js
* Explicit per-market state machine with strict transition guards.
*
* Rule: never derive state from floating booleans.
* Every state change must go through transition() to be validated and logged.
*/
import { State, TRANSITIONS } from './constants.js';
export class StateMachine {
/**
* @param {string} marketSlug - Market identifier (used in error messages and logs)
* @param {import('./EventBus.js').default} eventBus
*/
constructor(marketSlug, eventBus) {
this._state = State.IDLE;
this._slug = marketSlug;
this._eventBus = eventBus;
}
/** Current state string */
get state() {
return this._state;
}
/**
* Attempt a state transition.
* Throws if the transition is not in the allowed graph — this is intentional:
* a programming error that bypasses the guard should be loud and traceable.
*
* @param {string} nextState - One of the State enum values
* @param {string} [reason] - Human-readable reason for the transition
* @returns {StateMachine} - Returns `this` for chaining
*/
transition(nextState, reason = '') {
const allowed = TRANSITIONS[this._state] ?? [];
if (!allowed.includes(nextState)) {
throw new Error(
`[StateMachine] Invalid transition: ${this._state}${nextState}` +
` (market: ${this._slug}, reason: ${reason})`,
);
}
const from = this._state;
this._state = nextState;
this._eventBus.emit('state:transition', {
marketSlug: this._slug,
from,
to: nextState,
reason,
ts: Date.now(),
});
return this;
}
/** @param {string} state */
is(state) {
return this._state === state;
}
/** @param {string} state */
canTransitionTo(state) {
return (TRANSITIONS[this._state] ?? []).includes(state);
}
}