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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ cpp = transpile(pine)
print(cpp) # complete C++ source string
```

The output `#include`s `<pineforge/engine.hpp>`, `<pineforge/ta.hpp>`, … and
compiles into a `.so` exposing the engine's documented C-ABI.
The output `#include`s `<pineforge/source/pine_strategy_host.hpp>`, `<pineforge/ta.hpp>`, …; its `GeneratedStrategy` derives from `pineforge::source::PineStrategyHost` and compiles into a `.so` exposing the engine's documented C-ABI.

## Usage

Expand Down Expand Up @@ -257,8 +256,9 @@ hosted/embedded use. Email **luis@4pass.com.tw** with your use case for a quote.

## Explicit Pine execution attachment

Generated constructors select `attach_pine_execution_adapter()` before host
metadata when `PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1` is available.
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.
Expand Down
2 changes: 1 addition & 1 deletion pineforge_codegen/codegen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4066,7 +4066,7 @@ def generate(self) -> str:
self._emit_lazy_source_clock_helper(lines)

# 2. Open class
lines.append("class GeneratedStrategy : public BacktestEngine {")
lines.append("class GeneratedStrategy : public pineforge::source::PineStrategyHost {")
lines.append("public:")
_script_state_decl_start = len(lines)

Expand Down
90 changes: 54 additions & 36 deletions pineforge_codegen/codegen/emit_top.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class TopLevelEmitter:
Mixed into ``CodeGen``; not intended to be instantiated standalone."""

def _emit_includes(self, lines: list[str]) -> None:
lines.append('#include <pineforge/engine.hpp>')
lines.append('#include <pineforge/source/pine_strategy_host.hpp>')
lines.append('#include <pineforge/ta.hpp>')
lines.append('#include <pineforge/math.hpp>')
lines.append('#include <pineforge/series.hpp>')
Expand Down Expand Up @@ -803,22 +803,23 @@ def _emit_constructor(self, lines: list[str]) -> None:
# This is configuration, deliberately outside script-state reset.
ctor_body: list[str] = [
"#if defined(PINEFORGE_HAS_EXPLICIT_PINE_EXECUTION_ADAPTER_V1)",
" pineforge::BacktestEngine::attach_pine_execution_adapter();",
" pineforge::source::PineStrategyHost::attach_pine_execution_adapter();",
"#elif defined(PINEFORGE_HAS_EXPLICIT_PINE_CAP_V1)",
" pineforge::BacktestEngine::enable_pine_intraday_cap();",
" pineforge::source::PineStrategyHost::enable_pine_intraday_cap();",
"#endif",
" pineforge::source::PineStrategyConfig cfg{};",
]
# Strategy params that map to engine members
# Strategy params that map to the source host's config surface.
sp = self.ctx.strategy_params

if sp.get("process_orders_on_close") is True:
ctor_body.append(" process_orders_on_close_ = true;")
ctor_body.append(" cfg.process_orders_on_close = true;")

if sp.get("calc_on_order_fills") is True:
ctor_body.append(" calc_on_order_fills_ = true;")
ctor_body.append(" cfg.calc_on_order_fills = true;")

if "initial_capital" in sp and isinstance(sp["initial_capital"], (int, float)):
ctor_body.append(f" initial_capital_ = {float(sp['initial_capital'])};")
ctor_body.append(f" cfg.initial_capital = {float(sp['initial_capital'])};")

# default_qty_type: strategy.fixed / strategy.percent_of_equity / strategy.cash
qty_type_map = {
Expand All @@ -828,13 +829,15 @@ def _emit_constructor(self, lines: list[str]) -> None:
}
qty_type = sp.get("default_qty_type")
if qty_type in qty_type_map:
ctor_body.append(f" default_qty_type_ = {qty_type_map[qty_type]};")
ctor_body.append(
f" cfg.default_qty_type = static_cast<int>({qty_type_map[qty_type]});"
)

if "default_qty_value" in sp and isinstance(sp["default_qty_value"], (int, float)):
ctor_body.append(f" default_qty_value_ = {float(sp['default_qty_value'])};")
ctor_body.append(f" cfg.default_qty_value = {float(sp['default_qty_value'])};")

if "pyramiding" in sp and isinstance(sp["pyramiding"], int):
ctor_body.append(f" pyramiding_ = {sp['pyramiding']};")
ctor_body.append(f" cfg.pyramiding = {sp['pyramiding']};")

# commission_type: strategy.commission.percent / .cash_per_order / .cash_per_contract
comm_type_map = {
Expand All @@ -844,31 +847,35 @@ def _emit_constructor(self, lines: list[str]) -> None:
}
comm_type = sp.get("commission_type")
if comm_type in comm_type_map:
ctor_body.append(f" commission_type_ = {comm_type_map[comm_type]};")
ctor_body.append(
f" cfg.commission_type = static_cast<int>({comm_type_map[comm_type]});"
)

if "commission_value" in sp and isinstance(sp["commission_value"], (int, float)):
ctor_body.append(f" commission_value_ = {float(sp['commission_value'])};")
ctor_body.append(f" cfg.commission_value = {float(sp['commission_value'])};")

if "slippage" in sp and isinstance(sp["slippage"], (int, float)):
ctor_body.append(f" slippage_ = {int(sp['slippage'])};")
ctor_body.append(f" cfg.slippage = {int(sp['slippage'])};")

# margin_long / margin_short: percent of position value required as
# equity (default 100 = 1x leverage). When required_margin exceeds
# available equity, TV silently rejects the fill — engine mirrors
# this in execute_market_entry's FLAT branch.
if "margin_long" in sp and isinstance(sp["margin_long"], (int, float)):
ctor_body.append(f" margin_long_ = {float(sp['margin_long'])};")
ctor_body.append(f" cfg.margin_long = {float(sp['margin_long'])};")
if "margin_short" in sp and isinstance(sp["margin_short"], (int, float)):
ctor_body.append(f" margin_short_ = {float(sp['margin_short'])};")
ctor_body.append(f" cfg.margin_short = {float(sp['margin_short'])};")

# close_entries_rule: "FIFO" (default) or "ANY"
if sp.get("close_entries_rule") == "ANY":
ctor_body.append(" close_entries_rule_any_ = true;")
ctor_body.append(" cfg.close_entries_rule_any = true;")

# Turn on native source-series history only when the script uses
# input.source — otherwise the engine pays nothing per bar.
if self._script_has_input_source():
ctor_body.append(" _src_series_active_ = true;")
ctor_body.append(" cfg.src_series_active = true;")

ctor_body.append(" configure_pine_strategy(cfg);")

if init_parts and ctor_body:
lines.append(f" explicit GeneratedStrategy() : {', '.join(init_parts)} {{")
Expand All @@ -885,26 +892,37 @@ def _emit_constructor(self, lines: list[str]) -> None:

lines.append("")
lines.append(" void set_strategy_override(const std::string& key, const std::string& value) {")
lines.append(' if (key == "initial_capital") { initial_capital_ = std::stod(value); return; }')
lines.append(' if (key == "commission_value") { commission_value_ = std::stod(value); return; }')
lines.append(' if (key == "default_qty_value") { default_qty_value_ = std::stod(value); return; }')
lines.append(' if (key == "pyramiding") { pyramiding_ = std::stoi(value); return; }')
lines.append(' if (key == "slippage") { slippage_ = std::stoi(value); return; }')
lines.append(' if (key == "process_orders_on_close") { process_orders_on_close_ = (value == "true" || value == "1"); return; }')
lines.append(' if (key == "calc_on_order_fills") { calc_on_order_fills_ = (value == "true" || value == "1"); return; }')
lines.append(' if (key == "close_entries_rule") { close_entries_rule_any_ = (value == "ANY" || value == "any" || value == "1"); return; }')
lines.append(' if (key == "default_qty_type") {')
lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") default_qty_type_ = QtyType::FIXED;')
lines.append(' else if (value == "percent_of_equity" || value == "strategy.percent_of_equity" || value == "1") default_qty_type_ = QtyType::PERCENT_OF_EQUITY;')
lines.append(' else if (value == "cash" || value == "strategy.cash" || value == "2") default_qty_type_ = QtyType::CASH;')
lines.append(" return;")
lines.append(" }")
lines.append(' if (key == "commission_type") {')
lines.append(' if (value == "percent" || value == "strategy.commission.percent" || value == "0") commission_type_ = CommissionType::PERCENT;')
lines.append(' else if (value == "cash_per_order" || value == "strategy.commission.cash_per_order" || value == "1") commission_type_ = CommissionType::CASH_PER_ORDER;')
lines.append(' else if (value == "cash_per_contract" || value == "strategy.commission.cash_per_contract" || value == "2") commission_type_ = CommissionType::CASH_PER_CONTRACT;')
lines.append(" pineforge::source::StrategyOverrides overrides{};")
lines.append(' if (key == "initial_capital") {')
lines.append(" overrides.initial_capital = std::stod(value);")
lines.append(' } else if (key == "commission_value") {')
lines.append(" overrides.commission_value = std::stod(value);")
lines.append(' } else if (key == "default_qty_value") {')
lines.append(" overrides.default_qty_value = std::stod(value);")
lines.append(' } else if (key == "pyramiding") {')
lines.append(" overrides.pyramiding = std::stoi(value);")
lines.append(' } else if (key == "slippage") {')
lines.append(" overrides.slippage = std::stoi(value);")
lines.append(' } else if (key == "process_orders_on_close") {')
lines.append(' overrides.process_orders_on_close = (value == "true" || value == "1");')
lines.append(' } else if (key == "calc_on_order_fills") {')
lines.append(' overrides.calc_on_order_fills = (value == "true" || value == "1");')
lines.append(' } else if (key == "close_entries_rule") {')
lines.append(' overrides.close_entries_rule = (value == "ANY" || value == "any" || value == "1");')
lines.append(' } else if (key == "default_qty_type") {')
lines.append(' if (value == "fixed" || value == "strategy.fixed" || value == "0") overrides.default_qty_type = static_cast<int>(QtyType::FIXED);')
lines.append(' else if (value == "percent_of_equity" || value == "strategy.percent_of_equity" || value == "1") overrides.default_qty_type = static_cast<int>(QtyType::PERCENT_OF_EQUITY);')
lines.append(' else if (value == "cash" || value == "strategy.cash" || value == "2") overrides.default_qty_type = static_cast<int>(QtyType::CASH);')
lines.append(" else return;")
lines.append(' } else if (key == "commission_type") {')
lines.append(' if (value == "percent" || value == "strategy.commission.percent" || value == "0") overrides.commission_type = static_cast<int>(CommissionType::PERCENT);')
lines.append(' else if (value == "cash_per_order" || value == "strategy.commission.cash_per_order" || value == "1") overrides.commission_type = static_cast<int>(CommissionType::CASH_PER_ORDER);')
lines.append(' else if (value == "cash_per_contract" || value == "strategy.commission.cash_per_contract" || value == "2") overrides.commission_type = static_cast<int>(CommissionType::CASH_PER_CONTRACT);')
lines.append(" else return;")
lines.append(" } else {")
lines.append(" return;")
lines.append(" }")
lines.append(" pineforge::source::PineStrategyHost::set_strategy_override(overrides);")
lines.append(" }")

if self._security_eval_info:
Expand Down Expand Up @@ -981,7 +999,7 @@ def _emit_on_bar(self, lines: list[str]) -> None:
self._lexical_udt_types = {}
self._lexical_series_bindings = {}
self._lexical_known_var_tombstones = set()
lines.append(" void on_bar(const Bar& bar) override {")
lines.append(" void on_source_bar(const Bar& bar) override {")

# A GeneratedStrategy handle may execute multiple batch runs or
# streaming lifecycles. BacktestEngine resets broker/base state, but
Expand Down
37 changes: 23 additions & 14 deletions pineforge_codegen/codegen/visit_stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,35 +346,44 @@ def _visit_stmt(self, node: ASTNode, lines: list[str], indent: int) -> None:
and node.expr.args):
risk_func = c.member
_RISK_MEMBER_MAP = {
"max_intraday_filled_orders": ("max_intraday_filled_orders_", "int"),
"max_drawdown": ("risk_max_drawdown_", "double"),
"max_intraday_loss": ("risk_max_intraday_loss_", "double"),
"max_position_size": ("risk_max_position_size_", "double"),
"max_cons_loss_days": ("risk_max_cons_loss_days_", "int"),
"max_intraday_filled_orders": (
"set_pine_risk_max_intraday_filled_orders", "int"
),
"max_drawdown": ("set_pine_risk_max_drawdown", "double"),
"max_intraday_loss": ("set_pine_risk_max_intraday_loss", "double"),
"max_position_size": ("set_pine_risk_max_position_size", "double"),
"max_cons_loss_days": ("set_pine_risk_max_cons_loss_days", "int"),
}
if risk_func == "allow_entry_in":
val = self._visit_expr(node.expr.args[0])
if val == "1":
lines.append(f"{pad}risk_direction_ = RiskDirection::LONG_ONLY;")
direction = "1"
elif val == "-1":
lines.append(f"{pad}risk_direction_ = RiskDirection::SHORT_ONLY;")
direction = "-1"
else:
lines.append(f"{pad}risk_direction_ = RiskDirection::BOTH;")
direction = "0"
lines.append(f"{pad}set_pine_risk_direction({direction});")
return
if risk_func in _RISK_MEMBER_MAP:
member, cast_type = _RISK_MEMBER_MAP[risk_func]
setter, cast_type = _RISK_MEMBER_MAP[risk_func]
val = self._visit_expr(node.expr.args[0])
lines.append(f"{pad}{member} = ({cast_type})({val});")
# Handle percent_of_equity flag for max_drawdown / max_intraday_loss
# The percent flag travels with the matching setter so
# every risk update atomically replaces both fields.
if risk_func in ("max_drawdown", "max_intraday_loss") and len(node.expr.args) >= 2:
arg2 = node.expr.args[1]
is_pct = (isinstance(arg2, MemberAccess)
and isinstance(arg2.object, Identifier)
and arg2.object.name == "strategy"
and arg2.member == "percent_of_equity")
if is_pct:
pct_flag = "risk_max_drawdown_is_pct_" if risk_func == "max_drawdown" else "risk_max_intraday_loss_is_pct_"
lines.append(f"{pad}{pct_flag} = true;")
else:
is_pct = False
if risk_func in ("max_drawdown", "max_intraday_loss"):
lines.append(
f"{pad}{setter}(({cast_type})({val}), "
f"{'true' if is_pct else 'false'});"
)
else:
lines.append(f"{pad}{setter}(({cast_type})({val}));")
return
if self._is_skip_expr(node.expr):
return
Expand Down
Loading
Loading