Skip to content

Commit 07c6363

Browse files
luisleo526claude
andcommitted
cr150 lane proof (squash of the lane branch on the freeze head 2754739)
Lab proof commit, never pushed. Its tree equals the lane head; its single parent is the freeze head 2754739 (core/cr150-p00-contracts: batch A #457 + CANARY-WIRE + SyntaxError fix), as the GitHub squash will write it. CR-150 lane: see the lane REPORT.md for design, tests and wiring. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
1 parent 2754739 commit 07c6363

18 files changed

Lines changed: 2496 additions & 59 deletions

‎CMakeLists.txt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ set(PINEFORGE_KERNEL_SOURCES
155155
src/native_bar_views.cpp
156156
src/selected_report_view.cpp
157157
src/selected_window_plan.cpp
158+
src/report_scratch.cpp
158159
)
159-
# The kernel-only archive. Same objects, same flags as the kernel half of
160-
# libpineforge.a — a consumer that never speaks Pine links this one.
160+
# The kernel-only archive: libpineforge.a's kernel objects and flags, for consumers that never speak Pine.
161161
add_library(pineforge_kernel STATIC ${PINEFORGE_KERNEL_SOURCES})
162162
add_library(PineForge::kernel ALIAS pineforge_kernel)
163163
# So a find_package(PineForge) consumer spells it the same way an in-tree one

‎docs/adr/0001-kernel-adapter-boundary.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ summary of it.
182182
`report_open_position_at_end` books the still-open position as a mark-to-market row under
183183
`KernelRecorded`. Under `HostRecorded` an unrecorded curve still degenerates —
184184
`compute_equity_stats` answers its all-NaN value with no error (`src/engine_metrics.cpp:316`,
185-
consumed by `fill_metrics_section`, `src/engine_report.cpp:192-206`) — which is why the policy exists.
185+
consumed by `fill_metrics_section`, `src/engine_report.cpp:192-210`) — which is why the policy exists.
186186
- **Reproducibility.** Two digests, one wrapping the other. `native_continuation_hash()`
187187
(`native_host.hpp:1376`) returns the consumer's event/continuation hash. **A digest names
188188
the run's inputs** (rule 2): the timezone identity enters it as the zone's *content* —

‎docs/design/cr150-contracts.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ references; L15a must maintain these anchors and mechanically pin every current
210210
| Host feed lookup/delivery and column reads | `installed_symbol_feed` `src/source/pine_security_eval.cpp:1059-1068`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1291-1301`; `installed_symbol_feed` `src/source/pine_security_eval.cpp:1320-1328` | 359-360 |
211211
| Feed validation: bar values/order, close length/bounds, column length; configure and consumer entry points | `feed.bars` `src/native_run_spec.cpp:259-290`; `instrument_feed_shapes` `src/native_run_spec.cpp:743`; `validate_native_instrument_feeds` `src/native_run_spec.cpp:1019-1022`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9609-9612`; `validate_native_timeframe_subscriptions` `src/native_execution_consumer.cpp:9634-9636` | 361-363 |
212212
| Adapter projection validation: use the context normalizer; host stages after lending and before projection, using a context built from its store | `normalize_projected_spec` `src/source/pine_adapter.cpp:3006-3012` | 361-363; V1/V2 change 5 |
213-
| Instrument-feed digest and cached/uncached continuation folds | `digest_instrument_feed_views` `src/native_run_spec.cpp:1162-1205`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:322-324`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:2775-2776`; `native_run_spec_digest` `src/native_execution_consumer.cpp:13329-13334` | 364-365 |
213+
| Instrument-feed digest and cached/uncached continuation folds | `digest_instrument_feed_views` `src/native_run_spec.cpp:1162-1205`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:322-324`; `native_instrument_feeds_digest` `src/native_execution_consumer.cpp:2775-2776`; `native_run_spec_digest` `src/native_execution_consumer.cpp:13384-13389` | 364-365 |
214214
| Staged feed counts in host state and its hash | `installed_symbol_feed` `src/source/pine_strategy_host.cpp:234-244`; `staged_.instrument_feeds` `src/source/pine_state_hash.cpp:294-301` | 366-367 |
215215
| Refresh of symbol-data digest and host hash fold | `refresh_symbol_data_digest` `src/source/pine_security_eval.cpp:1553-1566`; `symbol_data_digest_` `src/source/pine_state_hash.cpp:1191-1197` | 367-368 |
216216
| Restore/store upkeep and column-setter length check | `held.bars` `src/source/pine_security_eval.cpp:1091-1101`; `data.bars` `src/source/pine_security_eval.cpp:1433-1439` | 369-370 |
@@ -254,3 +254,13 @@ one presentation of the full selected curve, preserving the hook's published-len
254254
changes before the selected writer emits the anchor and final window observations.
255255
At capture, the existing admitted selected-window flag chooses the presentation
256256
once; subsequent report reads reuse that capture.
257+
Ordinary sink removal (`set_report_sink_v1(NULL)`) returns the typed setter
258+
refusal `-3` while that run's scratch curve is live and keeps the sink,
259+
scratch-backed summary report and bounded observer capture unchanged;
260+
the scratch is dropped at the next run begin or handle destruction without
261+
restoring a curve array.
262+
Sealed trade rows (`TradeLedger::base() > 0`) also refuse sink removal with `-3`
263+
and `set_trade_history_streamable(false)` with `coded<std::logic_error>(engine_invariant)`
264+
before any state change, preserving the sink, receipt and report without rehydrating
265+
rows until a new run begin clears size/base to 0 (ordinary scratch ownership still
266+
independently refuses removal).

‎docs/design/native-feature-parity.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ Columns: **Feature** (adapter mechanism, cited) · **Native today** (`yes` / `pa
158158
| RP1 | Per-lot MFE / MAE, kernel-sampled or host-owned | **yes** — sampler `apply_excursion` native_execution_consumer.cpp:3404, closing-row fold `max_runup` engine_orders.cpp:158-172; `owns_lot_excursions` native_host.hpp:1021-1025 | K+A | — | F:H1 O:I1 O:I2 S:X1 | **Disagree:** S partial + its P8 excursion-mode enum (single-source, not adopted, Appendix A.9). **Lowered by R5 lane H-THIN:** the Pine host keeps the kernel's `owns_lot_excursions()` default, so the sampler is the Pine excursion; the host model, its entry-bar masks and its caches are deleted, and the source layer's fold no longer carries them. **Measured against TradingView first (R5 lane H-MEASURE, `tests/test_e19_excursion_tape.cpp`):** the host-owned model was `ab9714be`'s, not TradingView's — where it and the kernel sampler differ the kernel's number is TradingView's (entry-bar over-mask, exit-bar over-fold after an at-open exit, a `calc_on_order_fills` scratch TradingView reports as 0 / 0); both miss a bar's open print ahead of a same-bar stop fill. On the corpus the two models differ on 341 of 431 390 trades, and of the 326 of those a TradingView export identifies, the kernel's number is TradingView's on 315 and the host's on none. The lowering moved the cells of 335 trades in 19 corpus probes: 324 now equal TradingView's, 11 moved closer, none farther; the all-in residual's own entry-bar call is the tape of the private fixture set `e19_allin_trim`. |
159159
| RP2 | Closed-trade list, trade statistics, report struct | **yes** — `fill_report` engine_report.cpp:56-96, `get_trade` engine.hpp:1793-1830, `compute_trade_stats` `sha256:553d4d9c9f27de9ca2e760ebf1b62abbdb553f803bc18a09d0c04cb50f8baa5c` engine_metrics.cpp:184-302 | K | — | F:H2 O:I3 S:X2 | — |
160160
| RP3 | Equity curve | **no** — `update_equity_extremes` engine.hpp:1363 / `record_equity_point` engine.hpp:1381 are protected and were called only by the adapter **Closed:** `NativeReportPolicy::KernelRecorded` native_run_spec.hpp:63 has the consumer fold the extremes and append one point per script calculation, labelled with the script interval's open; `KernelRecordedAtHostMarks` records the same series at the host's own marks (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I4 S:X2 | S's "partial" folds RP2 in |
161-
| RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve `sha256:b1bf9ddebf761d446c05e46a934bd4476945cb94012f226b7b68001e87e1d52c` engine_metrics.cpp:304-458; `fill_metrics_section` engine_report.cpp:192-206 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) |
161+
| RP4 | Equity metrics (drawdown, run-up, Sharpe / Sortino, CAGR, time in market) | **no** — `compute_equity_stats` exits early on an empty curve `sha256:b1bf9ddebf761d446c05e46a934bd4476945cb94012f226b7b68001e87e1d52c` engine_metrics.cpp:304-458; `fill_metrics_section` engine_report.cpp:192-210 **Closed:** with a recorded curve the drawdown / run-up walk is finite and every equity metric is computed over a real series; `report_open_position_at_end` adds the still-open position as a mark-to-market row (`tests/test_native_report_truth.cpp`). | K | L2 | F:H3 O:I5 | silent degenerate metrics, no error (O) |
162162
| RP5 | Range-end row for a position still open at the end (`scheduler_record_range_end` pine_strategy_host.cpp:2003) | **yes, shared** — one generic producer `NativeExecutionConsumer::append_open_position_report_rows`, called by the kernel's run-end `record_open_position_report_rows` (`KernelRecorded` + `report_open_position_at_end`) and by the adapter at TradingView's mark; `report_trade_count()` engine.hpp:1832 includes the rows | K + A report shape | L2, Q6 | F:H4 O:I6 S:X2 | **Disagree:** S says kernel range-end rows exist (storage only). The ROWS are now the kernel's (§3.7 part 1); TV report shape — the equity re-mark, the extreme re-fold, the same-bar re-sort, three marks — stays A, measured in §3.7 part 2. The short-seed swaps (`project_short_seed_report_rows` pine_strategy_host.cpp:1465) stay A as well: a projection of two rows' `entry_incarnation` onto `ab9714be`'s numbering, which TradingView does not export; measured against the kernel's own lot identities by `tests/test_short_seed_report_swap.cpp` (R5 lane H-MEASURE), and pinned by `test_native_oracle_short_seed_percent_full_l2`, whose two incarnation checks fail when the swap is skipped. The swap runs only when a later close of the seed id closes a remnant; a flat collision keeps the kernel's numbering. |
163163
| RP6 | Per-trade accessors | **partial** — `get_trade` is public; the `closed_trade_*` family is protected engine.hpp:1392 **Closed:** `closed_trade_count()` / `closed_trade(i)` and `report_trade_count()` / `get_report_trade(i)` are public, and `closed_trade_close_cause(i)` engine.hpp:1850 answers the typed `execution::CloseCause` the closer recorded (`tests/test_native_report_truth.cpp`). | K | L2 | F:H6 | (single-source) |
164164
| RP7 | Live order-action stream (runner ledger / webhook) | **yes** — `stream_observe_actions_` engine_execution.cpp:1390, enabled `stream_observe_actions_` native_execution_consumer.cpp:10739, C ABI `strategy_stream_order_actions_len` c_abi.cpp:738-757 | K | — | F:H5 | (single-source) |

‎include/pineforge/engine.hpp‎

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@
5151
#define PINEFORGE_HAS_SCRIPT_RUN_PREPARE_V1 1
5252
#define PINEFORGE_HAS_NATIVE_LIVE_V1 1
5353
namespace pineforge {
54-
54+
namespace metrics { struct ScratchCurveRegion; }
5555
enum class PositionSide { FLAT, LONG, SHORT };
56-
56+
namespace report_sink { class ScratchCurveWriter; }
5757
// One physical emulator lot entry/exit, in actual execution order. A reversal
5858
// may produce several exits followed by an entry. No range-end report rows.
5959
struct StreamOrderAction {
@@ -1384,7 +1384,7 @@ class BacktestEngine {
13841384
p.time_ms = script_bar_ts;
13851385
p.open_profit = open_profit(current_bar_.close);
13861386
p.equity = initial_capital_ + net_profit_sum_ + p.open_profit;
1387-
equity_curve_.push_back(p);
1387+
store_equity_point(p);
13881388
if (position_side_ != PositionSide::FLAT) ++bars_in_market_;
13891389
}
13901390

@@ -2301,6 +2301,9 @@ class BacktestEngine {
23012301
std::uint32_t active_calls = 0;
23022302
std::uint32_t stream_calls = 0;
23032303
std::unique_ptr<report_sink::LineWriter> writer;
2304+
// Ordinary runs only; selected capture retains its full vector.
2305+
std::unique_ptr<report_sink::ScratchCurveWriter> equity_scratch;
2306+
bool equity_tail_stored = false; // scratch includes the held tail
23042307
pf_report_sink_receipt_v1 receipt{};
23052308
// Derived report-tail prefix, in max/min/drawdown/runup order. The
23062309
// resulting extrema already have their broker-state folds; this
@@ -2309,16 +2312,12 @@ class BacktestEngine {
23092312
// Report cursor only, covered by report_sink_state_'s existing
23102313
// broker-hash waiver. No execution decision reads it; copies reset it.
23112314
std::int64_t output_base = 0;
2312-
ReportSinkState() = default;
2313-
ReportSinkState(const ReportSinkState& other)
2314-
: enabled(other.enabled), trade_history_streamable(other.trade_history_streamable),
2315-
path(other.path), run_id(other.run_id), scratch_dir(other.scratch_dir) {}
2316-
ReportSinkState& operator=(const ReportSinkState& other) {
2317-
if (this != &other) *this = ReportSinkState(other);
2318-
return *this;
2319-
}
2320-
ReportSinkState(ReportSinkState&&) noexcept = default;
2321-
ReportSinkState& operator=(ReportSinkState&&) noexcept = default;
2315+
ReportSinkState();
2316+
~ReportSinkState();
2317+
ReportSinkState(const ReportSinkState& other);
2318+
ReportSinkState& operator=(const ReportSinkState& other);
2319+
ReportSinkState(ReportSinkState&&) noexcept;
2320+
ReportSinkState& operator=(ReportSinkState&&) noexcept;
23222321
};
23232322
// @broker-state begin
23242323
mutable ReportSinkState report_sink_state_;
@@ -2343,6 +2342,9 @@ class BacktestEngine {
23432342
int report_sink_setter_status() const;
23442343
void begin_report_sink();
23452344
void finish_report_sink(ReportC* out) const;
2345+
void stage_report_sink_equity_tail(const ReportC* out) const;
2346+
void compute_report_sink_metrics(ReportC* out, std::int64_t equity_count) const;
2347+
void fill_report_sink_metrics_section(ReportC* out) const;
23462348
template<class Write> void report_sink_record(Write write) const;
23472349
bool stream_report_sink_trace(const TraceEntryC& entry);
23482350
void reset_report_sink_outputs();
@@ -2363,10 +2365,14 @@ class BacktestEngine {
23632365
bool trade_history_streamable() const { return report_sink_state_.trade_history_streamable; }
23642366

23652367
protected:
2366-
// L6b-2: the last point remains editable until its successor is recorded.
2367-
// The array remains the metric/capture source until L6b-4/L7c/L17 land.
2368+
// The last point stays editable until its successor is recorded. Ordinary
2369+
// sinks keep only this tail; selected and sink-off runs keep the array.
23682370
pf_equity_point_t* last_equity_point() noexcept;
23692371
const pf_equity_point_t* last_equity_point() const noexcept;
2372+
// Ordinary sink execution prefix, excluding the editable held point.
2373+
// The returned descriptor is borrowed; observing flushes pending writes.
2374+
metrics::ScratchCurveRegion report_sink_equity_scratch_region() const;
2375+
void store_equity_point(const pf_equity_point_t& point);
23702376
bool prepare_equity_point(); // release the prior point; true for the first
23712377
void fold_report_equity_extreme(double equity);
23722378
void refold_equity_tail();

‎include/pineforge/report_sink.h‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ extern "C" {
2121
* the strings. The engine creates or truncates path, writes the version-1
2222
* header at run begin, and writes checked, buffered records (3.7:881-888).
2323
* NULL run_id means ""; NULL scratch_dir means the directory of path.
24-
* Scratch files are engine-private and live until handle destruction
25-
* (3.8:902-909). */
24+
* Scratch files are engine-private and live until the next run begins or
25+
* handle destruction (3.8:902-909). */
2626
typedef struct pf_report_sink_v1 {
2727
uint32_t struct_size;
2828
uint32_t version;
@@ -61,7 +61,15 @@ PF_API uint32_t pf_report_sink_version(void);
6161

6262
/** Install, replace or remove (NULL descriptor) the sink (6:1301, 1307-1309).
6363
* Returns 0 accepted; -1 bad struct_size, version, path or handle; -2 a
64-
* stream is running or begins; -3 run in progress. Strings are copied.
64+
* stream is running or begins; -3 run in progress or removal while an
65+
* ordinary run's scratch curve is live. Strings are copied.
66+
* A refused removal keeps the sink and its summary report unchanged;
67+
* it never restores a full curve array from scratch.
68+
* Sealed trade rows (ledger base > 0) also refuse removal with -3 and
69+
* set_trade_history_streamable(false) with coded logic_error engine_invariant,
70+
* preserving the sink, receipt and report without rehydrating rows until a
71+
* new run begin clears size/base to 0 (ordinary scratch ownership still
72+
* independently refuses removal).
6573
* The sink composes with the execution observer and selected window.
6674
* Write errors fail the run with the sink I/O code planned for
6775
* docker/run_failure_codes.json (DESIGN 3.7:881-888; 6:1330-1332).

‎src/engine_metrics.cpp‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,3 +476,23 @@ void BacktestEngine::reset_trade_percent_prefix() {
476476
}
477477

478478
} // namespace pineforge
479+
480+
// CR-150 L6b-4 read-side binding. Keep the existing metric implementation and
481+
// its array instantiations above unchanged; L7c owns the public SpillCurve
482+
// instantiations. The internal window can exercise the real scratch reader
483+
// before that downstream adapter is merged.
484+
#include "report_scratch.hpp"
485+
486+
namespace pineforge {
487+
namespace metrics {
488+
489+
template pf_equity_stats_t compute_equity_stats<report_sink::ScratchCurveWindow>(
490+
const report_sink::ScratchCurveWindow&, int64_t, double, const std::string&,
491+
double, double, int64_t, double);
492+
493+
template pf_equity_stats_t compute_equity_stats<report_sink::ScratchCurveWindow>(
494+
const report_sink::ScratchCurveWindow&, int64_t, double, const std::string&,
495+
double, double, int64_t, double, int64_t);
496+
497+
} // namespace metrics
498+
} // namespace pineforge

0 commit comments

Comments
 (0)