From 9d33edc67fde444d9c31269db27be41197c1a921 Mon Sep 17 00:00:00 2001 From: DBarr3 <143002219+DBarr3@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:59:25 -0400 Subject: [PATCH 1/2] Add curated G3 strategy batch --- .github/workflows/ci.yml | 2 +- README.md | 2 +- nano/library/README.md | 8 +- .../library/catalog/strategy_metadata_v1.json | 76 +++++- .../volatility_percentile_reclaim.nano | 56 ++++ .../volatility_percentile_reclaim_ir.json | 38 +++ .../volume_confirmed_channel_breakout.nano | 52 ++++ .../volume_confirmed_channel_breakout_ir.json | 34 +++ tests/test_catalog.py | 10 +- tests/test_cli.py | 6 +- tests/test_compiler_fuzz.py | 6 +- tests/test_g3_strategies.py | 245 ++++++++++++++++++ 12 files changed, 514 insertions(+), 21 deletions(-) create mode 100644 nano/library/mean_reversion/volatility_percentile_reclaim.nano create mode 100644 nano/library/mean_reversion/volatility_percentile_reclaim_ir.json create mode 100644 nano/library/trend/volume_confirmed_channel_breakout.nano create mode 100644 nano/library/trend/volume_confirmed_channel_breakout_ir.json create mode 100644 tests/test_g3_strategies.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d76ed5b..f357338 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: "assert baseline_control_frames.__module__ == " "'nano.library.contribution'; " "catalog = load_catalog(); " - "assert catalog['strategyCount'] == 53; " + "assert catalog['strategyCount'] == 55; " "assert catalog_diagnostics() == ()" ), ], diff --git a/README.md b/README.md index 42281c6..ae18274 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ The same corpus is browsable at **[aethersystems.net/nano](https://aethersystems | momentum | mean_reversion | trend | volatility | volume | risk | event_volatility | watchdog | | --- | --- | --- | --- | --- | --- | --- | --- | -| 6 rules | 6 rules | 7 rules | 4 rules | 4 rules | 7 rules | 11 rules | 8 rules | +| 6 rules | 7 rules | 8 rules | 4 rules | 4 rules | 7 rules | 11 rules | 8 rules | That table holds **two corpora**. A **baseline rule** compares a host-supplied named signal with a literal and compiles to byte-stable v0.1.0 IR. A **v1 rule** declares its own `input`s and lets Nano compute the indicators, so the host contract is OHLCV rather than a vocabulary of agreed indicator names. `tests/test_library.py` derives every count above from the directory, so the table cannot go stale silently. diff --git a/nano/library/README.md b/nano/library/README.md index 422cdec..f543553 100644 --- a/nano/library/README.md +++ b/nano/library/README.md @@ -15,7 +15,7 @@ The library holds two kinds of entry, and the difference is worth understanding | | **Baseline entries** | **v1 entries** | | --- | --- | --- | -| Count | 41 | 12 | +| Count | 41 | 14 | | Source shape | one `every`, one `if`, AND-chained comparisons of a named signal against a literal | `param`/`input`/`let` declarations, arithmetic, `else`, offsets | | Where the numbers come from | the **host** computes every indicator and injects it as a named series | the host supplies **OHLCV**; Nano computes the indicators | | Compiled IR | `0.1.0`, byte-stable | `1.0.0`, carries `sourceHash` and `moduleHash` | @@ -27,7 +27,7 @@ The practical difference for a host is the size of the contract. A baseline entr ## Learn, compare, contribute -The current library contains 53 strategies across eight categories — seven trading, one for deterministic watchdog controls — in two corpora: 41 baseline and 12 v1. Browse an entry to see the source, its expected IR, and the data contract it assumes. When you are ready, a well-documented strategy pair is the most direct contribution to Nano. +The current library contains 55 strategies across eight categories — seven trading, one for deterministic watchdog controls — in two corpora: 41 baseline and 14 v1. Browse an entry to see the source, its expected IR, and the data contract it assumes. When you are ready, a well-documented strategy pair is the most direct contribution to Nano. [Walk through a first contribution →](../../docs/first-contribution.md) · [Add a strategy →](../../CONTRIBUTING.md#add-a-strategy) · [Open a proposal →](https://github.com/AetherAI3/Nano/issues/new?template=strategy-library.yml) @@ -44,8 +44,8 @@ python scripts/check_contribution.py --write nano/library//.nano | Category | Strategies | | --- | --- | | `momentum/` | `rsi_oversold_reversal`, `stochastic_oversold`, `williams_r_reversal`, `roc_momentum`, `absolute_momentum_filter`†, `stochastic_reclaim`† | -| `mean_reversion/` | `bollinger_band_touch`, `zscore_reversion`, `cci_extreme`, `bollinger_lower_reclaim`†, `zscore_fade_trend_filtered`†, `opening_gap_fade`† | -| `trend/` | `golden_cross`, `macd_histogram_flip`, `donchian_breakout`, `supertrend_flip_long`, `ema_pullback_continuation`†, `macd_zero_line_reclaim`†, `donchian_high_breakout`† | +| `mean_reversion/` | `bollinger_band_touch`, `zscore_reversion`, `cci_extreme`, `bollinger_lower_reclaim`†, `zscore_fade_trend_filtered`†, `opening_gap_fade`†, `volatility_percentile_reclaim`† | +| `trend/` | `golden_cross`, `macd_histogram_flip`, `donchian_breakout`, `supertrend_flip_long`, `ema_pullback_continuation`†, `macd_zero_line_reclaim`†, `donchian_high_breakout`†, `volume_confirmed_channel_breakout`† | | `volatility/` | `atr_volatility_halt`, `bb_squeeze_breakout`, `squeeze_release_expansion`†, `atr_regime_halt`† | | `volume/` | `volume_spike_confirmation`, `obv_trend`, `volume_climax_reversal`†, `vwap_band_reversion`† | | `risk/` | `max_drawdown_breaker`, `daily_loss_limit`, `position_concentration_cap`, `correlation_cluster_guard`, `stale_data_halt`, `leverage_ceiling`, `consecutive_loss_circuit` | diff --git a/nano/library/catalog/strategy_metadata_v1.json b/nano/library/catalog/strategy_metadata_v1.json index 636f96f..acaf5c3 100644 --- a/nano/library/catalog/strategy_metadata_v1.json +++ b/nano/library/catalog/strategy_metadata_v1.json @@ -3,20 +3,20 @@ "type": "NanoStrategyCatalog", "schemaVersion": 1, "metadataVersion": "StrategyMetadataV1", - "strategyCount": 53, + "strategyCount": 55, "categoryCounts": { "event_volatility": 11, - "mean_reversion": 6, + "mean_reversion": 7, "momentum": 6, "risk": 7, - "trend": 7, + "trend": 8, "volatility": 4, "volume": 4, "watchdog": 8 }, "irMaturityCounts": { "baseline": 41, - "v1": 12 + "v1": 14 }, "strategies": [ { @@ -478,6 +478,40 @@ "sourcePath": "library/mean_reversion/opening_gap_fade.nano", "irPath": "library/mean_reversion/opening_gap_fade_ir.json" }, + { + "metadataVersion": "StrategyMetadataV1", + "id": "mean_reversion/volatility_percentile_reclaim", + "slug": "volatility_percentile_reclaim", + "name": "VolatilityPercentileReclaim", + "category": "mean_reversion", + "irMaturity": "v1", + "irVersion": "1.0.0", + "regime": "range-like or orderly pullback behavior outside an exceptional volatility state. Do NOT use the gate as a claim that low volatility causes reversal; it only separates a familiar reclaim from a distributional shock. INPUTS: high, low, close (series). Bars must be contiguous: ATR resets across gaps, and PERCENTRANK stays absent until a full new history is present.", + "conditions": "prior close at or below its prior lower band, current close above its current lower band, and ATR rank below 0.9 against exactly the preceding hundred ATR values. All three terms have independent mutation controls.", + "invalidation": "another close below the band, or ATR entering the excluded top decile. Either says the excursion is continuing or the volatility regime has changed; the host, not this rule, decides what to do with an existing position.", + "shape": "1d; a two-bar downside excursion and reclaim after enough contiguous history to compare today's range state with the preceding hundred ATR cells.", + "calibratedOn": "liquid US equity index proxies, daily. Bollinger 20/2 and ATR 14 are published conventions; 100 observations and 0.9 define the research regime split. None is a profitability claim, and cadence changes need review.", + "nearestConfused": [ + { + "slug": "bollinger_lower_reclaim", + "id": "mean_reversion/bollinger_lower_reclaim", + "distinction": "that rule takes every strict lower-band reclaim. This one abstains when the same price shape occurs at an extreme ATR rank; the percentile gate, not a different band threshold, is the structural hypothesis." + }, + { + "slug": "zscore_fade_trend_filtered", + "id": "mean_reversion/zscore_fade_trend_filtered", + "distinction": "that enters while z-score remains deeply negative and uses a long price trend as its regime gate. This waits for a full band reclaim and gates on the instrument's own volatility distribution." + } + ], + "provenance": "Bollinger-band conventions and Engle (1982) conditional-volatility research; the regime gate is an original hypothesis, with no source code copied.", + "requiredHostSignals": [ + "close", + "high", + "low" + ], + "sourcePath": "library/mean_reversion/volatility_percentile_reclaim.nano", + "irPath": "library/mean_reversion/volatility_percentile_reclaim_ir.json" + }, { "metadataVersion": "StrategyMetadataV1", "id": "mean_reversion/zscore_fade_trend_filtered", @@ -1119,6 +1153,40 @@ "sourcePath": "library/trend/supertrend_flip_long.nano", "irPath": "library/trend/supertrend_flip_long_ir.json" }, + { + "metadataVersion": "StrategyMetadataV1", + "id": "trend/volume_confirmed_channel_breakout", + "slug": "volume_confirmed_channel_breakout", + "name": "VolumeConfirmedChannelBreakout", + "category": "trend", + "irMaturity": "v1", + "irVersion": "1.0.0", + "regime": "liquid directional expansion after a range or consolidation. Do NOT fire in thin markets, opening auctions, or feeds where volume is synthetic: the participation term is only meaningful when bars measure comparable size. INPUTS: high, close, volume (series). Volume must be real traded size from one consistently defined venue or consolidated feed.", + "conditions": "close above the preceding twenty-bar high and volume at least 1.5 times the preceding twenty-bar average. Price expansion without volume is explicitly a no-fire; high volume without a price break is also a no-fire.", + "invalidation": "a following close back inside the prior channel, especially on equal or greater volume. The rule proposes an entry only; the host owns exits.", + "shape": "1d; a close through a multi-week ceiling with visibly greater participation than the bars that built the ceiling.", + "calibratedOn": "liquid continuously traded instruments, daily. Twenty bars and 1.5 times prior average volume are public conventions, not performance claims. The volume multiple does not travel across venue or session changes.", + "nearestConfused": [ + { + "slug": "donchian_high_breakout", + "id": "trend/donchian_high_breakout", + "distinction": "that price-only rule fires on every qualifying channel break and carries a short mirror. This long-only variant requires an independent participation confirmation and abstains on an ordinary-volume break, which is the essential condition pinned by its mutation control." + }, + { + "slug": "volume_climax_reversal", + "id": "volume/volume_climax_reversal", + "distinction": "that is a one-bar exhaustion reversal below its trend with a high close-position requirement. This is continuation through a prior price boundary; volume confirms expansion rather than capitulation." + } + ], + "provenance": "Donchian channel concepts and Lee-Swaminathan (2000) volume/momentum research; original Nano expression, with no source code copied.", + "requiredHostSignals": [ + "close", + "high", + "volume" + ], + "sourcePath": "library/trend/volume_confirmed_channel_breakout.nano", + "irPath": "library/trend/volume_confirmed_channel_breakout_ir.json" + }, { "metadataVersion": "StrategyMetadataV1", "id": "volatility/atr_regime_halt", diff --git a/nano/library/mean_reversion/volatility_percentile_reclaim.nano b/nano/library/mean_reversion/volatility_percentile_reclaim.nano new file mode 100644 index 0000000..3834fba --- /dev/null +++ b/nano/library/mean_reversion/volatility_percentile_reclaim.nano @@ -0,0 +1,56 @@ +// Volatility-percentile reclaim. The two-bar signal is the same strict +// lower-Bollinger-band excursion and reclaim shape readers already know, but it +// is admitted only when current ATR is below the top decile of its own preceding +// hundred observations. PERCENTRANK excludes the current cell, so an extreme +// current ATR can reach 1.0 and veto the trade without contaminating its baseline. +// SOURCE: Bollinger-band conventions and Engle (1982) conditional-volatility +// research; the regime gate is an original hypothesis, with no source code copied. +// REGIME: range-like or orderly pullback behavior outside an exceptional +// volatility state. Do NOT use the gate as a claim that low volatility causes +// reversal; it only separates a familiar reclaim from a distributional shock. +// INPUTS: high, low, close (series). Bars must be contiguous: ATR resets +// across gaps, and PERCENTRANK stays absent until a full new history is present. +// CONDITIONS: prior close at or below its prior lower band, current close above +// its current lower band, and ATR rank below 0.9 against exactly the preceding +// hundred ATR values. All three terms have independent mutation controls. +// INVALIDATION: another close below the band, or ATR entering the excluded top +// decile. Either says the excursion is continuing or the volatility regime has +// changed; the host, not this rule, decides what to do with an existing position. +// SHAPE: 1d; a two-bar downside excursion and reclaim after enough contiguous +// history to compare today's range state with the preceding hundred ATR cells. +// NOT bollinger_lower_reclaim: that rule takes every strict lower-band reclaim. +// This one abstains when the same price shape occurs at an extreme ATR rank; the +// percentile gate, not a different band threshold, is the structural hypothesis. +// NOT zscore_fade_trend_filtered: that enters while z-score remains deeply +// negative and uses a long price trend as its regime gate. This waits for a full +// band reclaim and gates on the instrument's own volatility distribution. +// CALIBRATED ON: liquid US equity index proxies, daily. Bollinger 20/2 and ATR +// 14 are published conventions; 100 observations and 0.9 define the research +// regime split. None is a profitability claim, and cadence changes need review. +strategy VolatilityPercentileReclaim { + + param band_window: int = 20 + param band_mult: float = 2.0 + param atr_window: int = 14 + param rank_window: int = 100 + param max_atr_rank: float = 0.9 + + input high: series + input low: series + input close: series + + let lower = BB_LOWER(close, band_window, band_mult) + let atr_now = ATR(high, low, close, atr_window) + let atr_rank = PERCENTRANK(atr_now, rank_window) + + every 1d { + + if close > lower and close[1] <= lower[1] and atr_rank < max_atr_rank { + + buy(SPY, 0.6) + + } + + } + +} diff --git a/nano/library/mean_reversion/volatility_percentile_reclaim_ir.json b/nano/library/mean_reversion/volatility_percentile_reclaim_ir.json new file mode 100644 index 0000000..ca2cc5d --- /dev/null +++ b/nano/library/mean_reversion/volatility_percentile_reclaim_ir.json @@ -0,0 +1,38 @@ +{ + "type": "Strategy", + "nanoIrVersion": "1.0.0", + "name": "VolatilityPercentileReclaim", + "tier": "nano", + "determinism": {"clock": "injected", "entropy": "injected", "fastmath": false}, + "params": [{"name": "band_window", "type": "int", "value": 20}, {"name": "band_mult", "type": "float", "value": 2.0}, {"name": "atr_window", "type": "int", "value": 14}, {"name": "rank_window", "type": "int", "value": 100}, {"name": "max_atr_rank", "type": "float", "value": 0.9}], + "inputs": [{"name": "high", "type": "series"}, {"name": "low", "type": "series"}, {"name": "close", "type": "series"}], + "warmup": 114, + "entries": ["n22"], + "provenance": {"compiler": {"name": "nnc", "version": "1.0.0"}, "sourceHash": "sha256:a59d47f6086accd206b1d991e610fe8df7a85d2680b69f3721b530ee051d2e7c"}, + "moduleHash": "sha256:077456625a685f240735382fb89a85bff92775774ebda859c1c6d34f55aee26a", + "effects": ["intent.emit", "log.append"], + "nodes": [ + { "id": "n1", "op": "input.ref", "inputs": [], "attrs": {"name": "close"}, "type": "series" }, + { "id": "n2", "op": "param.ref", "inputs": [], "attrs": {"name": "band_mult"}, "type": "float" }, + { "id": "n3", "op": "indicator", "inputs": ["n1", "n2"], "attrs": {"name": "BB_LOWER", "periods": [20], "lookback": 19, "lifted": false}, "type": "series" }, + { "id": "n4", "op": "let", "inputs": ["n3"], "attrs": {"name": "lower"}, "type": "series" }, + { "id": "n5", "op": "input.ref", "inputs": [], "attrs": {"name": "high"}, "type": "series" }, + { "id": "n6", "op": "input.ref", "inputs": [], "attrs": {"name": "low"}, "type": "series" }, + { "id": "n7", "op": "indicator", "inputs": ["n5", "n6", "n1"], "attrs": {"name": "ATR", "periods": [14], "lookback": 14, "lifted": false}, "type": "series" }, + { "id": "n8", "op": "let", "inputs": ["n7"], "attrs": {"name": "atr_now"}, "type": "series" }, + { "id": "n9", "op": "indicator", "inputs": ["n8"], "attrs": {"name": "PERCENTRANK", "periods": [100], "lookback": 114, "lifted": false}, "type": "series" }, + { "id": "n10", "op": "let", "inputs": ["n9"], "attrs": {"name": "atr_rank"}, "type": "series" }, + { "id": "n11", "op": "schedule", "inputs": [], "attrs": {"interval": "1d"} }, + { "id": "n12", "op": "compare.gt", "inputs": ["n1", "n4"], "type": "series" }, + { "id": "n13", "op": "series.index", "inputs": ["n1"], "attrs": {"offset": 1}, "type": "series" }, + { "id": "n14", "op": "series.index", "inputs": ["n4"], "attrs": {"offset": 1}, "type": "series" }, + { "id": "n15", "op": "compare.le", "inputs": ["n13", "n14"], "type": "series" }, + { "id": "n16", "op": "logic.and", "inputs": ["n12", "n15"], "type": "series" }, + { "id": "n17", "op": "param.ref", "inputs": [], "attrs": {"name": "max_atr_rank"}, "type": "float" }, + { "id": "n18", "op": "compare.lt", "inputs": ["n10", "n17"], "type": "series" }, + { "id": "n19", "op": "logic.and", "inputs": ["n16", "n18"], "type": "series" }, + { "id": "n20", "op": "intent.emit", "inputs": [], "attrs": {"action": "BUY", "asset": "SPY", "confidence": 0.6} }, + { "id": "n21", "op": "block", "inputs": ["n20"] }, + { "id": "n22", "op": "rule", "inputs": ["n11", "n19", "n21"] } + ] +} diff --git a/nano/library/trend/volume_confirmed_channel_breakout.nano b/nano/library/trend/volume_confirmed_channel_breakout.nano new file mode 100644 index 0000000..50debde --- /dev/null +++ b/nano/library/trend/volume_confirmed_channel_breakout.nano @@ -0,0 +1,52 @@ +// Volume-confirmed channel breakout. Price must close above the prior +// twenty-bar high while current volume reaches a multiple of the prior +// twenty-bar volume average. Both reference series are read at [1], so the +// breakout bar cannot raise the boundary or dilute its own participation test. +// SOURCE: Donchian channel concepts and Lee-Swaminathan (2000) volume/momentum +// research; original Nano expression, with no source code copied. +// REGIME: liquid directional expansion after a range or consolidation. Do NOT +// fire in thin markets, opening auctions, or feeds where volume is synthetic: +// the participation term is only meaningful when bars measure comparable size. +// INPUTS: high, close, volume (series). Volume must be real traded size +// from one consistently defined venue or consolidated feed. +// CONDITIONS: close above the preceding twenty-bar high and volume at least +// 1.5 times the preceding twenty-bar average. Price expansion without volume +// is explicitly a no-fire; high volume without a price break is also a no-fire. +// INVALIDATION: a following close back inside the prior channel, especially on +// equal or greater volume. The rule proposes an entry only; the host owns exits. +// SHAPE: 1d; a close through a multi-week ceiling with visibly greater +// participation than the bars that built the ceiling. +// NOT donchian_high_breakout: that price-only rule fires on every qualifying +// channel break and carries a short mirror. This long-only variant requires an +// independent participation confirmation and abstains on an ordinary-volume +// break, which is the essential condition pinned by its mutation control. +// NOT volume_climax_reversal: that is a one-bar exhaustion reversal below its +// trend with a high close-position requirement. This is continuation through a +// prior price boundary; volume confirms expansion rather than capitulation. +// CALIBRATED ON: liquid continuously traded instruments, daily. Twenty bars +// and 1.5 times prior average volume are public conventions, not performance +// claims. The volume multiple does not travel across venue or session changes. +strategy VolumeConfirmedChannelBreakout { + + param channel: int = 20 + param volume_window: int = 20 + param volume_multiple: float = 1.5 + + input high: series + input close: series + input volume: series + + let prior_top = HIGHEST(high, channel)[1] + let prior_volume = SMA(volume, volume_window)[1] + + every 1d { + + if close > prior_top and volume >= prior_volume * volume_multiple { + + buy(ETHUSD, 0.65) + + } + + } + +} diff --git a/nano/library/trend/volume_confirmed_channel_breakout_ir.json b/nano/library/trend/volume_confirmed_channel_breakout_ir.json new file mode 100644 index 0000000..f31859f --- /dev/null +++ b/nano/library/trend/volume_confirmed_channel_breakout_ir.json @@ -0,0 +1,34 @@ +{ + "type": "Strategy", + "nanoIrVersion": "1.0.0", + "name": "VolumeConfirmedChannelBreakout", + "tier": "nano", + "determinism": {"clock": "injected", "entropy": "injected", "fastmath": false}, + "params": [{"name": "channel", "type": "int", "value": 20}, {"name": "volume_window", "type": "int", "value": 20}, {"name": "volume_multiple", "type": "float", "value": 1.5}], + "inputs": [{"name": "high", "type": "series"}, {"name": "close", "type": "series"}, {"name": "volume", "type": "series"}], + "warmup": 20, + "entries": ["n18"], + "provenance": {"compiler": {"name": "nnc", "version": "1.0.0"}, "sourceHash": "sha256:c4f1f1cb21318137fc06ec72282c1ad19ecb705dd8c1409a6ce5c33acefe1eae"}, + "moduleHash": "sha256:861761017775f00c0d891215a3274ae7554b3339703bc61e820da0638bdc9707", + "effects": ["intent.emit", "log.append"], + "nodes": [ + { "id": "n1", "op": "input.ref", "inputs": [], "attrs": {"name": "high"}, "type": "series" }, + { "id": "n2", "op": "indicator", "inputs": ["n1"], "attrs": {"name": "HIGHEST", "periods": [20], "lookback": 19, "lifted": false}, "type": "series" }, + { "id": "n3", "op": "series.index", "inputs": ["n2"], "attrs": {"offset": 1}, "type": "series" }, + { "id": "n4", "op": "let", "inputs": ["n3"], "attrs": {"name": "prior_top"}, "type": "series" }, + { "id": "n5", "op": "input.ref", "inputs": [], "attrs": {"name": "volume"}, "type": "series" }, + { "id": "n6", "op": "indicator", "inputs": ["n5"], "attrs": {"name": "SMA", "periods": [20], "lookback": 19, "lifted": false}, "type": "series" }, + { "id": "n7", "op": "series.index", "inputs": ["n6"], "attrs": {"offset": 1}, "type": "series" }, + { "id": "n8", "op": "let", "inputs": ["n7"], "attrs": {"name": "prior_volume"}, "type": "series" }, + { "id": "n9", "op": "schedule", "inputs": [], "attrs": {"interval": "1d"} }, + { "id": "n10", "op": "input.ref", "inputs": [], "attrs": {"name": "close"}, "type": "series" }, + { "id": "n11", "op": "compare.gt", "inputs": ["n10", "n4"], "type": "series" }, + { "id": "n12", "op": "param.ref", "inputs": [], "attrs": {"name": "volume_multiple"}, "type": "float" }, + { "id": "n13", "op": "arith.mul", "inputs": ["n8", "n12"], "type": "series" }, + { "id": "n14", "op": "compare.ge", "inputs": ["n5", "n13"], "type": "series" }, + { "id": "n15", "op": "logic.and", "inputs": ["n11", "n14"], "type": "series" }, + { "id": "n16", "op": "intent.emit", "inputs": [], "attrs": {"action": "BUY", "asset": "ETHUSD", "confidence": 0.65} }, + { "id": "n17", "op": "block", "inputs": ["n16"] }, + { "id": "n18", "op": "rule", "inputs": ["n9", "n15", "n17"] } + ] +} diff --git a/tests/test_catalog.py b/tests/test_catalog.py index 739b6d5..1fa2cf9 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -99,14 +99,14 @@ def test_checked_in_catalog_is_byte_identical_and_counts_the_landed_corpus(): assert first.startswith("{\n\n") assert first.endswith("}\n") assert catalog_path(LIBRARY).read_bytes() == first.encode("utf-8") - assert document["strategyCount"] == 53 - assert document["irMaturityCounts"] == {"baseline": 41, "v1": 12} + assert document["strategyCount"] == 55 + assert document["irMaturityCounts"] == {"baseline": 41, "v1": 14} assert document["categoryCounts"] == { "event_volatility": 11, - "mean_reversion": 6, + "mean_reversion": 7, "momentum": 6, "risk": 7, - "trend": 7, + "trend": 8, "volatility": 4, "volume": 4, "watchdog": 8, @@ -121,7 +121,7 @@ def test_ids_slugs_and_serialized_order_are_stable_and_unambiguous(): slugs = [row["slug"] for row in rows] assert ids == sorted(ids) - assert len(ids) == len(set(ids)) == len(set(slugs)) == 53 + assert len(ids) == len(set(ids)) == len(set(slugs)) == 55 assert all(row["metadataVersion"] == "StrategyMetadataV1" for row in rows) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3625859..449aade 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -397,7 +397,7 @@ def test_library_list_is_stable_and_complete(capsys): assert code == EXIT_OK assert err == "" assert lines[0] == "ID\tIR\tHOST SIGNALS" - assert len(lines) == 54 + assert len(lines) == 56 assert lines[1].startswith("event_volatility/cpi_impulse_pullback_long\t0.1.0\t") assert lines[-1].startswith("watchdog/trusted_route_guard\t0.1.0\t") @@ -475,8 +475,8 @@ def test_library_check_regenerates_byte_identically(capsys): assert code == EXIT_OK assert err == "" - assert "53 strategies" in out - assert "41 baseline + 12 v1" in out + assert "55 strategies" in out + assert "41 baseline + 14 v1" in out # -- nano indicators / version / help ---------------------------------------- diff --git a/tests/test_compiler_fuzz.py b/tests/test_compiler_fuzz.py index 6f34335..fd9d080 100644 --- a/tests/test_compiler_fuzz.py +++ b/tests/test_compiler_fuzz.py @@ -42,11 +42,11 @@ def test_catalog_corpus_exercises_contribution_controls_and_both_ir_versions(): audit = audit_catalog_corpus() - assert audit.strategy_count == 53 + assert audit.strategy_count == 55 assert audit.baseline_ir_count == 41 - assert audit.v1_ir_count == 12 + assert audit.v1_ir_count == 14 assert audit.watchdog_count == 8 - assert audit.control_cases == 53 + assert audit.control_cases == 55 assert audit.defects == () diff --git a/tests/test_g3_strategies.py b/tests/test_g3_strategies.py new file mode 100644 index 0000000..02ee027 --- /dev/null +++ b/tests/test_g3_strategies.py @@ -0,0 +1,245 @@ +"""Adversarial controls for the deliberately small G3 strategy batch. + +Every negative frame reaches a warmed condition and differs from a positive +control in one essential hypothesis. Term-removal mutants prove those frames +would fail if the corresponding condition disappeared. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from nano.compiler import compile_module, compile_to_dict +from nano.indicators import evaluate +from nano.ir.module import NanoModule +from nano.runtime import MarketFrame, run_module + + +LIBRARY = Path(__file__).resolve().parents[1] / "nano" / "library" + + +def _source(relative: str) -> str: + return (LIBRARY / relative).read_text(encoding="utf-8") + + +def _module(relative: str) -> NanoModule: + return compile_module(_source(relative)) + + +def _frame(**signals) -> MarketFrame: + length = len(next(iter(signals.values()))) + return MarketFrame( + timestamps=tuple(index * 86400 for index in range(length)), + signals={ + name: tuple(None if cell is None else float(cell) for cell in values) + for name, values in signals.items() + }, + ) + + +def _fires(module: NanoModule, frame: MarketFrame) -> list[tuple[str, int]]: + return [ + (intent.action, intent.timestamp // 86400) + for intent in run_module(module, frame).intents + ] + + +def _breakout_frame(*, volume: float, breakout: bool = True) -> MarketFrame: + closes = [100.0] * 20 + [102.0 if breakout else 100.0] + highs = [100.5] * 20 + [102.5 if breakout else 100.5] + volumes = [1000.0] * 20 + [volume] + return _frame(high=highs, close=closes, volume=volumes) + + +_RECLAIM_BASE = [ + 100.0 + (0.3 if index % 2 else -0.3) for index in range(130) +] + + +def _reclaim_frame(tail: list[float], *, wide_history: bool) -> MarketFrame: + """Build the same close shape in ordinary or extreme current volatility. + + Wide early ranges make the final ATR ordinary relative to its preceding + hundred values. Calm history makes the excursion's final ATR the unique + maximum, so its inclusive rank is exactly 1.0. Bollinger bands read close + only, leaving the reclaim shape identical between the two frames. + """ + + closes = _RECLAIM_BASE + tail + widths = [ + 10.0 if wide_history and index < 100 else 0.5 + for index in range(len(closes)) + ] + highs = [close + width for close, width in zip(closes, widths)] + lows = [close - width for close, width in zip(closes, widths)] + return _frame(high=highs, low=lows, close=closes) + + +def _final_atr_rank(frame: MarketFrame) -> float: + length = len(frame.timestamps) + atr = evaluate( + "ATR", + [ + frame.signals["high"], + frame.signals["low"], + frame.signals["close"], + 14, + ], + length=length, + ) + ranks = evaluate("PERCENTRANK", [atr, 100], length=length) + assert ranks[-1] is not None + return float(ranks[-1]) + + +@pytest.mark.parametrize( + "relative", + [ + "trend/volume_confirmed_channel_breakout.nano", + "mean_reversion/volatility_percentile_reclaim.nano", + ], +) +def test_g3_sources_compile_to_their_canonical_v1_ir(relative: str): + source_path = LIBRARY / relative + ir_path = source_path.with_name(f"{source_path.stem}_ir.json") + compiled = compile_to_dict(source_path.read_text(encoding="utf-8")) + + assert compiled["nanoIrVersion"] == "1.0.0" + assert compiled == json.loads(ir_path.read_text(encoding="utf-8")) + + +def test_g3_warmups_are_derived_from_the_landed_registry_contracts(): + breakout = _module("trend/volume_confirmed_channel_breakout.nano") + reclaim = _module("mean_reversion/volatility_percentile_reclaim.nano") + + # HIGHEST(20) and SMA(20) each look back 19 bars; reading both at [1] + # raises the module warmup to 20. + assert breakout.warmup == 20 + breakout_lookbacks = { + node.attrs["name"]: node.attrs["lookback"] + for node in breakout.of_op("indicator") + } + assert breakout_lookbacks == {"HIGHEST": 19, "SMA": 19} + + # ATR(14) first exists after 14 bars. PERCENTRANK then requires exactly + # one hundred preceding ATR cells, so its cumulative lookback is 114. + assert reclaim.warmup == 114 + reclaim_lookbacks = { + node.attrs["name"]: node.attrs["lookback"] + for node in reclaim.of_op("indicator") + } + assert reclaim_lookbacks == { + "BB_LOWER": 19, + "ATR": 14, + "PERCENTRANK": 114, + } + + +def test_volume_breakout_requires_price_and_prior_volume_confirmation(): + relative = "trend/volume_confirmed_channel_breakout.nano" + source = _source(relative) + module = compile_module(source) + + # 1500 is exactly 1.5 times the untouched prior average of 1000. Reading + # the unshifted average would dilute the boundary with the current bar and + # incorrectly reject this positive control. + positive = _breakout_frame(volume=1500.0) + assert _fires(module, positive) == [("BUY", 20)] + + ordinary_volume = _breakout_frame(volume=1000.0) + no_price_break = _breakout_frame(volume=1500.0, breakout=False) + assert _fires(module, ordinary_volume) == [] + assert _fires(module, no_price_break) == [] + + no_volume_term = source.replace( + " and volume >= prior_volume * volume_multiple", "" + ) + assert no_volume_term != source + assert _fires(compile_module(no_volume_term), ordinary_volume) == [("BUY", 20)] + + no_price_term = source.replace("close > prior_top and ", "") + assert no_price_term != source + assert _fires(compile_module(no_price_term), no_price_break) == [("BUY", 20)] + + +def test_percentile_reclaim_vetoes_the_same_shape_at_extreme_atr_rank(): + relative = "mean_reversion/volatility_percentile_reclaim.nano" + source = _source(relative) + module = compile_module(source) + + ordinary = _reclaim_frame([92.0, 99.5], wide_history=True) + extreme = _reclaim_frame([92.0, 99.5], wide_history=False) + + assert _final_atr_rank(ordinary) < 0.9 + assert _final_atr_rank(extreme) == 1.0 + assert _fires(module, ordinary) == [("BUY", 131)] + assert _fires(module, extreme) == [] + + no_percentile_gate = source.replace(" and atr_rank < max_atr_rank", "") + assert no_percentile_gate != source + assert _fires(compile_module(no_percentile_gate), extreme) == [("BUY", 131)] + + +def test_percentile_reclaim_requires_both_halves_of_the_strict_reclaim(): + relative = "mean_reversion/volatility_percentile_reclaim.nano" + source = _source(relative) + module = compile_module(source) + + # The prior close is outside, but the current close deepens the excursion. + # Removing only the current reclaim term converts that invalidation to a buy. + deepening = _reclaim_frame([92.0, 91.0], wide_history=True) + assert _fires(module, deepening) == [] + no_current_reclaim = source.replace("close > lower and ", "") + assert no_current_reclaim != source + assert _fires(compile_module(no_current_reclaim), deepening) == [("BUY", 131)] + + # Both bars are inside the band, so there was no excursion to reclaim. + # Removing the prior-excursion half makes ordinary inside-band bars fire. + no_excursion = _reclaim_frame([99.5], wide_history=True) + assert _fires(module, no_excursion) == [] + no_prior_excursion = source.replace(" and close[1] <= lower[1]", "") + assert no_prior_excursion != source + assert ("BUY", 130) in _fires(compile_module(no_prior_excursion), no_excursion) + + +def test_percentile_reclaim_rebuilds_its_full_history_after_a_gap(): + module = _module("mean_reversion/volatility_percentile_reclaim.nano") + continuous = _reclaim_frame([92.0, 99.5], wide_history=True) + + signals = {name: list(values) for name, values in continuous.signals.items()} + for values in signals.values(): + values[100] = None + gapped = _frame(**signals) + + # The identical final reclaim remains inside the 114-bar post-gap warmup. + # It must stay absent rather than reusing percentile history from before + # the missing cell. + assert _fires(module, continuous) == [("BUY", 131)] + assert _fires(module, gapped) == [] + + +@pytest.mark.parametrize( + ("relative", "frame"), + [ + ( + "trend/volume_confirmed_channel_breakout.nano", + _breakout_frame(volume=1500.0), + ), + ( + "mean_reversion/volatility_percentile_reclaim.nano", + _reclaim_frame([92.0, 99.5], wide_history=True), + ), + ], +) +def test_g3_positive_controls_replay_deterministically( + relative: str, frame: MarketFrame +): + module = _module(relative) + first = run_module(module, frame) + second = run_module(module, frame) + + assert first.intents + assert first.to_dict() == second.to_dict() From 7410e58fc4a035bb1d824112178ee1fc1064798b Mon Sep 17 00:00:00 2001 From: DBarr3 <143002219+DBarr3@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:01:40 -0400 Subject: [PATCH 2/2] chore(release): publish 1.0.12 --- nano/__init__.py | 2 +- pyproject.toml | 2 +- tests/golden/receipt_drift.json | 2 +- tests/golden/receipt_empty.json | 2 +- tests/golden/receipt_reasoning.json | 2 +- tests/golden/receipt_tuned.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nano/__init__.py b/nano/__init__.py index f04803b..e04bff9 100644 --- a/nano/__init__.py +++ b/nano/__init__.py @@ -26,4 +26,4 @@ artifact cannot change behavior because a transitive dependency did. """ -__version__ = "1.0.11" +__version__ = "1.0.12" diff --git a/pyproject.toml b/pyproject.toml index 98ad190..e6689e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "aether-nano" -version = "1.0.11" +version = "1.0.12" description = "A deterministic strategy DSL and reference runtime for host-governed decision rules." readme = "README.md" license = { text = "MIT" } diff --git a/tests/golden/receipt_drift.json b/tests/golden/receipt_drift.json index 78b6147..436eda3 100644 --- a/tests/golden/receipt_drift.json +++ b/tests/golden/receipt_drift.json @@ -1 +1 @@ -{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","log.append"],"irVersion":"1.0.0","module":"Drift","moduleHash":"sha256:f43458398231b977af7a1114b0e0e7760cd18a09910c9e32d178f7d8d1e45c24","nanoVersion":"1.0.11","reasoningRequired":false,"tier":"nano","warmupDeclared":2},"inputs":{"bars":5,"firstTimestamp":0,"frameHash":"sha256:173b1650bc76a5efe8f882e3695d2b03047fc813ac4ddd51eaafbbd576dd7f8f","lastTimestamp":240,"signals":["close"]},"provenance":{"sourceHash":"sha256:308da8d305e125c2e5d35a97bd7e2cd32ab3f255635c5c67116cfc4c09c10bea"},"receiptVersion":1,"run":{"escalations":[],"intents":[{"intent":"OBSERVE","timestamp":120},{"asset":"BTC","confidence":0.7,"intent":"BUY","timestamp":180},{"intent":"OBSERVE","timestamp":240}],"log":[{"detail":"Drift tier=nano effects=['intent.emit', 'log.append'] warmup=2","event":"module.loaded","timestamp":0},{"detail":"n6 has no value at bar 0","event":"condition.unwarmed","timestamp":0},{"detail":"n6 has no value at bar 1","event":"condition.unwarmed","timestamp":60},{"detail":"n6 -> False","event":"condition.evaluated","timestamp":120},{"detail":"OBSERVE asset=None","event":"intent.emitted","timestamp":120},{"detail":"n6 -> True","event":"condition.evaluated","timestamp":180},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":180},{"detail":"n6 -> False","event":"condition.evaluated","timestamp":240},{"detail":"OBSERVE asset=None","event":"intent.emitted","timestamp":240}],"warmupBarsSkipped":2}} \ No newline at end of file +{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","log.append"],"irVersion":"1.0.0","module":"Drift","moduleHash":"sha256:f43458398231b977af7a1114b0e0e7760cd18a09910c9e32d178f7d8d1e45c24","nanoVersion":"1.0.12","reasoningRequired":false,"tier":"nano","warmupDeclared":2},"inputs":{"bars":5,"firstTimestamp":0,"frameHash":"sha256:173b1650bc76a5efe8f882e3695d2b03047fc813ac4ddd51eaafbbd576dd7f8f","lastTimestamp":240,"signals":["close"]},"provenance":{"sourceHash":"sha256:308da8d305e125c2e5d35a97bd7e2cd32ab3f255635c5c67116cfc4c09c10bea"},"receiptVersion":1,"run":{"escalations":[],"intents":[{"intent":"OBSERVE","timestamp":120},{"asset":"BTC","confidence":0.7,"intent":"BUY","timestamp":180},{"intent":"OBSERVE","timestamp":240}],"log":[{"detail":"Drift tier=nano effects=['intent.emit', 'log.append'] warmup=2","event":"module.loaded","timestamp":0},{"detail":"n6 has no value at bar 0","event":"condition.unwarmed","timestamp":0},{"detail":"n6 has no value at bar 1","event":"condition.unwarmed","timestamp":60},{"detail":"n6 -> False","event":"condition.evaluated","timestamp":120},{"detail":"OBSERVE asset=None","event":"intent.emitted","timestamp":120},{"detail":"n6 -> True","event":"condition.evaluated","timestamp":180},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":180},{"detail":"n6 -> False","event":"condition.evaluated","timestamp":240},{"detail":"OBSERVE asset=None","event":"intent.emitted","timestamp":240}],"warmupBarsSkipped":2}} \ No newline at end of file diff --git a/tests/golden/receipt_empty.json b/tests/golden/receipt_empty.json index cc8a0cb..b38db8b 100644 --- a/tests/golden/receipt_empty.json +++ b/tests/golden/receipt_empty.json @@ -1 +1 @@ -{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","log.append"],"irVersion":"1.0.0","module":"Drift","moduleHash":"sha256:f43458398231b977af7a1114b0e0e7760cd18a09910c9e32d178f7d8d1e45c24","nanoVersion":"1.0.11","reasoningRequired":false,"tier":"nano","warmupDeclared":2},"inputs":{"bars":0,"frameHash":"sha256:fad6747b2406c3e3e67ed553f010625725bc2f910129ed7479a1e50d5e9fdbea","signals":["close"]},"provenance":{"sourceHash":"sha256:308da8d305e125c2e5d35a97bd7e2cd32ab3f255635c5c67116cfc4c09c10bea"},"receiptVersion":1,"run":{"escalations":[],"intents":[],"log":[{"detail":"Drift tier=nano effects=['intent.emit', 'log.append'] warmup=2","event":"module.loaded","timestamp":0}],"warmupBarsSkipped":0}} \ No newline at end of file +{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","log.append"],"irVersion":"1.0.0","module":"Drift","moduleHash":"sha256:f43458398231b977af7a1114b0e0e7760cd18a09910c9e32d178f7d8d1e45c24","nanoVersion":"1.0.12","reasoningRequired":false,"tier":"nano","warmupDeclared":2},"inputs":{"bars":0,"frameHash":"sha256:fad6747b2406c3e3e67ed553f010625725bc2f910129ed7479a1e50d5e9fdbea","signals":["close"]},"provenance":{"sourceHash":"sha256:308da8d305e125c2e5d35a97bd7e2cd32ab3f255635c5c67116cfc4c09c10bea"},"receiptVersion":1,"run":{"escalations":[],"intents":[],"log":[{"detail":"Drift tier=nano effects=['intent.emit', 'log.append'] warmup=2","event":"module.loaded","timestamp":0}],"warmupBarsSkipped":0}} \ No newline at end of file diff --git a/tests/golden/receipt_reasoning.json b/tests/golden/receipt_reasoning.json index 538100e..de0e7df 100644 --- a/tests/golden/receipt_reasoning.json +++ b/tests/golden/receipt_reasoning.json @@ -1 +1 @@ -{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","llm.call","llmre.escalate","log.append"],"irVersion":"1.0.0","module":"Ask","moduleHash":"sha256:64f6b7772c76b293a9e5a8ddd3ddf6cb51e5fb1370cd6cd3979a5f3b6a0eabbc","nanoVersion":"1.0.11","reasoningRequired":true,"tier":"nano+","warmupDeclared":0},"inputs":{"bars":2,"firstTimestamp":0,"frameHash":"sha256:dad557a402d06845035c88d772fac0be496eb9a7099fa628a45cda7402142604","lastTimestamp":60,"signals":["close"]},"provenance":{"sourceHash":"sha256:31491116b0c6c727dedf4c7c71ce8c64d37a1d4869c7f35058ef5bf7cae2ae78"},"receiptVersion":1,"run":{"escalations":[{"escalate":"Desk","isAgent":true,"reason":"rule body","timestamp":60}],"intents":[{"asset":"BTC","intent":"BUY","timestamp":0}],"log":[{"detail":"Ask tier=nano+ effects=['intent.emit', 'llm.call', 'llmre.escalate', 'log.append'] warmup=0","event":"module.loaded","timestamp":0},{"detail":"Judge -> ['score']","event":"infer.called","timestamp":0},{"detail":"Judge -> ['score']","event":"infer.called","timestamp":60},{"detail":"n9 -> True","event":"condition.evaluated","timestamp":0},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":0},{"detail":"n9 -> False","event":"condition.evaluated","timestamp":60},{"detail":"Desk: rule body","event":"llmre.escalated","timestamp":60}],"warmupBarsSkipped":0}} \ No newline at end of file +{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","llm.call","llmre.escalate","log.append"],"irVersion":"1.0.0","module":"Ask","moduleHash":"sha256:64f6b7772c76b293a9e5a8ddd3ddf6cb51e5fb1370cd6cd3979a5f3b6a0eabbc","nanoVersion":"1.0.12","reasoningRequired":true,"tier":"nano+","warmupDeclared":0},"inputs":{"bars":2,"firstTimestamp":0,"frameHash":"sha256:dad557a402d06845035c88d772fac0be496eb9a7099fa628a45cda7402142604","lastTimestamp":60,"signals":["close"]},"provenance":{"sourceHash":"sha256:31491116b0c6c727dedf4c7c71ce8c64d37a1d4869c7f35058ef5bf7cae2ae78"},"receiptVersion":1,"run":{"escalations":[{"escalate":"Desk","isAgent":true,"reason":"rule body","timestamp":60}],"intents":[{"asset":"BTC","intent":"BUY","timestamp":0}],"log":[{"detail":"Ask tier=nano+ effects=['intent.emit', 'llm.call', 'llmre.escalate', 'log.append'] warmup=0","event":"module.loaded","timestamp":0},{"detail":"Judge -> ['score']","event":"infer.called","timestamp":0},{"detail":"Judge -> ['score']","event":"infer.called","timestamp":60},{"detail":"n9 -> True","event":"condition.evaluated","timestamp":0},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":0},{"detail":"n9 -> False","event":"condition.evaluated","timestamp":60},{"detail":"Desk: rule body","event":"llmre.escalated","timestamp":60}],"warmupBarsSkipped":0}} \ No newline at end of file diff --git a/tests/golden/receipt_tuned.json b/tests/golden/receipt_tuned.json index 74b94de..3505a33 100644 --- a/tests/golden/receipt_tuned.json +++ b/tests/golden/receipt_tuned.json @@ -1 +1 @@ -{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","log.append"],"irVersion":"1.0.0","module":"Tuned","moduleHash":"sha256:9bac1ba07ba7516c289efeac8bbd55b8d93202ba983e6127fa2ab59d7b00724f","nanoVersion":"1.0.11","params":[{"name":"window","type":"int","value":3}],"reasoningRequired":false,"tier":"nano","warmupDeclared":2},"inputs":{"bars":5,"firstTimestamp":0,"frameHash":"sha256:fe7d16ee1e9ce451bbbf20366d16d6cbd0987aed76fef08f41777ff4ce7035ab","lastTimestamp":240,"signals":["close"]},"provenance":{"sourceHash":"sha256:c243804836eecc54c7893626032216ff0b6a808738916af343d339232ad6cfcf"},"receiptVersion":1,"run":{"escalations":[],"intents":[{"asset":"BTC","intent":"BUY","timestamp":120},{"asset":"BTC","intent":"BUY","timestamp":180},{"asset":"BTC","intent":"BUY","timestamp":240}],"log":[{"detail":"Tuned tier=nano effects=['intent.emit', 'log.append'] warmup=2","event":"module.loaded","timestamp":0},{"detail":"n5 has no value at bar 0","event":"condition.unwarmed","timestamp":0},{"detail":"n5 has no value at bar 1","event":"condition.unwarmed","timestamp":60},{"detail":"n5 -> True","event":"condition.evaluated","timestamp":120},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":120},{"detail":"n5 -> True","event":"condition.evaluated","timestamp":180},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":180},{"detail":"n5 -> True","event":"condition.evaluated","timestamp":240},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":240}],"warmupBarsSkipped":2}} \ No newline at end of file +{"identity":{"compiler":{"name":"nnc","version":"1.0.0"},"effects":["intent.emit","log.append"],"irVersion":"1.0.0","module":"Tuned","moduleHash":"sha256:9bac1ba07ba7516c289efeac8bbd55b8d93202ba983e6127fa2ab59d7b00724f","nanoVersion":"1.0.12","params":[{"name":"window","type":"int","value":3}],"reasoningRequired":false,"tier":"nano","warmupDeclared":2},"inputs":{"bars":5,"firstTimestamp":0,"frameHash":"sha256:fe7d16ee1e9ce451bbbf20366d16d6cbd0987aed76fef08f41777ff4ce7035ab","lastTimestamp":240,"signals":["close"]},"provenance":{"sourceHash":"sha256:c243804836eecc54c7893626032216ff0b6a808738916af343d339232ad6cfcf"},"receiptVersion":1,"run":{"escalations":[],"intents":[{"asset":"BTC","intent":"BUY","timestamp":120},{"asset":"BTC","intent":"BUY","timestamp":180},{"asset":"BTC","intent":"BUY","timestamp":240}],"log":[{"detail":"Tuned tier=nano effects=['intent.emit', 'log.append'] warmup=2","event":"module.loaded","timestamp":0},{"detail":"n5 has no value at bar 0","event":"condition.unwarmed","timestamp":0},{"detail":"n5 has no value at bar 1","event":"condition.unwarmed","timestamp":60},{"detail":"n5 -> True","event":"condition.evaluated","timestamp":120},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":120},{"detail":"n5 -> True","event":"condition.evaluated","timestamp":180},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":180},{"detail":"n5 -> True","event":"condition.evaluated","timestamp":240},{"detail":"BUY asset=BTC","event":"intent.emitted","timestamp":240}],"warmupBarsSkipped":2}} \ No newline at end of file