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
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ and trade your own account with your own capital at no cost. See
- First complete PineScript v6 → C++ transpiler with a real support checker (to
our knowledge).

## What this owns

This repository owns **Pine → C++ translation only**. It turns a Pine v6 script
into a `GeneratedStrategy`: the indicator math plus the `strategy.entry` /
`exit` / `close` / … calls, emitted on the engine's
`pineforge::source::PineStrategyHost` with code that attaches the engine's Pine
execution adapter.

It does **not** own execution semantics. Order lifecycle, bracket legs,
fill-price and slippage rules, `process_orders_on_close` / `calc_on_order_fills`,
margin revival and trail/stop behaviour — everything TradingView parity depends
on at run time — live in the engine's source-adapter runtime
([`src/source/`](https://github.com/pineforge-4pass/pineforge-engine/tree/main/src/source)),
which maps them onto the engine's Pine-agnostic kernel. See the engine's
[architecture notes](https://github.com/pineforge-4pass/pineforge-engine#architecture-kernel-vs-parity).

---

## Install
Expand Down Expand Up @@ -182,6 +198,10 @@ pine source
└─ 5. CodeGen → C++ source string
```

The emitted `GeneratedStrategy` does not execute orders itself: its
`strategy.*` calls go to the engine's Pine execution adapter, which it attaches
in its constructor.

## Compile & run against the engine

The emitted C++ targets the C-ABI in `<pineforge/pineforge.h>`. To build and run
Expand Down Expand Up @@ -256,10 +276,17 @@ hosted/embedded use. Email **luis@4pass.com.tw** with your use case for a quote.

## Explicit Pine execution attachment

The Pine execution adapter is the engine's full Pine execution runtime
(`PineExecutionAdapter` and `PineStrategyHost` in the engine's `src/source/`):
order lifecycle, bracket legs, fill-price and slippage rules, POOC /
`calc_on_order_fills`, margin revival, trail/stop semantics, the intraday caps
and the retained-parent priority rule. Codegen's job is to emit the strategy
that attaches it and the `strategy.*` calls it executes, against a matching
engine ABI.

Generated constructors configure their `PineStrategyConfig` before host
metadata and select `attach_pine_execution_adapter()` when
`PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1` is available.
Its current scope is the Pine intraday cap and retained-parent priority rule.
A guarded `enable_pine_intraday_cap()` fallback supports existing cap-only
engines; engines with neither capability keep their established defaults.
Risk statements remain in source execution order. This bridge requires matching
Expand Down
5 changes: 5 additions & 0 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ def _emit_includes(self, lines: list[str]) -> None:
# byte-identical — mirrors the matrix.hpp gating above.
if getattr(self, "_uses_drawing", False):
lines.append('#include <pineforge/drawing.hpp>')
lines.extend([
"#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1",
'#error "generated code requires pineforge-engine native lowering v1 (PINEFORGE_HAS_NATIVE_LOWERING_V1)"',
"#endif",
])
lines.append("")
# Compatibility shim for the namespace-wrap refactor: unqualified
# references to BacktestEngine / Bar / na<T>() / ta::* / etc. resolve
Expand Down
4 changes: 2 additions & 2 deletions pineforge_codegen/codegen/visit_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -2555,8 +2555,8 @@ def _visit_strategy_call(self, func_name: str, node: FuncCall) -> str:
return (f"strategy_exit({exit_id}, {from_id}, {limit_val}, {stop_val}, "
f"{trail_pts}, {trail_off}, {trail_pr}, {qty_pct}, {comment}, "
f"{qty_val}, {oca_val}, {profit_ticks}, {loss_ticks})")
close_comment = self._visit_expr(comment_n) if comment_n is not None else '""'
return f"strategy_close({exit_id}, {close_comment})"
comment = self._visit_expr(comment_n) if comment_n is not None else '""'
return f"strategy_exit_cancel_bracket({exit_id}, {from_id}, {comment})"

if func_name == "cancel":
p = self._resolve_func_args(node, "strategy.close") # same shape: id first
Expand Down
2 changes: 1 addition & 1 deletion pineforge_codegen/codegen/visit_expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ def _visit_member_access(self, node: MemberAccess) -> str:
if node.member == "islast":
return "barstate_islast_"
if node.member == "isnew":
return "is_first_tick_"
return "is_first_tick()"
if node.member == "isconfirmed":
return "is_last_tick_"
if node.member == "ishistory":
Expand Down
3 changes: 3 additions & 0 deletions tests/golden/matrix_eigen_pca.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#include <pineforge/str_utils.hpp>
#include <pineforge/session_time.hpp>
#include <pineforge/matrix.hpp>
#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1
#error "generated code requires pineforge-engine native lowering v1 (PINEFORGE_HAS_NATIVE_LOWERING_V1)"
#endif

using namespace pineforge;

Expand Down
4 changes: 2 additions & 2 deletions tests/test_calc_on_order_fills_codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def test_post_fill_recalc_updates_current_history_slot_but_barstate_stays_new():
fill recalculation. History advancement therefore has a separate runtime
predicate: every rolling member updates its existing current-bar slot when
the ordinary-close checkpoint is restored, while ``barstate.isnew`` keeps
lowering to ``is_first_tick_``.
lowering to ``is_first_tick()``.
"""
cpp = transpile(_HISTORY_ADVANCE_PROBE)
on_bar = cpp.split("void on_source_bar(const Bar& bar) override {", 1)[1].split(
Expand Down Expand Up @@ -229,7 +229,7 @@ def test_post_fill_recalc_updates_current_history_slot_but_barstate_stays_new():

# Mutation guard: coupling barstate.isnew to history advancement would make
# it false during historical fill recalcs, contrary to Pine semantics.
assert "if (is_first_tick_) {" in on_bar
assert "if (is_first_tick()) {" in on_bar
assert "if (history_advances_new_bar()) {\n strategy_entry" not in on_bar


Expand Down
44 changes: 43 additions & 1 deletion tests/test_codegen_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ def test_includes_present():
assert '#include <pineforge/ta.hpp>' in cpp


def test_native_lowering_capability_is_required_once_after_engine_includes():
cpp = _generate('//@version=6\nstrategy("T")\n')
guard = (
"#ifndef PINEFORGE_HAS_NATIVE_LOWERING_V1\n"
'#error "generated code requires pineforge-engine native lowering v1 '
'(PINEFORGE_HAS_NATIVE_LOWERING_V1)"\n'
"#endif"
)
assert cpp.count(guard) == 1
assert cpp.index('#include <pineforge/source/pine_strategy_host.hpp>') < cpp.index(guard)
assert cpp.index(guard) < cpp.index("using namespace pineforge;")


def test_class_structure():
cpp = _generate('//@version=6\nstrategy("T")\n')
assert "class GeneratedStrategy : public pineforge::source::PineStrategyHost" in cpp
Expand Down Expand Up @@ -526,6 +539,35 @@ def test_strategy_exit_forwards_comment_to_runtime():
cpp = _generate(src)
assert 'std::string("stop exit")' in cpp
assert 'strategy_exit(std::string("X"), std::string("Long")' in cpp
assert "strategy_exit_cancel_bracket(" not in cpp


def test_priceless_strategy_exit_cancels_bracket_with_from_entry_and_comment():
cpp = _generate(
'strategy.exit(id="cancel", from_entry="Long", comment="remove bracket")'
)
assert (
'strategy_exit_cancel_bracket(std::string("cancel"), '
'std::string("Long"), std::string("remove bracket"));'
) in cpp
assert "strategy_close(" not in cpp


def test_priceless_strategy_exit_cancels_bracket_with_default_from_entry_and_comment():
cpp = _generate('strategy.exit("cancel")')
assert 'strategy_exit_cancel_bracket(std::string("cancel"), "", "");' in cpp
assert "strategy_close(" not in cpp


def test_strategy_convert_to_account_and_symbol_remain_identity_lowerings():
cpp = _generate(
"to_account = strategy.convert_to_account(close)\n"
"to_symbol = strategy.convert_to_symbol(open)"
)
assert "to_account = (current_bar_.close);" in cpp
assert "to_symbol = (current_bar_.open);" in cpp
assert "strategy_convert_to_account" not in cpp
assert "strategy_convert_to_symbol" not in cpp


def test_strategy_position_size():
Expand Down Expand Up @@ -1555,7 +1597,7 @@ def test_barstate_tick_members_use_runtime_state():
if barstate.isnew and barstate.isconfirmed
strategy.entry("L", strategy.long)
""")
assert "is_first_tick_" in cpp
assert "is_first_tick()" in cpp
assert "is_last_tick_" in cpp


Expand Down
4 changes: 2 additions & 2 deletions tests/test_collection_scope_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ def test_temporal_outer_alias_avoids_user_name_and_routes_subscript() -> None:

def test_unique_local_collection_output_hash_is_stable() -> None:
cpp = transpile(_IDENTITY_SOURCE)
assert len(cpp) == 14048
assert len(cpp) == 14200
assert sha256(cpp.encode()).hexdigest() == (
"3e811b8da0bfa832577a6e261d9f2ee145c6a88368d964d84b9c94cac6ede616"
"95d691b847e65641f38a89c387d5e6638f1061b0c0486e93d50b31bb22219184"
)
20 changes: 10 additions & 10 deletions tests/test_map_call_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_duplicate_keyword_argument_is_a_parser_compile_error():
def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift():
cpp = transpile(_VALID_EXISTING_FORMS)
assert sha256(cpp.encode()).hexdigest() == (
"6d8475b06b3393521b6be96296c9791aee166380117be0347279869305e7fff6"
"29350cf5f3c01e8b3009a2c24c3dde7b0def653b1d7586a249027ff24a8fb0d1"
)


Expand All @@ -78,7 +78,7 @@ def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift():
map.get("key")
observed = probe(map.new<string, int>())
''',
"c3e1ad52f99ed628a7692606e9aef13c50b6dc22ffa9714d0bf71ccdc8060434",
"75f3301f5bc255c39e176d13577e9347c000c140ba7aee6e02cedf1f2252f732",
),
(
'''//@version=6
Expand All @@ -89,7 +89,7 @@ def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift():
map.get("key")
observed = probe()
''',
"69048a24494dc0d684aabecfbffc5b86aba80ce3c26cbae6848e4ede5ca5bda6",
"133698bbd81b744c544b444b3aebee239d36bf366694267f770fed5916c41d91",
),
(
'''//@version=6
Expand All @@ -98,7 +98,7 @@ def test_valid_existing_positional_and_typed_keyword_forms_do_not_drift():
map.put("key", 1)
observed = map.get("key")
''',
"2f6db3cfce2f6b908fc6c4485c0187a6418e3599a94be678f4bcb43e1e2b3b90",
"b6cc20d55c958298066f641d8558ea74e58ba8ed73fe2d8b60b3e395134f9777",
),
],
)
Expand Down Expand Up @@ -141,7 +141,7 @@ def test_security_timeframe_clone_keeps_preceding_map_namespace_source_order():
)

assert sha256(cpp.encode()).hexdigest() == (
"74f31ea3937aed127eed7641d0d9a9f23c64e5c746e09596a8a284799f9b3686"
"72a6cd9615311ffa3ef636f6a2b51ac4d23b4cee9565e5c4b979bc54d5f1fa79"
)


Expand All @@ -159,7 +159,7 @@ def test_security_timeframe_clone_keeps_visible_map_receiver_source_order():
cpp = transpile(source, filename="synthetic-lexical-map-source-order.pine")

assert sha256(cpp.encode()).hexdigest() == (
"5a087945ff76c28afd2112c7441c43e34436b9f60febf11f37204547a23438ed"
"9c57c2ef6a85ab578b9261d5dc2052c1aa8ccf5718148fbb03c02038a2068680"
)


Expand Down Expand Up @@ -204,22 +204,22 @@ def test_security_timeframe_clone_keeps_visible_map_receiver_source_order():
[
(
_LATER_GLOBAL_MAP_SOURCE,
"bc28346812b8fb9427e9394f81fe51ca59565fae6daa505ce09cc55b51a1f042",
"4be34d0a72fc017a45c8af577c61cf00e756f750aae3758d6fbf51c0994c1339",
34.0,
),
(
_NESTED_LEXICAL_MAP_ROOT_SOURCE,
"b305db152684f9e7496f051f075880cbeed620775d5fbb63beb7d765f2091d04",
"53fac635abe32e0ced972b59d8dad2cddd36475b9d8499838a24f4da21174f14",
7.0,
),
(
_BLOCK_LOCAL_MAP_ISOLATION_SOURCE,
"1211d22c8be706089ca26b687259aad3a5ac693621eacc6a6ae0a5a23b72fd1e",
"e3da0e24d4d4f5044ff442139f974262599bada534cea8a240c2884517d3cc3e",
923.0,
),
(
_FOR_BLOCK_LOCAL_MAP_SOURCE,
"aa2d0b0195b24dfac6e0dada1f511e3614d25c65a9f1a51b21766c5d4daa23af",
"f8aec0239b2825e34212cbf90aae87fff051171e0a5b27373569e06448bdb19e",
8.0,
),
],
Expand Down
4 changes: 2 additions & 2 deletions tests/test_map_terminal_returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def test_map_terminal_return_forms_compile():
def test_nonterminal_pinemap_output_hash_is_stable():
cpp = transpile(_NONTERMINAL_SOURCE)
assert sha256(cpp.encode()).hexdigest() == (
"5e3d7b6dc5790842e39bef36eb932d1a6b0596baab068c8ffe2294d277c3a5bf"
"441f37c04025dfc2771de3ae507d1760f9505676bb5055fe2431925bc54d3103"
)


Expand Down Expand Up @@ -425,5 +425,5 @@ def test_invalid_terminal_map_shapes_raise_compile_errors():
def test_unresolved_parameter_keeps_lexical_precedence_over_global_map():
cpp = transpile(_SHADOWED_UNRESOLVED_PARAM_SOURCE)
assert sha256(cpp.encode()).hexdigest() == (
"135b9183598b99e6461d1f58b9fa7965c7283abcc21b83ea226e50373aa90885"
"95b2b176ea93b27c467b805193c2d2098d70988e420aaf75b2f22bcc05760769"
)
2 changes: 1 addition & 1 deletion tests/test_pinemap_boundaries_and_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def test_non_map_user_call_remains_exact_baseline_bytes() -> None:
cpp = transpile(source)
assert "__pf_call_arg_" not in cpp
assert sha256(cpp.encode()).hexdigest() == (
"f448daf0f4ded4554d117c74414ea6325e189ff70b3c7f1471c3072a7477c997"
"a876072a5b2827875f3ff271690d22213cd859681bdd832f7e8edc0e3f13ad01"
)


Expand Down
2 changes: 1 addition & 1 deletion tests/test_pinemap_semantics.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ def test_non_map_cpp_remains_exact_baseline_bytes() -> None:
# Whole-output pin includes the generated source-host constructor and
# lifecycle reset. The ordinary non-map lowering remains unchanged.
assert sha256(cpp.encode()).hexdigest() == (
"1d822b51179dfff05d5b1ecc9b2bfdb7ae2e33b2d497463b96c9f4d6b9b30a38"
"5979a0d3a4465192b456635c5dfb590358c353d547ec80de368e54629b807afb"
)
assert '#include <pineforge/map.hpp>' not in cpp
assert "_PFCheckpointTraits" not in cpp
Expand Down
Loading