PineForge unreleased
Deterministic PineScript v6 backtest runtime — C ABI reference
Loading...
Searching...
No Matches
intraday_cap.hpp
Go to the documentation of this file.
1#pragma once
2
3// TEMPORARY ENGINE-SIDE PINE COMPATIBILITY. Native engines start detached.
4// New generated sources attach this component before host metadata arrives;
5// old protected limit assignments remain an explicit legacy-source opt-in.
6// The source facade and physical package location remain migration boundaries.
10#include "../../timeframe.hpp"
11#include <cmath>
12#include <cstdint>
13#include <string>
14#include <variant>
15#include <vector>
16
18
20enum class Placement { Allow, Deny };
21enum class Dispatch { Allow, Decline };
24enum class OrderKind { Market, Entry, Other };
25enum class Side { Flat, Long, Short };
27
29 int limit = 0;
30 bool skip_noop_market = false;
31 bool defer_pooc_close = false;
33};
34struct CapClock {
35 int64_t timestamp = 0;
36 std::string session;
37 std::string timezone;
38 int chart_day = 0;
39 int chart_month = 0;
40};
61struct Admission {
63 std::optional<QuotaTrigger> trigger;
64};
79struct Prices { double fill; double open; double high; double low; };
82using CloseDecision = std::variant<std::monostate, CloseNow, CloseNextOpen>;
83
85public:
86 static constexpr uint64_t schema_version = 1;
89
90 void attach() { attachment_ = CapAttachment::LegacySource; }
91
92 // The old generated assignment is an explicit request for Pine behavior,
93 // even from an opted-out native fixture. It never silently drops a rule.
94 // Only the limit changes: statement-time updates do not reset the ledger.
95 IntradayCap& operator=(int limit) {
96 attach();
97 configuration_.limit = limit;
98 return *this;
99 }
100 CapAttachment attachment() const { return attachment_; }
101 const CapConfiguration& configuration() const { return configuration_; }
102 const IntradayOrderBudget& budget() const { return budget_; }
103 const std::optional<CloseCause>& due_cause() const { return due_cause_; }
104 uint64_t next_action() const { return next_action_; }
105 bool active() const {
106 return attachment_ != CapAttachment::None && configuration_.limit > 0;
107 }
108 bool needs_clock() const {
109 return active() || (configuration_.count_pooc_full_close && budget_.transfer());
110 }
111 // Existing shortcut guards distinguish negative from exactly zero. This
112 // is a Pine facade predicate, not a generic "any risk policy" switch.
113 bool legacy_limit_is_zero() const {
114 return attachment_ == CapAttachment::None || configuration_.limit == 0;
115 }
116 void metadata(const std::string& key, double value) {
117 // Retain declaration values in the ONE configuration owner even when
118 // detached. Metadata alone never attaches or activates the policy.
119 // A later explicit legacy assignment therefore preserves metadata sent
120 // before the first risk statement, without a second buffer or replay.
121 const bool enabled = std::isfinite(value) && value > 0.0;
122 if (key == "intraday_cap_skip_noop_market_fills") {
123 configuration_.skip_noop_market = enabled;
124 declared_ |= kDeclaredSkipNoopMarket;
125 } else if (key == "intraday_cap_defer_pooc_close") {
126 configuration_.defer_pooc_close = enabled;
127 declared_ |= kDeclaredDeferPoocClose;
128 } else if (key == "intraday_cap_count_pooc_full_close_fills") {
129 configuration_.count_pooc_full_close = enabled;
130 declared_ |= kDeclaredCountPoocFullClose;
131 }
132 }
133 // A Pine script's own intraday filled-order cap statement (reaching here
134 // through PineStrategyHost::set_pine_risk_max_intraday_filled_orders)
135 // counts as TradingView does: every candidate switch its host did not
136 // declare is on (lane W10-DIAG-UNKNOWN rule CAP-ON,
137 // tests/fixtures/intraday_cap_tv). A declared switch keeps its value, and
138 // a component selected any other way keeps the defaults above.
140 if (!(declared_ & kDeclaredSkipNoopMarket)) configuration_.skip_noop_market = true;
141 if (!(declared_ & kDeclaredDeferPoocClose)) configuration_.defer_pooc_close = true;
142 if (!(declared_ & kDeclaredCountPoocFullClose))
143 configuration_.count_pooc_full_close = true;
144 }
145 std::uint8_t declared() const { return declared_; }
146 // Selected-window replay seam (Pine compatibility, not a kernel policy):
147 // writes back exactly the three configuration-owner fields and nothing
148 // else. It is not attach(), adopt_script_count() or operator=(int), and it
149 // never touches budget_, due_cause_ or next_action_, so no consumed-count
150 // fact is rewound.
153 std::uint8_t declared) noexcept {
154 attachment_ = attachment;
155 configuration_ = configuration;
156 declared_ = declared;
157 }
158 static bool uses_chart_clock(const std::string& session) {
159 return !(session.size() >= 9 && session[4] == '-'
160 && hhmm_to_minutes(session.substr(0, 4)) >= 0
161 && hhmm_to_minutes(session.substr(5, 4)) >= 0);
162 }
163 static OrderRiskDay risk_day(const CapClock& clock) {
164 if (!uses_chart_clock(clock.session)) {
166 clock.timestamp, clock.timezone, clock.session)};
167 }
168 // Preserve the existing fallback, including its omitted year.
169 return {clock.chart_day * 100 + clock.chart_month};
170 }
172 if (!active()) return Placement::Allow;
173 budget_.enter_day(risk_day(clock));
174 return budget_.latched() ? Placement::Deny : Placement::Allow;
175 }
176 void decline(uint64_t incarnation) { budget_.decline(incarnation); }
177 AttemptOrigin origin(const CapClock& clock, const Calculation& c,
178 uint64_t incarnation, uint64_t latest_fill) const {
179 return configuration_.count_pooc_full_close
180 && budget_.can_inherit(risk_day(clock), c.bar, incarnation, latest_fill)
182 }
183 Admission pre_dispatch(const CapClock& clock, const Calculation& context,
184 const MatchedAttempt& attempt, uint64_t latest_fill) {
185 if (!active()) return {};
186 if (configuration_.skip_noop_market && attempt.kind == OrderKind::Market
187 && attempt.live_side != Side::Flat
188 && (attempt.live_side == Side::Long) == attempt.is_long
189 && attempt.live_entries >= attempt.pyramiding) {
190 decline(attempt.incarnation);
191 return {Dispatch::Decline, std::nullopt};
192 }
193 const auto day = risk_day(clock);
194 const auto result = budget_.admit_matched_attempt(
195 day, configuration_.limit, context.bar,
196 configuration_.count_pooc_full_close ? attempt.incarnation : 0,
197 latest_fill);
198 if (result == QuotaAdmission::Blocked)
199 return {Dispatch::Decline, std::nullopt};
200 if (result == QuotaAdmission::ReachedLimit)
201 return {Dispatch::Allow, QuotaTrigger{day, budget_.charged_slots()}};
202 return {};
203 }
206 budget_.expire_transfer();
207 }
209 return active() && configuration_.count_pooc_full_close && full
210 && ordinary(c) && !c.coof_scheduler && c.fifo
212 }
213 void committed_close(const CapClock& clock, const Calculation& c, Side before,
214 uint64_t fill, const std::vector<ContinuationCandidate>& candidates) {
215 const ContinuationCandidate* selected = nullptr;
216 for (const auto& candidate : candidates) {
217 if (candidate.kind != OrderKind::Market || candidate.created_bar != c.bar
218 || candidate.is_long == (before == Side::Long)) continue;
219 if (!selected || candidate.created_seq < selected->created_seq)
220 selected = &candidate;
221 }
222 budget_.count_committed_close(risk_day(clock), configuration_.limit,
223 fill, c.bar, selected ? selected->incarnation : 0);
224 }
226 const MatchedAttempt& attempt, Side side,
227 int64_t cycle, Prices prices) {
228 if (!admission.trigger) return {};
229 if (side == Side::Flat) { budget_.latch(); return {}; }
230 const uint64_t action = next_action_++;
231 broker::PositionCloseRequest request{action, cycle, c.bar,
232 "Close Position (Max number of filled orders in one day)"};
233 if (configuration_.defer_pooc_close && ordinary(c)
234 && attempt.kind == OrderKind::Market && attempt.created_bar == c.bar) {
235 budget_.latch();
236 due_cause_ = CloseCause{action, admission.trigger->day,
237 admission.trigger->charged_slots, c.bar, attempt.incarnation};
238 return CloseNextOpen{std::move(request)};
239 }
240 double price = prices.fill;
241 if (attempt.kind == OrderKind::Market || attempt.kind == OrderKind::Entry) {
242 if (side == Side::Long && prices.fill > prices.open) price = prices.high;
243 else if (side == Side::Short && prices.fill < prices.open) price = prices.low;
244 }
245 return CloseNow{std::move(request), price};
246 }
247 // The synchronous simulator attempted the requested close. The existing
248 // cap latches even if no economic effect occurred; flatness is read from
249 // actual position state, never inferred from this notification.
250 void after_immediate_close_attempt() { budget_.latch(); }
251 void ordinary_open(int bar) {
252 budget_.expire_transfer();
253 if (due_cause_ && bar > due_cause_->trigger_bar) due_cause_.reset();
254 }
255 void source_batch_end() { budget_.expire_transfer(); }
256 void reset_run() {
257 budget_ = {};
258 due_cause_.reset();
259 next_action_ = 1;
260 }
261private:
262 static bool ordinary(const Calculation& c) {
263 return c.process_on_close && !c.calc_on_fills && !c.magnifier
264 && !c.stream_warmup && c.stream_idle;
265 }
266 static constexpr std::uint8_t kDeclaredSkipNoopMarket = 1;
267 static constexpr std::uint8_t kDeclaredDeferPoocClose = 2;
268 static constexpr std::uint8_t kDeclaredCountPoocFullClose = 4;
269 CapAttachment attachment_;
270 CapConfiguration configuration_;
271 std::uint8_t declared_ = 0;
272 IntradayOrderBudget budget_;
273 std::optional<CloseCause> due_cause_;
274 uint64_t next_action_ = 1;
275};
276
277} // namespace pineforge::compat::pine
void decline(uint64_t incarnation)
const CapConfiguration & configuration() const
void metadata(const std::string &key, double value)
AttemptOrigin origin(const CapClock &clock, const Calculation &c, uint64_t incarnation, uint64_t latest_fill) const
static constexpr uint64_t schema_version
DirectCloseRouting direct_close_routing(const Calculation &c, bool full) const
static OrderRiskDay risk_day(const CapClock &clock)
void restore_replay_configuration(CapAttachment attachment, const CapConfiguration &configuration, std::uint8_t declared) noexcept
static bool uses_chart_clock(const std::string &session)
Admission pre_dispatch(const CapClock &clock, const Calculation &context, const MatchedAttempt &attempt, uint64_t latest_fill)
IntradayCap(CapAttachment attachment=CapAttachment::None)
const IntradayOrderBudget & budget() const
const std::optional< CloseCause > & due_cause() const
void committed_close(const CapClock &clock, const Calculation &c, Side before, uint64_t fill, const std::vector< ContinuationCandidate > &candidates)
void outcome(FillOutcome outcome, AttemptOrigin origin)
CloseDecision post_dispatch(const Admission &admission, const Calculation &c, const MatchedAttempt &attempt, Side side, int64_t cycle, Prices prices)
IntradayCap & operator=(int limit)
Placement placement(const CapClock &clock)
std::variant< std::monostate, CloseNow, CloseNextOpen > CloseDecision
int64_t session_trading_day_index(int64_t ms, const std::string &tz, const std::string &session)
Broker trading-day ordinal on the unmerged symbol session clock.
int hhmm_to_minutes(const std::string &hhmm)
std::optional< QuotaTrigger > trigger
broker::PositionCloseRequest request
broker::PositionCloseRequest request