Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Loading