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
1 change: 1 addition & 0 deletions benchmarks/reference/signals/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":"4c3150439cc45b282919562bce0e9273ab87a9b14022413b34133ac8fccf3249","input":{"columns":["score","exit_mask"],"dtypes":{"exit_mask":"boolean","score":"float64"},"rows":[[-2.0,null],[-0.5,false],[0.5,true],[1.5,true],[2.0,false],[2.5,true],[null,false],[0.0,null]]},"output":{"columns":["score","exit_mask","enter_tag","enter_long","enter_short","exit_tag","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,null,"",0,1,"",0,0],[-0.5,false,"",0,1,"",0,0],[0.5,true,"",1,0,"",0,1],[1.5,true,"",1,0,"",0,1],[2.0,false,"",0,0,"",1,0],[2.5,true,"",0,0,"",1,1],[null,false,"",0,0,"",0,0],[0.0,null,"",0,0,"",0,0]]},"schema_version":"freqtrade-signal-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/SignalProgramContract.py","strategy_sha256":"d3e1316e336273490b2da8dcfefc5a8ecca64046250a9225971910d78a3e1746","version":"2026.5.1"}}
22 changes: 22 additions & 0 deletions benchmarks/reference/strategies/SignalProgramContract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from freqtrade.strategy import IStrategy


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

def populate_entry_trend(self, dataframe, metadata):
dataframe.loc[:, ["enter_long", "enter_short"]] = (0, 0)
positive = dataframe["score"] > 0
dataframe.loc[positive, "enter_long"] = 1
dataframe.loc[dataframe["score"] >= 2, "enter_long"] = 0
dataframe["enter_short"] = (dataframe["score"] < 0).astype(int)
return dataframe

def populate_exit_trend(self, dataframe, metadata):
dataframe.loc[:, ["exit_long", "exit_short"]] = 0
dataframe.loc[
(dataframe["enter_long"] == 0) & (dataframe["score"] > 1),
"exit_long",
] = 1
dataframe.loc[dataframe["exit_mask"], "exit_short"] = 1
return dataframe
45 changes: 45 additions & 0 deletions docs/native-signal-program.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Native Signal Program

`signal-program-v1` is the versioned contract between NFI's Python signal source and the
Native vector runtime. It compiles source; it does not execute strategy Python.

## Exact boundary

The compiler processes `populate_entry_trend` and then `populate_exit_trend`, matching
Freqtrade 2026.5.1. It records every full-column or masked `.loc` write as an ordered
`frame-write` node. Each node depends on the prior DataFrame version, so overlapping masks,
read-after-write, and last-write-wins behavior cannot be reordered by an optimizer.

M21-01 owns only the four raw numeric columns:

- `enter_long`
- `enter_short`
- `exit_long`
- `exit_short`

Signal values remain raw through this stage. Freqtrade opens a new position only when the
corresponding value is exactly numeric `1`; arbitrary nonzero values are not promoted to
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.

## Evidence and regeneration

The committed oracle at
`benchmarks/reference/signals/freqtrade-2026.5.1.json` executes the exact pinned
`IStrategy.advise_entry` and `advise_exit` source around the compact contract strategy. It
covers entry-to-exit dependencies, nullable Boolean masks, overlapping writes, all four
directions, and raw dtype preservation.

```bash
uv run python scripts/generate_signal_fixture.py
uv run pytest -q tests/test_signal_program.py tests/test_signal_fixture.py
nfi-bte strategy signal-program strategy.py --class Strategy --output signal-program.json
```

Unsupported masks, assignments, phase-crossing writes, and dynamic behavior stop with a
source location. Signal numbers, strategy names, pairs, timeranges, and source hashes are
never runtime branches; the source hash is evidence identity only.
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": 133,
"updated_at": "2026-08-11T02:31:11+09:00",
"revision": 135,
"updated_at": "2026-08-11T15:54:39+09:00",
"acceptance_commands": "planning/acceptance-commands.json",
"active_task_id": null,
"execution_policy": {
Expand Down Expand Up @@ -4804,7 +4804,7 @@
"order": 2101,
"milestone": "M21",
"title": "Define and compile signal-program-v1",
"status": "pending",
"status": "completed",
"depends_on": [
"M20-05"
],
Expand All @@ -4823,10 +4823,17 @@
"signal schema tests",
"entry and exit mutation tests"
],
"started_at": null,
"completed_at": null,
"commit_sha": null,
"evidence": [],
"started_at": "2026-08-11T03:03:16+09:00",
"completed_at": "2026-08-11T15:54:39+09:00",
"commit_sha": "b2255b57fb47cec88c2d97308a77cfa5f891f9ac",
"evidence": [
".nfi/roadmap-acceptance/M21-01/b2255b57fb47cec88c2d97308a77cfa5f891f9ac/acceptance-report.json",
".nfi/roadmap-acceptance/M21-01/b2255b57fb47cec88c2d97308a77cfa5f891f9ac/latest-signal-program-gap.json",
".nfi/roadmap-acceptance/M21-01/b2255b57fb47cec88c2d97308a77cfa5f891f9ac/SHA256SUMS.txt",
".nfi/roadmap-acceptance/M21-01/b2255b57fb47cec88c2d97308a77cfa5f891f9ac/x7-spot/run.json",
".nfi/roadmap-acceptance/M21-01/b2255b57fb47cec88c2d97308a77cfa5f891f9ac/x7-futures/run.json",
"benchmarks/reference/signals/freqtrade-2026.5.1.json"
],
"blocker": null
},
{
Expand Down Expand Up @@ -8401,6 +8408,54 @@
"next_eligible_task": "M21-01",
"next_task_manual_gate": false
}
},
{
"sequence": 181,
"timestamp": "2026-08-11T03:03:16+09:00",
"task_id": "M21-01",
"event": "task_started",
"details": {
"objective": "define and compile signal-program-v1 with exact dataframe assignment and source-order overwrite semantics",
"signal_number_execution_branches_allowed": false,
"unordered_assignment_evaluation_allowed": false,
"entry_and_exit_long_short_required": true,
"original_source_order_must_be_preserved": true,
"latest_upstream_commit": "897a1523391b8222ee711eba9714b59a3e77265a"
}
},
{
"sequence": 182,
"timestamp": "2026-08-11T15:54:39+09:00",
"task_id": "M21-01",
"event": "task_completed",
"details": {
"implementation_commit": "b2255b57fb47cec88c2d97308a77cfa5f891f9ac",
"latest_upstream_commit": "897a1523391b8222ee711eba9714b59a3e77265a",
"latest_strategy_sha256": "e99b4f58ecf507da86d2ba94e641c2994f17a1456133c2cff26946fa6b4a1afb",
"freqtrade_version": "2026.5.1",
"signal_oracle_row_count": 8,
"signal_oracle_fingerprint": "4c3150439cc45b282919562bce0e9273ab87a9b14022413b34133ac8fccf3249",
"signal_program_node_count": 38,
"signal_mutation_count": 7,
"entry_then_exit_order_exact": true,
"overlapping_mask_last_write_exact": true,
"nullable_boolean_mask_exact": true,
"freqtrade_exact_numeric_one_semantics": true,
"python_test_count": 786,
"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,
"signal_number_execution_branches_added": false,
"latest_x7_fully_compiled": false,
"latest_x7_fail_closed_source": "strategy.py:13034:13",
"tag_program_complete": false,
"rust_vector_shadow_complete": false,
"full_native_strategy_claim": false,
"next_eligible_task": "M21-02",
"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 @@ -692,6 +692,18 @@ def build_parser() -> argparse.ArgumentParser:
strategy_indicator_program.add_argument("source", type=Path)
strategy_indicator_program.add_argument("--class", dest="class_name")
strategy_indicator_program.add_argument("--output", "-o", type=Path, required=True)
strategy_signal_program = strategy_commands.add_parser(
"signal-program",
help="compile ordered entry and exit mutations into signal-program-v1",
)
strategy_signal_program.add_argument("source", type=Path)
strategy_signal_program.add_argument("--class", dest="class_name")
strategy_signal_program.add_argument(
"--trading-mode",
choices=("spot", "futures"),
default="spot",
)
strategy_signal_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 @@ -311,6 +311,24 @@ def _execute_strategy(args: argparse.Namespace) -> int:
)
print(f"indicator program report: {args.output}")
return 0
if args.strategy_command == "signal-program":
from ..signal_program import compile_signal_program

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

Expand Down
2 changes: 1 addition & 1 deletion python/nfi_backtest_engine/generic_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ def _surface_trade(


def _enabled(value: Any) -> bool:
return not pd.isna(value) and float(value) != 0.0
return not pd.isna(value) and bool(value == 1)


def _optional_text(value: Any) -> str | None:
Expand Down
Loading