Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions benchmarks/reference/strategies/TagProgramContract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from freqtrade.strategy import IStrategy


class TagProgramContract(IStrategy):
timeframe = "5m"

def populate_entry_trend(self, dataframe, metadata):
dataframe.loc[:, ["enter_long", "enter_short"]] = (0, 0)
long_route = 101
short_route = 562
long_mask = dataframe["score"] >= 0
short_mask = dataframe["score"] <= 0
dataframe.loc[long_mask, "enter_long"] = 1
dataframe.loc[long_mask, "enter_tag"] += f"{long_route} "
dataframe.loc[short_mask, "enter_short"] = 1
dataframe.loc[short_mask, "enter_tag"] += f"{short_route} "
override_mask = dataframe["score"] >= 2
dataframe.loc[override_mask, "enter_tag"] = "override "
dataframe.loc[override_mask, "enter_tag"] += "final "
return dataframe

def populate_exit_trend(self, dataframe, metadata):
dataframe.loc[:, ["exit_long", "exit_short"]] = (0, 0)
long_mask = (dataframe["enter_long"] == 1) & (dataframe["score"] > 1)
dataframe.loc[long_mask, ["exit_long", "exit_tag"]] = (1, "profit ")
dataframe.loc[dataframe["exit_mask"], "exit_short"] = 1
dataframe.loc[dataframe["exit_mask"], "exit_tag"] += "signal "
return dataframe
1 change: 1 addition & 0 deletions benchmarks/reference/tags/freqtrade-2026.5.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"call_order":["advise_entry","advise_exit"],"fingerprint":"11219ab2fae512e0e833f05c323a5cb8e47a724154a9d50ac126c6d8dbadba77","input":{"columns":["score","exit_mask","enter_tag","exit_tag"],"dtypes":{"enter_tag":"str","exit_mask":"boolean","exit_tag":"str","score":"float64"},"rows":[[-2.0,false,"stale-entry","stale-exit"],[-0.5,true,"stale-entry","stale-exit"],[0.0,false,"stale-entry","stale-exit"],[0.5,true,"stale-entry","stale-exit"],[1.5,false,"stale-entry","stale-exit"],[2.0,true,"stale-entry","stale-exit"],[2.5,null,"stale-entry","stale-exit"],[null,false,"stale-entry","stale-exit"]]},"output":{"columns":["score","exit_mask","enter_tag","exit_tag","enter_long","enter_short","exit_long","exit_short"],"dtypes":{"enter_long":"int64","enter_short":"int64","enter_tag":"str","exit_long":"int64","exit_mask":"boolean","exit_short":"int64","exit_tag":"str","score":"float64"},"rows":[[-2.0,false,"562 ","",0,1,0,0],[-0.5,true,"562 ","signal ",0,1,0,1],[0.0,false,"101 562 ","",1,1,0,0],[0.5,true,"101 ","signal ",1,0,0,1],[1.5,false,"101 ","profit ",1,0,1,0],[2.0,true,"override final ","profit signal ",1,0,1,1],[2.5,null,"override final ","profit ",1,0,1,0],[null,false,"","",0,0,0,0]]},"schema_version":"freqtrade-tag-fixture-v1","source":{"commit":"6fa470939cc74bf0672e0e348a4d9b293072e43c","interface":"freqtrade/strategy/interface.py","interface_sha256":"93ddb2f5579acd7a20d489174ffb68cd191428ff996d291b33be81d97fa9bf66","method_sha256":{"advise_entry":"768ac9a3356d6a99b67334932814c727ed29649dd9d5d5220ac1412ee26dba83","advise_exit":"074403ac03325690972e8899878f08e5ee50e20dbfc72115e59b116d2de2caa9"},"pandas":"3.0.3","strategy":"benchmarks/reference/strategies/TagProgramContract.py","strategy_sha256":"c7b42a8b6eb855f438bf0243412faea25694f7e3c2be0b41dd1a2ee67ae31919","version":"2026.5.1"}}
6 changes: 3 additions & 3 deletions docs/native-signal-program.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ corresponding value is exactly numeric `1`; arbitrary nonzero values are not pro
orders. Same-candle long/short/exit conflicts are resolved later by the Freqtrade-compatible
simulation kernel, never by the compiler.

Tag initialization, literal and compound tag generation, and original whitespace are the
separate M21-02 contract. A tag write encountered by the M21-01 compiler therefore fails
closed instead of being discarded or guessed.
Tag initialization, literal and compound tag generation, and original whitespace belong to the
separate [`tag-program-v1`](native-tag-program.md) contract. A tag write encountered by the
signal-only compiler still fails closed instead of being discarded or guessed.

## Evidence and regeneration

Expand Down
41 changes: 41 additions & 0 deletions docs/native-tag-program.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Native Tag Program

`tag-program-v1` is the versioned contract for exact NFI entry and exit tag generation. It
compiles strategy source into ordered data; it does not execute NFI Python in the Native lane.

## Exact contract

The runtime mirrors Freqtrade 2026.5.1 by initializing `enter_tag` to `""` immediately before
`populate_entry_trend`, then initializing `exit_tag` immediately before
`populate_exit_trend`. Strategy writes retain source order and each write depends on the prior
DataFrame version. Mask overlap, last-write-wins assignment, and compound append therefore
cannot be reordered.

Raw strings are preserved byte-for-byte, including repeated and trailing whitespace. NFI route
lookup uses Python `str.split()` token order without replacing the stored value. For example,
`"101 562 "` remains the trade's original tag while its canonical route is `("101", "562")`.
Signal numbers and tag strings are program data, never Python or Rust execution branches.
Formatted fragments such as `f"{signal_id} "` lower to a generic `format-string` node followed
by an ordered append, so adding another numeric Signal does not require a runtime code branch.

`tag-program-v1` also records numeric signal writes that share the same source function. This
keeps tag masks and read-after-write behavior exact while `signal-program-v1` remains the public
raw-signal contract. M21-03 will run the compiled program independently in Rust and compare every
Indicator, Signal, Tag, and execution-index output.

## Evidence and limits

The committed oracle at `benchmarks/reference/tags/freqtrade-2026.5.1.json` executes the exact
pinned `IStrategy.advise_entry` and `advise_exit` methods. It covers simultaneous Long/Short
matches, assignment priority, literal replacement, compound appends, nullable masks, wrapper
initialization, and original trailing whitespace.

```bash
uv run python scripts/generate_tag_fixture.py
uv run pytest -q tests/test_tag_program.py tests/test_tag_fixture.py
nfi-bte strategy tag-program strategy.py --class Strategy --output tag-program.json
```

Dynamic configuration and loop lowering not yet represented by the source compiler fail closed
with an exact source location. They are not inferred from Signal IDs, source hashes, pairs, or
expected results.
6 changes: 3 additions & 3 deletions docs/native-vector-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ The latest X7 indicator operation set now has exact Native kernels. This does no
yet claim that the entire latest X7 source compiles or executes Full Native.
M20-05 establishes exact multi-timeframe primitives and compiler contracts,
but their in-memory `VectorEngine` connection is deliberately owned by M21.
M21 also owns Native
signal assignments, tag generation, independent Python/Rust vector shadowing,
and the in-memory simulator connection. M22 owns latest-upstream Spot/Futures
M21-01 and M21-02 establish Native signal assignment and exact tag-generation
contracts. M21-03 and M21-04 own independent Python/Rust vector shadowing and
the in-memory simulator connection. M22 owns latest-upstream Spot/Futures
full-state qualification, removal of Python strategy execution from the Native
lane, and release certification. Until those proofs pass, unsupported source
constructs remain fail-closed and the official Freqtrade fallback remains the
Expand Down
69 changes: 62 additions & 7 deletions planning/roadmap-state.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"schema_version": "1.0.0",
"roadmap_id": "nfi-backtest-engine-post-v1.1.0",
"revision": 135,
"updated_at": "2026-08-11T15:54:39+09:00",
"revision": 137,
"updated_at": "2026-08-11T18:31:20+09:00",
"acceptance_commands": "planning/acceptance-commands.json",
"active_task_id": null,
"execution_policy": {
Expand Down Expand Up @@ -4841,7 +4841,7 @@
"order": 2102,
"milestone": "M21",
"title": "Compile exact Native tag generation",
"status": "pending",
"status": "completed",
"depends_on": [
"M21-01"
],
Expand All @@ -4860,10 +4860,18 @@
"tag priority tests",
"compound-tag vector exactness"
],
"started_at": null,
"completed_at": null,
"commit_sha": null,
"evidence": [],
"started_at": "2026-08-11T18:03:14+09:00",
"completed_at": "2026-08-11T18:31:20+09:00",
"commit_sha": "a85ae58bf9f689b03d221bf2ebde03371553fd61",
"evidence": [
".nfi/roadmap-acceptance/M21-02/a85ae58bf9f689b03d221bf2ebde03371553fd61/acceptance-report.json",
".nfi/roadmap-acceptance/M21-02/a85ae58bf9f689b03d221bf2ebde03371553fd61/latest-tag-program-gap.json",
".nfi/roadmap-acceptance/M21-02/a85ae58bf9f689b03d221bf2ebde03371553fd61/tag-program.json",
".nfi/roadmap-acceptance/M21-02/a85ae58bf9f689b03d221bf2ebde03371553fd61/SHA256SUMS.txt",
".nfi/roadmap-acceptance/M21-02/a85ae58bf9f689b03d221bf2ebde03371553fd61/x7-spot/run.json",
".nfi/roadmap-acceptance/M21-02/a85ae58bf9f689b03d221bf2ebde03371553fd61/x7-futures/run.json",
"benchmarks/reference/tags/freqtrade-2026.5.1.json"
],
"blocker": null
},
{
Expand Down Expand Up @@ -8456,6 +8464,53 @@
"next_eligible_task": "M21-02",
"next_task_manual_gate": false
}
},
{
"sequence": 183,
"timestamp": "2026-08-11T18:03:14+09:00",
"task_id": "M21-02",
"event": "task_started",
"details": {
"objective": "compile literal and compound entry and exit tags with exact source-order assignment and original string preservation",
"tag_specific_execution_branches_allowed": false,
"original_tag_strings_must_be_preserved": true,
"compound_tag_order_and_whitespace_exact_required": true,
"official_freqtrade_oracle_required": true,
"latest_upstream_commit": "897a1523391b8222ee711eba9714b59a3e77265a"
}
},
{
"sequence": 184,
"timestamp": "2026-08-11T18:31:20+09:00",
"task_id": "M21-02",
"event": "task_completed",
"details": {
"implementation_commit": "a85ae58bf9f689b03d221bf2ebde03371553fd61",
"latest_upstream_commit": "897a1523391b8222ee711eba9714b59a3e77265a",
"latest_strategy_sha256": "e99b4f58ecf507da86d2ba94e641c2994f17a1456133c2cff26946fa6b4a1afb",
"freqtrade_version": "2026.5.1",
"tag_oracle_row_count": 8,
"tag_oracle_fingerprint": "11219ab2fae512e0e833f05c323a5cb8e47a724154a9d50ac126c6d8dbadba77",
"tag_program_node_count": 51,
"tag_mutation_count": 6,
"formatted_signal_id_is_program_data": true,
"compound_tag_order_exact": true,
"original_tag_whitespace_preserved": true,
"wrapper_initialization_exact": true,
"python_test_count": 800,
"future_compatibility_test_count": 143,
"rust_workspace_test_count": 212,
"spot_trade_surface_and_full_state_exact": true,
"futures_trade_surface_and_full_state_exact": true,
"runtime_hardcoding_added": false,
"tag_specific_execution_branches_added": false,
"latest_x7_fully_compiled": false,
"latest_x7_fail_closed_source": "strategy.py:13034:13",
"rust_vector_shadow_complete": false,
"full_native_strategy_claim": false,
"next_eligible_task": "M21-03",
"next_task_manual_gate": false
}
}
]
}
12 changes: 12 additions & 0 deletions python/nfi_backtest_engine/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,18 @@ def build_parser() -> argparse.ArgumentParser:
default="spot",
)
strategy_signal_program.add_argument("--output", "-o", type=Path, required=True)
strategy_tag_program = strategy_commands.add_parser(
"tag-program",
help="compile exact ordered entry and exit tags into tag-program-v1",
)
strategy_tag_program.add_argument("source", type=Path)
strategy_tag_program.add_argument("--class", dest="class_name")
strategy_tag_program.add_argument(
"--trading-mode",
choices=("spot", "futures"),
default="spot",
)
strategy_tag_program.add_argument("--output", "-o", type=Path, required=True)
strategy_callback_ir = strategy_commands.add_parser(
"callback-ir",
help="compile source-ordered callback routes, tags, and data dependencies",
Expand Down
18 changes: 18 additions & 0 deletions python/nfi_backtest_engine/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,24 @@ def _execute_strategy(args: argparse.Namespace) -> int:
)
print(f"signal program report: {args.output}")
return 0
if args.strategy_command == "tag-program":
from ..tag_program import compile_tag_program

program = compile_tag_program(
args.source,
class_name=args.class_name,
trading_mode=args.trading_mode,
)
write_json(args.output, program)
print(
"tag program: "
f"class={program['selected_class']}, "
f"mode={program['compile_context']['trading_mode']}, "
f"nodes={len(program['nodes'])}, "
f"tag_mutations={len(program['tag_mutation_nodes'])}"
)
print(f"tag program report: {args.output}")
return 0
if args.strategy_command == "callback-ir":
from ..callback_source_ir import compile_callback_source_ir

Expand Down
Loading