--- name: desk-strategy-research description: How the agents author, test, review and promote their own strategies on the autonomous desk - the strategy folder contract (RULES.md, params.json, review.json), the shared factor engine and its parameter space, the research loop the Strategist runs each day, the deterministic strategy gate (scripts/strategy_review.py and template/strategy-criteria.json), the long-history and wide-universe checks, the correlation report that says whether two strategies are one bet, the shipped hg-carry and hg-breakout drafts, the promotion ladder draft to paper to live and who may move each rung, multi-strategy budgets, and the anti-overfitting rules. Use when the Strategist proposes, revises, reviews, retires or compares strategies, or when the user asks what the desk is running and why. license: MIT metadata: version: "1.1.0" author: SWC Studio category: desk --- # Strategy research The agents may invent strategies. What they may not do is decide alone that a strategy trades real money. The design that squares those: a strategy is a folder of files, a deterministic gate reviews the files against written criteria on real history and signs the result, the engine runs only signed strategies, and the one word `live` is the user's to write. Everything else is the agents' to do, and the loop below is how they do it well. ## 1. The strategy folder ``` strategies// RULES.md frontmatter: name, status (draft | paper | live), share (0-1), allowed_markets (optional list); body: the rules in prose params.json parameters for the shared engine (any key in engine.signals.DEFAULT_PARAMS; unspecified keys use the defaults) review.json written by strategy_review.py; signed over RULES.md and params.json bytes; eligible or rejected paper-record.json written by paper_record.py; signed over the review's digest; ready or not-yet BACKTEST.md optional: the Strategist's own record of what was tried and why ``` The engine runs every folder whose status is `paper` (testnet only) or `live` (any network) **and** whose `review.json` verifies: same bytes, signed with this desk's gate key, verdict `eligible`, younger than 14 days. Anything else is treated as draft and the reason is logged once (`engine.log`, `engine-heartbeat.json` under `skipped`). Editing `RULES.md` or `params.json` voids the review by construction; that is the point. `share` splits the policy's `max_positions` among runnable strategies (`engine.strategies.allocate_slots`); `allowed_markets` narrows a strategy to a subset of the engine universe. Positions are attributed to the strategy that opened them; a strategy never exits another's position, and a coin held by one strategy is not entered by another. ## 2. The parameter space Every strategy is the same factor engine with different parameters, which is what makes the review honest: the code path is identical to the shipped `hg-systematic`, so a review compares like with like. The knobs, all in `params.json`: | Group | Keys | | --- | --- | | signal horizons | `mom_fast_hours`, `mom_slow_hours`, `mr_ema_hours`, `atr_hours`, `vol_span_hours`, `carry_lookback_hours`, `breakout_hours`, `oi_change_hours` | | combination | `weights` (momentum, carry, reversion, breakout, oi_change; renormalised by the review perturbations; `oi_change` is renormalised away for a coin without enough OI history in `data/oi-.jsonl`) | | execution | `execution_style`, `maker_offset_bps`, `maker_wait_s` (starting values; `scripts/execution_stats.py` may override the last two per market on new tickets without touching the file) | | trading rules | `entry_threshold`, `exit_threshold`, `min_hold_hours`, `max_hold_hours`, `decision_interval_hours`, `stop_atr_multiple` | | sizing and regime | `target_daily_vol_pct`, `regime_vol_percentile_cut`, `regime_trend_hours`, `min_bars` | A strategy that needs a factor the engine does not compute is a proposal for an engine change, which is a pull request to `engine/signals.py` with tests, not a strategy folder. Say so rather than faking the factor in prose. Two folders beside `hg-systematic` ship in `template/strategies/` as starting points, both `status: draft` with `share: 0.5` and no review: `hg-carry` (carry 0.7, momentum 0.3, carry lookback 168 h, an 8-hour clock, holds of 48 to 240 h, a 3.5 ATR stop) and `hg-breakout` (breakout 0.7, momentum 0.3, `breakout_hours` 168, a 4-hour clock, holds of 24 to 120 h, a 2.5 ATR stop, maker offset 0.5 bps and wait 30 s). Each names in its own hypothesis what would falsify it, including a daily-return correlation with `hg-systematic` above 0.7. They are hypotheses with a folder, not records: review them on your own desk before setting `paper`, and treat their `BACKTEST.md` as the place that review is written up. **Is it a second bet?** `python3 /workspace/hypergrok/scripts/strategy_correlation.py --desk-root ` reads the daily equity curve every review stores under `curve`, prints the pairwise daily-return correlation between reviewed strategies and the combined equal-weight curve's return and drawdown beside each strategy alone. A pair above 0.7 is one bet with two names, and splitting slots between them diversifies nothing; a pair near zero is what `share` is for. Run it before any promotion proposal and before any change to a `share`; the report reads only. ## 3. The research loop (Strategist, once a day, in the supervising cycle) 1. **Hypothesis.** One sentence, written before any number is looked at: what inefficiency, why it should persist, what would falsify it. Into `strategies//RULES.md` under `## hypothesis`. 2. **Rules.** The parameter choices that express it, with the reason for each, into `params.json` and the RULES body. No parameter is chosen by looking at the backtest first. 3. **Review.** `python3 /workspace/hypergrok/scripts/strategy_review.py --desk-root --strategy `. Read the whole output: the distribution, the split, every perturbation row, every check, starting with `history-complete`. A `history-complete` FAIL means a coin could not be read; rerun the review, do not reason about the partial result. Write the universe count into every review summary: two runs with different universes are not comparable, and the same strategy has scored +18.6% on 15 coins and +2.7% on 12 coins an hour apart when a 429 dropped ETH from one of them. 4. **Decide.** Eligible and the perturbation rows look like one strategy rather than a lucky corner: set `status: paper` (the Strategist may write that word) and note the review time in the journal. Rejected: write what failed and why into `BACKTEST.md`, and either revise the hypothesis (a new folder or a new version, never a silent edit that keeps the old name) or retire it. 5. **Watch.** A paper strategy runs on testnet under the engine's supervision for at least a week. The Trade Reviewer grades its trades on process; the Strategist runs `python3 /workspace/hypergrok/scripts/paper_record.py --desk-root --strategy `, which compares the realised testnet trades with the backtest distribution (win rate, average hold, stop rate, drawdown) against the `paper` block of the criteria and writes a signed `paper-record.json`. Divergence is a finding, and `not-yet` says which check needs more trades or days. 6. **Propose.** When the paper record says `ready`, the Strategist runs `strategy_correlation.py` and writes a promotion proposal in the journal: hypothesis, review summary (including the `long-history` and `wide-universe` lines), the paper record's numbers, the correlation with every other reviewed strategy, what could go wrong, and the exact one-line edit the user would make. The user makes it, or does not. The engine refuses `status: live` without a `ready` paper record that matches the current review, so a proposal without one is premature. Budget: at most three new strategy folders per week, at most one review rerun per folder per day, and no review of a folder whose parameters differ from a rejected one by a single knob nudged toward the data. The review script has no memory of that; the Strategist's journal is where it is enforced and the Trade Reviewer checks it. ## 4. The strategy gate `template/strategy-criteria.json` is the user's, like `autonomy.json`; the defaults: | Criterion | Default | | --- | --- | | history, universe | 5000 hourly bars (about 208 days), top 20 by open interest | | minimum trades and days | 40 trades, 120 days | | profit factor | at least 1.1 | | maximum drawdown | no worse than -15% | | fee load | fees at most 5% of starting equity | | out of sample | second 40% of the period returns above 0% | | robustness | weights tilted by plus and minus 20%, entry threshold shifted by plus and minus 0.25, stop multiple scaled by 0.75 and 1.25: the worst variant loses no more than 10% and at least half are positive | | recent window | the last 30 days (`recent_window_days`) return no worse than -5% (`recent_min_return_pct`), so a strategy whose last month looks nothing like its history fails even when the whole period passes | | `long_history` block | the same rules on 4-hour bars (`bar_hours`), 5000 bars (about 833 days, back to 2024-05): return at least 0% (`min_return_pct`) and drawdown no worse than -25% (`max_drawdown_pct`); printed as `long-history` with its days, coins and trade count | | `universe_floors` block | the liquidity floors the review's base and long-history universes are ranked under: `min_day_volume_usd` 5,000,000 and `min_open_interest_usd` 10,000,000, ranked by open interest. They are lower than the live policy's $20M volume floor on purpose: that floor moves with the hour of the week (a Monday-morning run kept 15 coins where a weekday afternoon keeps 20 or more), and a review needs the same universe from one run to the next | | `wide_universe` block | the same rules on the top 40 by open interest (`top`) over the hourly window, under its own `min_day_volume_usd` and `min_open_interest_usd` (the same two values by default): return at least 0% (`min_return_pct`); printed as `wide-universe`, so a result that depends on the exact twenty coins of the day is visible. When the wide floors produce no more coins than the base run it prints "not wider than the base run ... no separate evidence" and passes without counting as evidence | | `history-complete` | the first check printed, with no knob: the review records every coin the exchange would not serve (`summary.skipped_coins`) and fails when any coin in the ranked universe could not be read, because a review on a partial universe is not the review the criteria describe | | `paper` block | the paper-record gate: at least 20 closed testnet trades over 7 days; win rate within 15 points of the backtest, average hold within 50%, stop rate within 20 points, paper drawdown no worse than 1.5 times the backtest's | Passing means the folder met the written bar on that history. It is not a forecast, it does not choose the status, and a shipped example that passes (`hg-systematic` did: worst perturbation -4.85%, 83% positive; on the 4-hour, 833-day window +13.47%, Sharpe 0.83, maxDD -7.06%, +8.36% in-sample and +4.72% out-of-sample) still shows how fragile a record is, whatever its length. The review also stores the backtest's daily curve, which is what `strategy_correlation.py` compares. The result is sensitive to the universe as much as to the parameters: on 2026-09-07 the same code over the same 208 days returned +15.51% on 20 coins and +4.50% on the 15 coins the live policy floors kept, which is why the criteria pin `universe_floors` and why a review states its coin count. Tighten the criteria before loosening them; a criteria file looser than the defaults is a decision the user records in `desk.md`. ## 5. Promotion ladder | Rung | Who writes it | What it needs | | --- | --- | --- | | `draft` | anyone | nothing; the engine ignores it | | `paper` | Strategist or user | an eligible, fresh, signed review; policy mode testnet | | `live` | the user only | the same review, a `ready` paper record written by `paper_record.py` for that review, and the edit made by hand; the engine refuses `live` without the record | The Claude Code guard denies any agent tool writing `status: live` and any edit to a live strategy's `RULES.md` or `params.json`; Grok Build's deny rules and the Grok Bot Require Approval rule should be set the same way (`desk-multi-runtime`). The review file and the paper record are never hand-written: only the scripts write them, and a forged one fails the signature. **Re-review and demotion.** `scripts/strategy_rereview.py` reruns the review for every `paper` and `live` strategy on fresh history, weekly from the user's cron (`deploy/supervisor-cron.example`). A rejected review, including one that fails only the recent window, demotes the strategy one rung: `live` to `paper`, `paper` to `draft`, written into `RULES.md` by the script with an event in `autonomy/events.jsonl` and an alert. It is the only path that moves a status downward on its own; nothing moves one upward, and a review that passes keeps the strategy where it is. The Strategist treats a demotion as a finding to explain, not a verdict to appeal by rerunning the review (`desk-operations`). ## 6. Retiring and versioning A strategy is retired by setting `status: draft` (the user for live, the Strategist for paper) and writing why in `BACKTEST.md`. It is versioned by a new folder name (`-v2`) with the previous folder left intact, so the journal's references keep pointing at what actually ran. Never delete a folder that ever had a review. ## Never - Never set `status: live`, edit a live strategy, or hand-write `review.json` or `paper-record.json`. - Never choose parameters after looking at their backtest and call the result a hypothesis. - Never rerun a review to get a different verdict without changing the hypothesis, and never split one idea across folders to pass the weekly budget. - Never quote a review as expected return; quote the drawdown, the trade count and the worst perturbation with it. - Never run a strategy whose review the engine reports as voided; find out what changed. - Never propose raising a `share`, or call two strategies diversification, without the correlation report; a pair above 0.7 is one bet.