Feature-rule runner
featurerule (strategies/featurerule, GLE-1307) runs a frozen Feature Analyser rule. AlgoChart exports the rule as a draft. The runner trades the draft through the engine’s own orders, fills, account and export, and writes a trace that the app checks against its own research. It has two inputs, both paths: Draft, the draft file, and Trace, where the trace is written. The rule itself is not an input, so a --config sweep cannot refit it.
bin/algo run --adapter bin/marketfeed-adapter --adapter-inputs Dir=/usr/local/marketfeed \
--strategy bin/featurerule --inputs Draft=/abs/draft.json,Trace=/abs/trace.jsonl \
--symbols fut:XCEC:HG:cont:adj=none --interval 1h \
--start 2021-10-31T00:00:00Z --end 2026-10-31T00:00:00Z \
--commission-per-share 2.50 --slippage-ticks 1 --export /abs/run.algorun
bin/featurerule --capabilities--capabilities prints algolang.feature-rule-capabilities/1: the runner’s version (1.0.0), its build, and every format, feature, operator, exit and contract policy it honours. The app refuses a handoff that needs anything the list leaves out.
The draft
A draft is algolang.feature-rule/1 JSON. The runner validates all of it at init and refuses, with a stable code, anything it cannot honour exactly. It never approximates, rounds or drops a clause. Unknown keys are refused everywhere except inside lineage, which is free-form and copied into the trace header. The draft’s digest is the SHA-256 of the file’s bytes as written. The app writes its canonical sorted-key encoding, and the header’s draftSortedKeys says whether the file was in that form.
The feature program:
price: the bar’sopen,high,loworclose. Onadj=nonethis is the raw served series.rsi: nseriesSeries.RSI(length)(Wilder’s, seeded with the mean of the firstlengthchanges) over the lastwarmupbars, the current bar included, on a price feature. The value is traced from thelength+1th bar. The rule waits forminBarsknown bars.atr: the average true range over the lastwarmupbars. Seedsmais the mean of the lastlengthtrue ranges, which is what the research’s ATR(14) is. Seedwilderis TA-Lib’s recursion. ATR is for scaling only, withrole: "scale"; a rule that reads it is refused.hour_of_day(clockbar_end): the wall-clock hour of the bar END in the draft’s IANA zone, DST applied. The 15:00 to 16:00 bar reads 16.day_of_week(clocktrade_date): 0 Monday to 6 Sunday, of the bar’s exchange trade date. The trade date is the session whose span holds the bar’s close less 1 ns, from the exchange calendar. The runner asks for the calendar only when the draft has this feature. A bar on no trade date isskipped_data.
A price, rsi or atr feature may name the price frame it reads, with frame: none, panama or ratio (GLE-1308). Without frame it reads the signal series’ own frame. That frame comes from the symbol:
- A bare root (
fut:XCEC:HG) or a bare:contispanama. :cont:adj=XisX.- A dated contract, or a symbol with no adjusted view, is
none.
v1 honours a frame only when it is the series’ own, and capabilities say so with "frames": ["series"]. Any other frame is refused with frame_unsupported, for example “rsi14 reads panama; the signal series is none”. The trace header records the series’ frame.
The rule is a conjunction (all) of comparisons, each a feature against a float64 constant at full precision: <, <=, >, >=, and == or != on the two integer features only. any, or and not are refused.
The policy (v1):
- One series, which is also the traded market.
- A fixed side, long or short, quantity 1.
- One position, no pyramiding. A signal while an order is working or a position is open is ignored and traced
ignored_busy. - Entry
next_open: a Day market order at the signal bar’s close, filled at the next bar’s open. - Exit
bars, on one of two clocks:- Clock
signal: the exit bar is E = S + bars, with S the signal bar. An entry still working when its next fill would be on E or later is cancelled, tracedfill_after_exit, rather than extending the trade.barsmust be at least 2. A signal on E itself is ignored, because the exit order is working at that close. - Clock
fill: E = F + bars - 1, with F the bar the entry filled on.
- Clock
- Exit price:
bar_close: a market-on-close order settled at E’s close. This is a simulation, and the runner logs a one-line warning that no closing auction prices the market there.next_open: a market order at E’s close, filled at E+1’s open.
- Bars are served bars, so a Friday signal holds over the weekend.
Costs are declared per side, with the instrument’s tick size, point value and currency. Zero costs need explicitZero: true. Every fill’s commission and slippage are checked against the draft’s. A mismatch fails the run with costs_mismatch, naming the --commission-per-share and --slippage-ticks flags to set.
The runner also checks the run. The draft’s symbol must be a run symbol. The run’s bars must lie within the evaluation window; the engine’s Init window is the bars served, not --start and --end. At the first bar the instrument the engine resolved must match the draft’s tick size, point value, currency and exchange clock, and the account must start with the draft’s capital.
Contracts and the engine’s limits
The draft schema’s hold_entered_until_exit is refused. On a continuous future the engine rolls an open position at the served cut, with two market legs, each charged commission and slippage. A strategy cannot read the roll schedule in advance, so the runner can neither hold the entered contract nor skip an entry whose exit would fall after the roll. It honours roll_at_served_cut/1 instead, and traces every roll: whether a position was open, whether an order was working, the leg count, and the raw gap.
The roll legs’ fills are the engine’s and never reach the strategy. They are in the export, not the trace. The trace’s trade statistics therefore cover only the trades that crossed no roll; a trade held over a roll is counted crossedRoll and its P&L is the export’s. skipped_roll is never written.
The trace
algolang.feature-rule-trace/1 is JSON Lines in event order, which is time order. Times are unix seconds of the bar END. The lines are:
header: the runner, version and build, and the draft’s digest, id and revision. Also the series, the zone, both tz database versions, the lifetime, the Init window, the lookback, the contract policy, and the lineage and limitations.refused: the code and reason, when the run is refused. It is the last line.instrument, andcalendarwhenday_of_weekis used: what the engine resolved.bar: one per bar, with every feature’s value (nullwhen unknown), the signal, the action, the dated contract and the bars known. The action is one ofnone,warmup,submitted,ignored_busyorskipped_data.order,fillandcancel. A fill records its price, contract, commission and slippage, the bar it priced on and its reference price (that bar’s raw open, or the close for market-on-close).roll.summary: the counts and the trade statistics.
The --export file is the engine’s ordinary export, roll legs included.
Tests
go test ./strategies/featurerule/ runs:
- The copper parity check against the research’s fixtures in
testdata/(sha256s checked on read;testdata/gen_fixtures.pyregenerates them). RSI(14) matches within 1e-9 from bar 2,000, the Chicago hour on every bar, and the 793 reference matches exactly. - A replay of the holdout through the policy, which reproduces the research’s per-trade prices and mean net.
- Policy tests on crafted bars with a fake engine.
- Draft refusal tests, one per refusal.
cf21conformance through the engine and the file-csv adapter under both lifetimes. Two drafts reproduce their frozen blotters (FEATURERULE_FREEZE_CF21=1rewrites them), and their traces reconcile with the blotters fill for fill.