johnamcruz

strategy-authoring

Write a new trading strategy as a STRATEGY.md document - frontmatter schema, required sections, validation, and promotion from paper to active. Use when the user describes a strategy idea, ports one from code or a backtest, or edits an existing strategy.

johnamcruz 1 Updated 1w ago
GitHub

Install

npx skillscat add johnamcruz/futures-trading-harness/strategy-authoring

Install via the SkillsCat registry.

SKILL.md

Strategy Authoring

When to Use

  • "Add a strategy that ..." or porting a strategy from code or a backtest.
  • Changing an existing strategy's rules or status.

How It Works

  1. Copy strategies/_template/STRATEGY.md to strategies/<name>/STRATEGY.md
    (or to a folder in FTH_STRATEGIES_DIRS for private strategies). <name>
    is lowercase with _ or -; it becomes the journal tag setup:<name>.

  2. Frontmatter (checked by code):

    Field Meaning
    name, description Folder name; what/when, ≥ 40 chars
    status paper (new), active (live entries allowed), disabled
    instruments Contract roots, e.g. [MNQ, MES]. A rules strategy trades one leg of each micro/mini family it lists (MNQ/NQ, MES/ES, MYM/YM, M2K/RTY, MGC/GC, SIL/SI): the selected one (the runner's symbols, or FTH_SYMBOLS), never the other, so list one leg per family (listing MNQ covers a selected NQ). With no leg selected, exactly the roots listed (listing both legs of a family, neither until one is selected). The order gate, order_plan, the scan, and the backtester (its symbols) all match this way. The contract ordered sets the tick specs and the dollars (max_risk_usd on an NQ entry is measured at NQ's $5.00 a tick). manual and policy strategies trade exactly the roots listed
    timeframe Trigger timeframe, e.g. 3m
    sessions Optional windows, entries only inside: named sessions asia (18:00-03:00 ET), london (03:00-09:30 ET), ny (09:30-16:00 ET), or "HH:MM-HH:MM@Zone". Without it the strategy trades the whole market session, from 18:00 ET until 16:00 ET the next day (closed only 16:00-18:00 ET and weekends; no position is carried past 16:00 ET)
    signal rules (trigger written in rules, checked by code) or manual (agents judge the body). Every shipped single-setup strategy, the ported ones included, is rules; the prop strategies (prop_*) are policy (below)
    connectors Optional data the strategy needs beyond bars, like a skill's tools: order_flow (aggressor buy/sell volume per bar, for ofi/delta, from recorded flow files; without them a bar-shape estimate). Required when the rules use that data
    regimes Optional list of regimes the strategy fits: trend-up, trend-down, trend, range, transition, high-vol, normal-vol, low-vol (any match fits). Out-of-regime strategies are never scan candidates
    mtf trend (the default) or reversal. A trend strategy never enters against the prevailing higher-timeframe trend (the highest of 4h, 1h, 15m with one): the scan drops the signal, the backtester skips it, and the order gate refuses the entry. Set reversal only when the setup is a raid or exhaustion at a higher-timeframe level that fades that trend by design (crt_1h, crt_4h, cisd_ote, ofi_absorption, value_area_reentry); say why in the body. Policy strategies have no mtf: each setup keeps its strategy's
    regime_gate Optional true: the MCP gateway also refuses entries when the live regime (from that strategy's timeframe bars) doesn't fit
    rules With signal: rules: long: and/or short: lists of conditions, all of which must hold on the closed bar
    params Optional overrides of the snapshot and rules series (e.g. orbMinutes: 30); periods must be whole numbers
    filters Optional adx_min, adx_max, adx_slope_min, max_vwap_distance_atr; checked by the scan only, not at order time
    exit Optional: trail_activate_r + trail_giveback_r (trail the stop from +NR, giving back MR; trend setups use 2 / 0.5), target_r (fixed target), target (a distance expression from the signal close, or { long, short }, e.g. crt_target(60): a target at a level instead of R), max_bars (time stop). Without it the target is risk.min_rr. The backtester and the live runner apply it
    signal: policy A policy strategy: the prop challenge as a strategy (see strategies/prop_portfolio). strategies lists the rules strategies whose setups it trades, in priority order, all on its timeframe; account names accounts/<name>/ACCOUNT.md (the gate enforces its floor, daily limits, and size budget); sizing sets risk per trade from the cushion (cushion_frac ≤ 1, cap_usd, clock_k, r_per_session, min_size_guard, drawdown_halve_usd: halve the budget while the balance is at least that far below its peak, stop_slippage_ticks: the stop's expected slippage counted in each contract's risk, at least 1, the default and the backtester's fill slippage); contracts is micro, mini, or auto (minis once the size reaches one mini); exit needs a trail (it exits every trade the policy strategy takes); risk.stop: strategy keeps each setup's own stop. Each setup is taken at the largest size the budget allows, or skipped. Only policy strategies have these keys
    risk stop (atr:<k> = k × ATR(20), a distance expression such as 0.5 * atr(20) or cisd_ote_risk, a map { long: <expr>, short: <expr> } when each side's stop sits somewhere else (beyond a swept low or high, as in crt_1h), structure, swing, manual), min_rr, optional max_risk_usd. The order gate checks that a stop exists and, when max_risk_usd is set, refuses an entry whose risk (size × stop ticks × the broker's tick value) is over it, or whose risk it can't compute (a market entry with its stop only in the rationale: give stopLossBracket.ticks); min_rr is applied by the agents (risk-manager), not by code. In a policy strategy, only its own max_risk_usd applies to its entries, not its components'
    source, version Where it came from; bump version on rule changes
  3. Body (read by the agents): ## When to Use, ## How It Works (context
    filter, trigger, entry/stop/targets, skip when), ## Examples with
    tick-correct numbers and a sample rationale.

  4. Port faithfully. When porting code, copy the exact rules and parameters and
    flag suspected bugs to the user instead of silently fixing them.

  5. Validate: the harness tool strategies_validate (a person runs the
    strategies script's validate).

  6. Promote only on evidence: paper-trade it (reviews tagged paper), run
    setup-expectancy, and set status: active only with the user's approval.

  7. Write mechanical triggers as rules, not code. Every item in a side
    must hold (AND); an item can be a group, any: [...] (one of them holds,
    OR) or all: [...] (every one), nested three deep, so one strategy can take
    two setups or two confirmations (see value_area: a POC rejection or
    breakout, confirmed by order flow or expansion). The scan shows each
    group's parts, so you can see which branch held. A condition is
    <expr> <op> <expr> with >, >=, <, <=, crosses_above,
    crosses_below. Expressions use series and numbers joined by +, -, and
    number *: open high low close volume, ema(n) sma(n) atr(n) adx(n) highest(n) lowest(n) (n up to 500; adx up to 250), order flow
    ofi(n) delta(n) vol_sma(n), supertrend supertrend_dir, keltner_upper/mid/lower,
    vwap_session vwap_rth or_high or_low swing_high swing_low,
    prior_high prior_low prior_close overnight_high overnight_low, rth_open
    (today's 09:30 ET open, until 16:00 ET), rth_high rth_low (today's RTH
    high and low so far), ib_high ib_low (today's initial balance, the
    first hour, from the bar that completes it), adr(n) (the average RTH
    range of the last n complete days), minute_et,
    cisd_ote_dir cisd_ote_risk (the original bot's CISD + OTE detector), and
    higher-timeframe candles htf_open(m) htf_high(m) htf_low(m) htf_close(m)
    (the previous m-minute candle) and htfc_open(m) htfc_high(m) htfc_low(m)
    (the one in progress, up to this bar); m divides a day (60 = 1 hour, 240 =
    4 hours, candles aligned to the 18:00 ET open: 18, 22, 02, 06, 10, 14 ET).
    htfc_low(60) < htf_low(60) says this hour swept the last hour's low.
    crt_dir(m) crt_risk(m) crt_target(m) are the Candle Range Theory sweep
    detector on m-minute candles (one setup per candle, tuned by the
    crt* params; see crt_1h). mtf_bias(m) is the m-minute trend as of
    the last m-minute candle completed before the bar (1 up, -1 down, 0 range;
    the multi-timeframe-analysis skill): mtf_bias(240) >= 0 keeps a long
    out of a 4-hour downtrend.
    Volume profile (bar-based; the vwap-volume-profile skill): prior_poc prior_vah prior_val (the last complete RTH day, fixed for the day),
    session_poc session_vah session_val (this Globex session, developing),
    vp_poc(n) vp_vah(n) vp_val(n) (the last n bars), and the nearest node
    above or below the close, prior_hvn_above prior_lvn_below (and
    _hvn_below, _lvn_above, session_*, hvn_above(n) ...). Use nodes
    as targets and distances, not crosses: they move as price passes them.
    Tune with params vpRows (default 100), vpRowSize (points per row
    instead), vpValueArea (70), vpNodePct (9), vpTroughPct (7),
    vpThreshold (1).
    [n] looks back n bars: highest(20)[1] is the 20-bar high before this
    bar (without it the current bar is included, so a close can never cross
    above it). minute_et is the bar's open time in New York minutes (9:45 =
    585). prior_* is the last completed RTH day and overnight_* this Globex
    session before 9:30 ET up to the previous bar, as each bar saw them; a
    session the bars start mid-way through has none (likewise vwap_session
    and vwap_rth). The runner scans three trading days of bars, enough
    for these and for long indicators to match a backtest. ofi(n) is order-flow imbalance
    over n bars, from -1 (all selling) to +1 (all buying): real buy and sell
    volume from recorded flow files (<FTH_HOME>/flow/), or, for a bar without it,
    volume signed by where the bar closed in its range. delta(n) is that signed volume
    summed, vol_sma(n) the average volume per bar. A value
    that doesn't exist yet (indicator warm-up, no opening range yet) makes the
    condition false and the scan marks it missing. At most 12 conditions per
    side. A rules strategy with only long rules can't be used to sell into an
    entry. validate reports typos, unknown keys and series. Only a pattern the
    rules can't express (multi-bar zone logic like cisd_ote) needs manual or
    a new series in scripts/lib/trading/rules.js (with tests), used from the
    rules like cisd_ote_dir.

  8. Backtest it before paper trading (the user, or the strategy-researcher
    role in an interactive authoring session; never a trading run):
    node <root>/scripts/backtest.js --data <bars.parquet|.xlsx|.csv> --symbol MNQ --strategy <name>
    (docs/BACKTESTING.md). Check that it fires where you expect (trades.csv)
    and that its R statistics hold up with harness rules on.

Examples

---
name: donchian_break
description: 20-bar Donchian breakout on MNQ/MES 3-minute bars with an ADX trend filter, written entirely as rules.
version: 1
status: paper
instruments: [MNQ, MES]
timeframe: 3m
sessions: ["09:45-15:00@America/New_York"]
signal: rules
rules:
  long:
    - close crosses_above highest(20)[1]
    - adx(14) >= 20
    - close > vwap_rth
  short:
    - close crosses_below lowest(20)[1]
    - adx(14) >= 20
    - close < vwap_rth
risk:
  stop: atr:1
  min_rr: 2
source: user idea, 2026-10-08
---

See strategies/vwap_reclaim/STRATEGY.md for a complete rules strategy.

Categories