Skip to content

fix(codegen): resolve input-derived TA lengths and input.timeframe request.security tf - #29

Merged
luisleo526 merged 1 commit into
mainfrom
fix/input-derived-ta-length-and-security-tf
Jun 29, 2026
Merged

luisleo526 merged 1 commit into
mainfrom
fix/input-derived-ta-length-and-security-tf

Conversation

@luisleo526

Copy link
Copy Markdown
Contributor

Summary

Two related transpiler resolution gaps where a structural argument derived from an input (not a literal or direct input alias) silently degraded the generated indicator:

  1. Computed/parameter TA lengths → silent period-1. A ta.* length that is arithmetic-of-input (wilderLen = rsiLen*2-1, wp = sf*2-1) or a function parameter (f(src, len) => ta.sma(src, len)) fell back to period 1 (unsmoothed). Now folded to the compile-time value / expanded to the real runtime length expression; genuinely unresolvable lengths raise a guardrail instead of silently emitting 1. Function per-call-site TA clones now get globally-unique member names, so a helper reached via multiple enclosing functions no longer emits duplicate member declarations.
  2. input.timeframe()-backed request.security timeframe → bare/empty. tf = input.timeframe("60") then request.security(sym, tf, …) emitted tf as a bare variable, uninitialized at evaluator-registration time, so the HTF series degraded to the chart timeframe. Now resolved to its runtime input value.

Validation (TradingView deep-backtest, BINANCE:ETHUSDT.P @ 15m)

  • 3commas DCA cohort: tao 0→100%, dual-rsi 0→100%, bb-rsi 23→99%, sol 7→93%.
  • QQE pair: qqe-trend 1.4→99.8%, qqe-channel 18→84%; jayentriken-bbwp 84→88%.
  • Scraped set verify_corpus tiers: excellent+strong 42→57 / 90.

Corpus parity (hard gate) — intact

  • verify_corpus.py --all = 251 excellent / 1 documented TV anomaly, 0 weak/moderate/minimal.
  • Only 2 corpus strategies' generated C++ changes (mtf-htf-confluence-manual-trail, …-static-bracket): both moderate/weak → excellent, realigning codegen with the committed validated output (HEAD had drifted). The other 252 are byte-identical.
  • Full pytest: 1372 passed, 1 skipped (+6 new regression tests). 0 new transpile/compile failures across the scraped set.

🤖 Generated with Claude Code

…quest.security tf

Two related resolution gaps where a structural argument derived from an input
(not a literal or direct input alias) silently degraded:

- TA constructor length that is arithmetic-of-input (e.g. wilderLen = rsiLen*2-1,
  wp = sf*2-1) or a function parameter (f(src, len) => ta.sma(src, len)) fell back
  to period 1, yielding an unsmoothed indicator. Now folded to the compile-time
  value / expanded to the real runtime length expression; genuinely unresolvable
  lengths raise a guardrail instead of silently emitting 1. Function per-call-site
  TA clones get globally-unique member names so a helper reached via multiple
  enclosing functions no longer emits duplicate member declarations.
- request.security timeframe backed by input.timeframe() (tf = input.timeframe("60"))
  was emitted as a bare variable, uninitialized at evaluator-registration time, so
  the HTF series degraded to the chart timeframe. Now resolved to its runtime input
  value.

Validated against TradingView deep-backtest on the scraped strategy set
(BINANCE:ETHUSDT.P @ 15m): recovers the 3commas DCA cohort (tao/dual-rsi 0->100%,
bb-rsi 23->99%, sol 7->93%) and the QQE pair (qqe-trend 1.4->99.8%, qqe-channel
18->84%). Corpus parity intact: verify_corpus --all = 251 excellent / 1 documented
TV anomaly; the only two corpus strategies whose generated C++ changes
(mtf-htf-confluence-manual-trail / -static-bracket) move moderate/weak -> excellent,
realigning codegen with the committed validated output. Full pytest: 1372 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@luisleo526
luisleo526 merged commit 6545234 into main Jun 29, 2026
9 checks passed
@luisleo526
luisleo526 deleted the fix/input-derived-ta-length-and-security-tf branch June 29, 2026 21:44
luisleo526 added a commit that referenced this pull request Sep 23, 2026
…ound spelling (C2 W2)

Root cause. 6545234 (#29, first shipped in 0.8.1) added the TA-constructor
guard in _emit_constructor: a ctor arg must fold to a literal or expand,
through _runtime_ctor_arg_for_reset, to a runtime expression whose every
identifier token is input-backed, a known constant or structural. Ctor args
travel as Pine source spellings, and the token scan read an inline call's
own argument text -- `input`, `int`, the title `fast` -- as unknown names,
so the length was refused. Before that commit 0.7.5-0.8.0 "accepted" it by
sizing both EMAs at 1 with no runtime reset (ta::EMA(1): fast == slow ==
close, a strategy that can never trade). 8390364 later let the bare word
`input` through, which admitted argument-free spellings such as input(15)
but not a title; and the analyzer's _expr_to_str dropped keyword arguments,
so `input.int(defval=9, title="fast")` reached codegen as `input.int()` and
silently became ta::EMA(get_input_int("", 0)).

Fix:
  * pine_spelling.py: string-literal-aware helpers -- the Pine literal of a
    string, identifier substitution that leaves string contents alone, the
    extent of each inline input call, and a full spelling of an input call.
  * analyzer _expr_to_str spells an input call with its keyword arguments,
    escapes string literals and spells list literals (options=[...]).
  * codegen: an inline input call is admitted exactly when its bound
    spelling `v = <call>` would make v input-backed (_is_stable_inline_input:
    not a source input, constant defval; _input_default_value is now the one
    rule for both). The reset gate masks each admitted call to the bare word
    `input`; the placeholder, precalc and prepare_script_run sizes read its
    defval (_fold_inline_input_defaults), as a bound name does through
    _known_vars; derived-length tracking (`n = input.int(9, "fast") * 2`,
    `var n = input.int(9, "fast")`) spells it in full and counts it as an
    input reference; _expand_derived no longer rewrites a title that happens
    to spell a derived name.
  * The input manifest lists every global-scope input call, inline ones too,
    one entry per call site in source order, titled with the key the C++
    reads the input by.

The inline spelling now emits the bound spelling's C++ except for the bound
twin's own member and its source-line call-site ids. A length that is
otherwise a series (a bar field, bar_index, a source input) is still refused
with the same diagnostic.

Evidence: tests/test_e2e_inline_input_ta_length.py 79 passed (was 74 failed);
all 312 corpus/validation generated.cpp re-transpile byte-identical; the
manifest of two corpus probes gains the inputs their C++ already reads
(analyzer-anvil-percent-costs-01: "Stop %", "Target %";
na-ledger-history-anchor-01: "Entry Deviation %").

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
luisleo526 added a commit that referenced this pull request Sep 23, 2026
…the member and the manifest (C4 W4)

TradingView: "If not specified, the variable name is used as the input's
title" (Pine v6 reference, input.int title). The manifest and the member
followed that rule, but several getters re-derived the key from a
re-spelled call or from whichever name they were handed, so an override
set the member and never reached the TA size or the security feed:

- tf = input.timeframe("60") as a request.security timeframe registered the
  evaluator with get_input_string("", ...) -- since 6545234 (#29), which
  renders the bound call with no enclosing name;
- len = input.int(9) with a stable `len := ...` reassignment: the TA reset
  read the folded `(cond ? 30 : input.int(9))` under the default's text "9"
  from 8390364 on, under "" since C2's spelling;
- var n = input.int(9) * 2 (or math.max(input.int(9), 2), or a float
  feeding ta.bb): the reset re-read the nested call under "" -- 097f499 (#54);
- a = input.int(9) then b = a: the reset read it under "b" -- since the
  initial public release 5b92828.

One key per input, from the call node itself:
pine_spelling.input_binding_names() maps each global-scope input call a
declaration names -- the call bound straight to `v = input.*()` and any call
in a var initializer, the manifest's rule -- to that name.
_get_input_title() consults it before the name its caller passes, so the
member, the TA reset, a security timeframe and an alias's read all agree
with the manifest; and a call re-spelled for a derived length (codegen
_arith_expr_to_str, analyzer _expr_to_str) carries the name as title=, since
the re-parsed node has lost its declaration. The manifest walker moves to
pine_spelling.global_input_calls() so both share one definition of "global
scope".

E2E (tests/test_e2e_untitled_input_keys.py): each untitled shape runs next
to the same strategy with an explicit title, at the defaults and under the
same override keyed by each one's manifest title; the trades must be
identical and the override must move them. Before (on d0ae5be): 7 failed --
security_timeframe(_alias), stable_reassignment, var_nested(_call,_float),
alias -- each "override left the trades unchanged"; the direct binding,
input.source, a security length and a ta.change length through an alias
passed. After: 11 passed (e.g. security_timeframe override {"tf": "240"}
5508 trades == the "TF"-titled twin).

Re-transpile: no C++ or manifest change in the 312 probes, the corpus or
the 101 closed strategies -- none uses these shapes.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant