feat(library): twelve v1-native strategies, and a checker that understands them - #30
Merged
Conversation
…ontrols The library was entirely baseline: every entry compared a host-supplied signal name with a numeric literal, which is what keeps those fixtures on byte-stable v0.1.0 IR. Nothing in it exercised the language Nano actually ships. These twelve declare their own `input`s and let Nano compute the indicators, so the host contract collapses from "publish DONCHIAN_POS" to "publish OHLCV". The v1 features here are load-bearing rather than demonstrative. `line[1] <= 0` turns a level test into an edge test and is the whole difference between firing once per regime change and firing every bar of a trend. `HIGHEST(high, 20)[1]` is correctness, not style: the unshifted channel includes the current bar, so `close > HIGHEST(high, 20)` can never be true. `-entry_z` needs unary minus, which baseline IR cannot carry at all, which is why the older entries ship pre-negated feed signals instead. Test harness: the parametrised conformance tests now partition on the pinned fixture's `nanoIrVersion` rather than assuming one shape. Baseline entries keep the StrategyGraph round-trip and gain an explicit guard that they still compile to 0.1.0; v1 entries get the NanoModule round-trip, a source-hash/module-hash check that catches a fixture drifting from its own source text, and VM replay determinism. No baseline fixture is touched. Every no-fire assertion is paired with a positive control on the same strategy, per 811d6c7 — and several pairs are the discriminating claim itself: the same z-score flush in an uptrend and a downtrend, the same climax-sized bar closing at its high and at its low, the same gap held and given back.
…unts A term-level mutation sweep — drop one conjunct of one rule, run that rule's test, restore — found ten of twenty-six terms unfalsifiable. Two of them were unfalsifiable because they were genuinely redundant, and those are now gone rather than tested: * `ema_pullback_continuation` carried `fast_ma > slow_ma` beside a band test requiring `slow_ma < close <= fast_ma`. The band implies the ordering, so no frame could ever separate them. The rule is now the band alone and the header says why. * `macd_zero_line_reclaim` carried `MACD_HIST > 0`. The line crosses up through zero only after a negative stretch, when its signal EMA is still below it, so a positive histogram is implied by the crossing rather than confirming it. The other eight got the frame that isolates them: a second bar still outside the Bollinger band, a down-gap that holds, a squeeze resolving downward, a climax bar at the end of an advance instead of a decline, a 0.77 percent VWAP discount under a one percent threshold. The sweep now kills 25 of 25. Docs: the library README claimed 26 strategies against 32 on disk before this branch added anything, so the number is derived from the filesystem now. tests/test_docs.py gains four guards — the README category table, the library total and its baseline/v1 split, the dagger marking v1 entries, and a check that no strategy hides below `library/<category>/<name>.nano`, which packaging globs one level deep while CI's verifier globs recursively. CONTRIBUTING said library entries are deliberately v0.1.0-only. That is now false, so it states the two-corpus rule instead: baseline entries stay pinned and are never converted, v1 is for ideas that genuinely need it, and no-fire assertions never ship without a positive control. docs/README.md flags that docs/language.md still documents only the v0.1.0 subset while the compiler implements the v1.0 superset. Repairing that document is outside this lane; naming the gap is not.
A public strategy library invites the question of where its ideas came from, and "trust us" is not an answer. Names the published sources — Donchian, Wilder, Lane, Appel, Bollinger, the academic momentum literature — and states plainly what was not done: no platform source was copied, adapted, or translated, and indicator names are used nominatively rather than as any claim of affiliation.
RED. The previous commit deleted `MACD_HIST > 0` from macd_zero_line_reclaim on the argument that a line crossing up through zero is necessarily above its own signal EMA. That argument is false, and the deletion changed behaviour. It holds only for the FIRST cross out of a sustained negative stretch. After a rally and a retrace the signal EMA still carries the earlier high line values and sits ABOVE the line at a second crossing — which is precisely the "spike that is already fading" the term was written to reject. Reproduced: a 45-bar decline, 19 bars up, 11 down, 4 up puts bar 78 at line = +0.3243, line[1] = -0.0980, hist = -3.6859. The deleted rule fired there; the restored rule does not. The term is back with its `smoothing` param, the fixture is regenerated from the CLI, and the counterexample is pinned as its own test. The header now records why the term survives a redundancy argument, so the next reader does not repeat the deletion. This is the failure the lane exists to prevent, committed by the lane: a mutation survived, and instead of writing the frame that isolates it I argued in prose that no such frame could exist. Untested reasoning is how vacuous tests are born, and a survivor is a question, not a verdict. Falsified alongside it, all previously unfalsifiable: * the unary minus in opening_gap_fade's else arm — written `gap <= threshold` it buys an ordinary small green bar. `nano/library/README.md` advertises unary minus as the headline v1 construct, so leaving it untested was doubly wrong. * `ATR(...)[1]` and `open - close[1]` in opening_gap_fade, via a gap of 2.30 against a prior-bar threshold of 2.25 that the gap bar's own range lifts to 2.39. The header claimed "the prior bar's ATR"; now something checks it. * `lower[1]` in bollinger_lower_reclaim, via a flat tape where the reclaim bar's violence drops the current band 6 points below the prior one. * `HIGHEST[1]`/`LOWEST[1]` against `[2]` on donchian_high_breakout — two bars beyond the channel in a row, where the stale top would signal twice. * boundary direction on donchian (`>` not `>=`, both arms) and on absolute_momentum_filter (a flat tape returns exactly zero and is risk-off), matching the baseline corpus's own convention of pinning its boundaries. The mutation harness now runs the whole test file per mutation instead of a `-k` selector. The selector for macd predated the new test and never ran it, which is how a sweep reports a survivor it has already killed — or misses one it has not. Whole file: 26/26. README maturity no longer outruns the packaging metadata. "Reference implementation, v1.0.0" asserted a stability that `Development Status :: 4 - Beta` denies; repairing a stale version number is in scope for this lane and upgrading the project's advertised maturity is not. It now reads "Beta reference implementation, v1.0.0" and a test pins the adjective to the classifier. The trademark nominative-use notice moved to the top-level README, because `nano/library/README.md` is not packaged — confirmed against the built wheel — and an installed distribution is exactly where the strategy filenames and the BollingerLowerReclaim identifier appear with no context. It now travels in METADATA.
…ctually guard Four items from delta review, each now verified by a run rather than an argument. 1. The absolute_momentum boundary pin did not work and the claim built on it was false. A flat tape zeroes BOTH returns, so relaxing either `>` to `>=` alone leaves the other conjunct false and the rule still reports risk-off; only the simultaneous mutation of both is caught, and nobody writes that mutation. Replaced with one frame per operator: 114 advancing bars then 21 flat gives short_return exactly 0 against long_return +50.48, and its mirror — nine flat bars, a decline to 47.5, a recovery landing bar 134 on exactly the value 126 bars earlier — gives long_return exactly 0 against short_return +110.53. Every step is a binary-exact half, so `close[134] == close[8]` is arithmetic and not a rounding coincidence, and the test asserts that equality directly. 2. CONTRIBUTING now requires mutation sweeps to run whole test files and forbids `-k` selectors, with the reason: a selector silently excludes any test whose name it does not match, including the one written for the mutation being checked. That is the defect that caused the previous return trip. 3. `test_the_trademark_notice_travels_with_the_distribution` asserted a proxy. It read README.md by name and never checked that README.md is what pyproject ships, so repointing `readme` left it green while the notice stopped reaching METADATA. It now parses the declaration and asserts against that file. 4. The maturity classifier regex captured `\w+`, which stops at the slash in `Production/Stable` and would have demanded the README read "Production". Now captures `[\w/]+` and compares the last path segment. Both boundaries previously called unpinnable are pinned. The artifacts came from the constants, not the operators: a window of span exactly 5.0 with a close 1.0 above the bottom makes STOCH_K exactly 20.0, and 19 quiet bars at 1700 with a climax at 5700 make SMA exactly 1900.0 and 3*SMA exactly 5700.0. Neither resembles the earlier 0.5999999999999943, which lived inside the asserted quantity and could not be chosen away. Verified by run, not by reasoning: 26 mutations (2 boundary operators fixed here, 2 newly pinned, 2 donchian, 2 macd incl. the hist regression lock, 6 offset and unary-minus, 6 doc guards) all killed against the whole test file; the classifier regex checked against Alpha, Beta and Production/Stable literals; the trademark guard checked by repointing pyproject at a decoy readme and confirming red, with pyproject restored byte-identical afterwards. Conjunct-drop sweep 26/26.
`scripts/check_contribution.py` calls `compile_source()` unconditionally, which is baseline-only, so `--all` died with an uncaught `IRVersionError` on the first v1 entry. Nothing had exercised it because every library entry was v0.1.0 until this branch; the twelve entries added here are what make the assumption false. Three checks assumed a `StrategyGraph`. Each now has a v1 equivalent rather than an exemption, so the newer corpus is checked as strictly as the older one: - Undocumented-signal check. A baseline entry names the feed signals the host must inject; a v1 entry declares them as typed inputs and computes its indicators from them. Either way the host owes the data, so both are checked against the same rule — the input names now stand in for condition signals. - IR round trip. v1 documents archive and reload through `NanoModule`. - Determinism replay. v1 modules execute through `run_module`. Also regenerates this branch's twelve `_ir.json` fixtures with `--write`, so they carry the repository's canonical one-node-per-line formatting. No baseline fixture is touched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AetherAI3
force-pushed
the
feat/nano-st3-library
branch
from
August 19, 2026 20:23
067809b to
1021f3a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
Twelve v1-native strategies — the library's first. The corpus goes 35 → 47 across eight categories, in two clearly-marked corpora: 35 baseline (
0.1.0) and 12 v1 (1.0.0).ema_pullback_continuation,macd_zero_line_reclaim,donchian_high_breakoutabsolute_momentum_filter,stochastic_reclaimbollinger_lower_reclaim,zscore_fade_trend_filtered,opening_gap_fadesqueeze_release_expansion,atr_regime_haltvolume_climax_reversal,vwap_band_reversionEach documents its intended regime, the regime where it must not fire, required host inputs, invalidation conditions, instrument assumptions, threshold-calibration assumptions, and the nearest strategy it could be confused with. Each ships a positive control, a no-fire control, and a determinism test. No performance, win-rate or profitability claim appears anywhere.
Concepts, not code
Every idea comes from pre-1990 published literature — Donchian's trading guidelines, Wilder 1978, Lane, Appel, Bollinger, academic time-series momentum. No Pine, MQL, or other platform source was read, copied, adapted, or translated; each was written from the concept against Nano's own grammar.
Six candidates were dropped rather than approximated: ADX/DI trend strength (no Wilder-DI kernel), OBV slope and linear-regression trend (no
LINREG_SLOPE—OBV > OBV[n]is a crude two-point substitute, not the concept), anchored/session VWAP (needs host state), percentile-rank regime filters (noPERCENTRANK), pairs/correlation spread (noCORREL, and one frame carries one instrument), and price/oscillator divergence (needs pivot detection). "Nano does not support this yet" beats a weird substitute.Named proprietary indicators were avoided on both licensing and not-in-the-registry grounds. "Bollinger Bands" is a live US trademark and is used nominatively, with a no-affiliation notice that now travels in the wheel's
METADATA— verified in the built artifact, and guarded by a test that parsesreadmeout ofpyproject.tomlrather than hardcoding a filename.The baseline corpus is untouched
git diff --diff-filter=MD origin/main..HEAD -- nano/librarylists onlyREADME.md. Not one pre-existing.nanoor_ir.jsonchanged. Every new fixture is generated by the repo's own tooling and regenerates byte-identically.Why this PR also touches
scripts/check_contribution.pyBecause this branch is what makes that script's central assumption false. It calls the baseline-only
compile_source()unconditionally, so--alldied with an uncaughtIRVersionErroron the first v1 entry — thecontribution-shapeCI job would have failed here even with pytest green. Nothing had exercised it, because every library entry was0.1.0until now.Three checks assumed a
StrategyGraph. Each got a v1 equivalent rather than an exemption, so the newer corpus is checked as strictly as the older one: the undocumented-signal check (a v1 entry's declaredinputs are the host's obligation, exactly as a baseline entry's feed signals are), the IR round trip (NanoModule), and the determinism replay (run_module). The twelve fixtures are regenerated with--writeso they carry the canonical one-node-per-line formatting.The RED this lane was returned on
The mutation sweep reported that dropping
MACD_HIST > 0changed nothing, so the term was reasoned about in prose, judged redundant, and deleted. It is not redundant. The implication holds only for the first cross out of a sustained negative stretch; after a rally and retrace the signal EMA sits above the line at the second crossing — exactly the fading spike the original header cited as the reason the term existed. An independent reviewer built the counterexample (bar 78:line = +0.3243,line[1] = −0.0980,hist = −3.6859); the shipped rule fired twice, the intended rule once.The root cause was worse than the symptom: the mutation harness ran
pytest -kwith a selector that predated the new test, so it never executed the test that would have killed the mutation — it reported a survivor that was already dead. The term is restored with the counterexample pinned as a regression test, the header explains why it is not redundant so nobody repeats the deletion, and CONTRIBUTING.md now requires mutation sweeps to run whole test files and forbids-kselectors.Verification
main.check_contribution.py --all: 47 entries ready for review, exit 0.Version: 1.0.2.Merge order
Takes 1.0.2, so it should merge after #27 (1.0.1) and before #28 (1.0.3). If the order changes I will re-bump.
Rebased from
811d6c7onto currentmain, which moved underneath this work. Upstream's rewrittennano/library/README.md, its per-category table format, and its count-drift guards intests/test_library.pyare all treated as canonical — this lane's duplicate count guards were dropped rather than shipped alongside, and only the guards upstream does not cover were kept: packaging depth, the baseline/v1 dagger split, README maturity vs the packaging classifier, and the trademark notice.🤖 Generated with Claude Code