agentleFS
MendurimMendurim/strikegrok-trading-desk/skills/desk-autopilot/SKILL.md

desk-autopilot

Skill0 starsChanged 2 months agoMITView on GitHub

Running the desk's runbooks on a clock instead of a prompt - scripts/autopilot.py, which evaluates a frozen rule at bar close, checks the four clocks, sizes on a stressed stop, writes the proposal and its RISK PASS block, and asks the signer for exactly one bracketed order. Covers the two-OS-user split that makes unattended trading a boundary rather than a claim, the rule file format, the cron and sudoers layout, the preflight that predicts every refusal, and the staged rollout from rehearsal to testnet to a small live standing approval. Use when the user asks to let the desk run unattended, when setting up the autopilot, or when it holds or refuses.

  • Reads credentials

The file

We split this skill into the rules it gives an agent

11 rules. About 0 are common, 4 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Create both users. chown -R strike-signer the state directory.

    line 46
  2. Very specific:

    Put the API wallet in ~strike-signer/.strikegrok/api-wallet.env, chmod 600.

    line 47
  3. Very specific:

    Write the rule. Freeze RULES.md, record its sha256, backtest per desk-strategy-lab and report trials, holdout and the 5th percentile.

    line 140
  4. Very specific:

    Run monitor --dry once more against the signed register, then remove --dry.

    line 144
  5. Distinctive:

    Never run autopilot unattended where the Bots and the signer share an OS user.

    line 165
  6. Very specific:

    Never set STRIKEGROK_STATE_TRUSTED=1 to make a refusal go away.

    line 166
  7. Very specific:

    Never retry a refusal, and never edit desk_policy.py to get a trade through.

    line 167
  8. Distinctive:

    Never widen a bound in the rule file to make a held signal fire.

    line 168
  9. Distinctive:

    Never run a rule live that is not frozen, hashed, backtested and forward tested.

    line 169
  10. Very specific:

    Never let a could_not_tell pass as not_fired, in the script or in a report.

    line 170
  11. Distinctive:

    Never treat the example rule as a strategy.

    line 171

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Autopilot

desk-signal-scan and desk-standing-approvals describe a desk that scans hourly, fires on a frozen rule at 03:00 and sends inside signed bounds. They are written for agents. scripts/autopilot.py is the same two runbooks with the language model taken out of the path.

That is the whole point. A Bot deciding at 03:00 whether a rule fired is a Bot writing its own evidence. A script evaluating a frozen rule on closed bars produces the same answer twice, and the answer can be checked afterwards against the bars.

python3 scripts/autopilot.py scan                                  # hourly
python3 scripts/autopilot.py monitor --rule strategies/r/rule.json # at bar close
python3 scripts/autopilot.py sweep                                 # Tier 0 housekeeping
python3 scripts/autopilot.py monitor --rule … --dry                # rehearsal, sends nothing

The prerequisite that is not negotiable

Tier 1 refuses unless STRIKEGROK_STATE_TRUSTED=1, and that flag is the operator asserting the signer runs as its own OS user. On a shared Grok workspace the Bots and the signer are the same user, so the assertion is false and autopilot must not be run there unattended. Setting the flag to make standing approvals work hands the slot, replay and daily-loss bookkeeping back to the thing it is meant to bound.

Unattended trading therefore needs your own machine or VPS, and two accounts:

UserOwnsMay
strike-signerthe API wallet, desk/policy-state/, policy.logrun strike_request.py; nothing else runs as this user
botuserthe agents, autopilot.py, the desk directoryread everything, write proposals, signals, journals and suspensions; sign nothing

botuser reaches the exchange only through one sudoers line (template/autopilot.sudoers), and desk_policy.py runs on the signer's side of it. Everything else follows from that: a Bot can suspend a standing approval and cannot lift one; it can write a proposal and cannot approve it; it can delete a state file and gain nothing, because the layer asks the venue.

Confirm before going further:

sudo -u strike-signer python3 scripts/desk_policy.py verify    # mode: unattended

Setting it up

  1. SETUP.md steps 1-6 on the VPS, plus desk-signal-scan and desk-standing-approvals.
  2. Create both users. chown -R strike-signer the state directory; chmod 700 it. The desk directory is botuser's.
  3. Put the API wallet in ~strike-signer/.strikegrok/api-wallet.env, chmod 600. It is never in a Bot's environment, a command line or a file botuser can read.
  4. Install template/autopilot.sudoers with visudo -f /etc/sudoers.d/strikegrok.
  5. Install both crontabs from template/autopilot.crontab, one per user.
  6. Register a testnet API wallet first. --network testnet is autopilot's default; mainnet is something a rule earns.

The frozen rule

A rule is a JSON file next to its RULES.md, and the register is signed against the sha256 of that RULES.md. Every key is known and an unknown key is an error: a rule the script half-understands is a rule nobody backtested. template/rules/funding-fade-v1.json is a worked example and not a recommendation - it has no backtest behind it here.

KeyMeaning
name, versionbecome signal: <name>@<version>, which must name the register's rule and rule_version
marketsthe symbols this rule watches; the SA's markets bound them again
timeframe_seconds, klines_intervalthe bar. timeframe_seconds must equal the SA's bar_seconds
conditionfunding_pct30d: [lo, hi], price_vs_sma20: above|below, range_expansion_atr20: [lo, hi]
directionfade, follow, long, short. A fade takes the other side of whoever is paying
entrymarket only. A resting limit entry fills when the signal may no longer be true
stop_atr_multiple, take_profit_atr_multiplethe protective legs, in ATR of the rule's own timeframe
slippage_bps, fee_bps_round_turnwhat the stressed stop assumes it pays
max_spread_bpsthe liquidity clock's ceiling, tightened further by the 7-day median
funding_positionpays, earns or either; see the funding clock
sl_order_type, tp_order_typedefault stop_market / take_profit_market - market on trigger, checked against the market's own orderType list

The percentile needs history. funding_pct30d ranks the current rate inside data/funding/<symbol>.csv, which the hourly scan appends to. A rule with a 30-day percentile cannot fire until the scan has been running for ten days (240 samples, min_funding_samples). That is not a bug to route around: it is the rule's own condition being measurable.

There is no backfill, and there cannot be an honest one. Strike publishes no historical funding series: /v2/klines offers last, mark and index and no premium, there is no fundingRate history endpoint, and the account-scoped /v2/history/funding is one account's charges on markets it happened to hold. Funding cannot be reconstructed from the premium either - the published rate does not follow from the instantaneous premium, and the averagePremiumIndex that drives it is accumulated inside the interval and never exposed historically. A reconstructed series would be a guess in the shape of an observation, and the percentile would be wrong without anyone being able to see that it was.

So the history is collected, not recovered:

python3 scripts/funding_collect.py --desk /workspace/trading-desk --rule rules/R.json
python3 scripts/funding_collect.py --desk /workspace/trading-desk --rule rules/R.json --status

--status reports samples against the threshold and when the rule becomes measurable. The collector keeps one sample per funding interval, so running it more often than hourly cannot inflate the count. autopilot.py scan holds the same cadence for the two series a window statistic reads - funding and depth - so a scan on a tighter cron no longer shortens a thirty-day percentile or a seven-day median without saying so. Open interest is unaffected: that check looks for a row twenty-two to twenty-six hours old rather than counting samples, so a finer series only sharpens it.

The four clocks, in code

An item is actionable only when all four pass, and the ticket carries all four readings.

ClockPasses when
rulethe condition is true on the last closed bar, and the fire is younger than one bar or one hour, whichever is tighter
liquiditydepth within 10 bps on the side the entry takes is at least 5x the ticket notional, the spread is inside the rule's ceiling and its own 7-day median, and the book reaches 25 bps
fundingthe position receives the next charge, or the rule earns the right to pay it, or the charge is more than ten minutes away
catalystdesk/blackouts.json has no window in force on this market and none opening inside the hour

A short is measured against the bids, because that is the side a market sell takes. Measuring the other one passes a book that cannot fill the ticket at all.

Sizing

The Risk Manager's arithmetic, written out on every ticket:

risk        equity x the tighter of SA risk_per_trade and the 0.5% ceiling
stop        entry +/- stop_atr_multiple x ATR, rounded onto the tick AWAY from the entry
stressed    stop +/- slippage_bps of entry
per unit    |entry - stressed stop| + (entry + stressed stop) x half the round-turn fee
size        risk / per unit, rounded DOWN onto the step

The step comes from MARKET_LOT_SIZE where the venue publishes one, because a market entry is bounded by that filter and not by LOT_SIZE. Sizing on the stressed stop is why the nominal loss at the stop is always a little under the stated risk_usd; the policy layer refuses a ticket where it is above.

What it writes, and what it cannot

PathWhat
signals/YYYY-MM-DD.mdSCAN, RULE FIRED, RULE STALE, WATCH COULD NOT TELL, append-only
proposals/SG-…mdthe ticket and its RISK | … | PASS block
proposals/SG-…-entry.jsonthe exact request body. The file is the preview: the bytes sent are these bytes
journal/YYYY-MM-DD.mdevery send, hold, defer, cancel and reconciliation
journal/incidents/open/refusals and unknown results
watch/rule-<name>/logfired, not_fired, could_not_tell with the arithmetic, every bar
watch/autopilot/state.jsonfired bars, the pacing mark, open tickets. Bot-owned, and therefore never a control
desk/standing-approvals.suspended.jsonsuspensions, which only ever remove permission

It cannot write desk/policy-state/, the register, or an approval token. The test suite asserts it never even creates the state directory.

Exit codes

CodeMeaningWhat to do
0done, or nothing to donothing
1an operating error - a bad rule file, an unreadable deskfix it; nothing was sent
2held: a clock failed, a bound refused, a read was blindread the journal line. This is the normal quiet outcome
3refused by the policy layeran incident is filed and nothing is retried

A refusal is a defect in autopilot's preflight, not an obstacle. The preflight checks every bound the gate checks - incidents, suspensions, the rule hash, notional, risk, pace, the book, the SA's occupied markets, the venue's own filters - so a refusal means the two disagree. The refusal names the bound; that name is authoritative. Fix the preflight, then clear the incident with a re-signed register.

Earning the standing approval

The SA is earned in this order, and skipping a step is how a backtest becomes a loss.

  1. Write the rule. Freeze RULES.md, record its sha256, backtest per desk-strategy-lab and report trials, holdout and the 5th percentile.
  2. Rehearse: monitor --rule … --dry on testnet. It writes the proposal, the PASS block and the request body and stops. Check the body against the ticket by hand, then run desk_policy.py explain <body>.json --path /v2/order/strategy --equity <n> - that is the real gate answering.
  3. Forward test at Tier 2, minimum size, every ticket signed by you, until the Trade Reviewer has 20 closed trades with live expectancy above the backtest's 5th percentile.
  4. Sign the register on your own machine. First bounds are small: 0.25% risk, one or two markets, max_open 1 or 2, thirty days. template/standing-approvals.example.json is the shape.
  5. Run monitor --dry once more against the signed register, then remove --dry.
  6. Widen only on the Reviewer's record, in writing, dated, through the same route.

Leverage is set out of band, once per market, before the SA goes live. Autopilot never changes it: a leverage change is a gated write like any other, and one sent inside a minute of an entry would hit the 60-second pace floor and refuse the entry.

How it stops

ConditionWhat stops
daily loss against the day's opening equityopening exposure, at the gate. Autopilot holds before it
three consecutive losses on the marketthe SA, suspended by the policy layer from live fills
RULES.md hash changedthe SA. Autopilot holds first rather than spending the approval on a changed rule
an open incidentevery opening order, until a re-signed register clears it
a monitor blind for two barsthe SA, suspended by autopilot itself
the equity snapshot older than ten minutessizing, so nothing opens
the register expired, or its signature invalidTier 1

Exits and protection keep working throughout: reduce-only orders and scoped cancels are Tier 0 and need no approval. A desk that cannot open is not a desk that cannot close.

Never

  • Never run autopilot unattended where the Bots and the signer share an OS user.
  • Never set STRIKEGROK_STATE_TRUSTED=1 to make a refusal go away.
  • Never retry a refusal, and never edit desk_policy.py to get a trade through.
  • Never widen a bound in the rule file to make a held signal fire. The hold is the desk working.
  • Never run a rule live that is not frozen, hashed, backtested and forward tested.
  • Never let a could_not_tell pass as not_fired, in the script or in a report.
  • Never treat the example rule as a strategy. It is a format.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.