PineForge v0.13.1-379-g9b50973
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 <string>
13#include <variant>
14#include <vector>
15
17
19enum class Placement { Allow, Deny };
20enum class Dispatch { Allow, Decline };
23enum class OrderKind { Market, Entry, Other };
24enum class Side { Flat, Long, Short };
26
28 int limit = 0;
29 bool skip_noop_market = false;
30 bool defer_pooc_close = false;
32};
33struct CapClock {
34 int64_t timestamp = 0;
35 std::string session;
36 std::string timezone;
37 int chart_day = 0;
38 int chart_month = 0;
39};
60struct Admission {
62 std::optional<QuotaTrigger> trigger;
63};
78struct Prices { double fill; double open; double high; double low; };
81using CloseDecision = std::variant<std::monostate, CloseNow, CloseNextOpen>;
82
84public:
85 static constexpr uint64_t schema_version = 1;
88
89 void attach() { attachment_ = CapAttachment::LegacySource; }
90
91 // The old generated assignment is an explicit request for Pine behavior,
92 // even from an opted-out native fixture. It never silently drops a rule.
93 // Only the limit changes: statement-time updates do not reset the ledger.
94 IntradayCap& operator=(int limit) {
95 attach();
96 configuration_.limit = limit;
97 return *this;
98 }
99 CapAttachment attachment() const { return attachment_; }
100 const CapConfiguration& configuration() const { return configuration_; }
101 const IntradayOrderBudget& budget() const { return budget_; }
102 const std::optional<CloseCause>& due_cause() const { return due_cause_; }
103 uint64_t next_action() const { return next_action_; }
104 bool active() const {
105 return attachment_ != CapAttachment::None && configuration_.limit > 0;
106 }
107 bool needs_clock() const {
108 return active() || (configuration_.count_pooc_full_close && budget_.transfer());
109 }
110 // Existing shortcut guards distinguish negative from exactly zero. This
111 // is a Pine facade predicate, not a generic "any risk policy" switch.
112 bool legacy_limit_is_zero() const {
113 return attachment_ == CapAttachment::None || configuration_.limit == 0;
114 }
115 void metadata(const std::string& key, double value) {
116 // Retain declaration values in the ONE configuration owner even when
117 // detached. Metadata alone never attaches or activates the policy.
118 // A later explicit legacy assignment therefore preserves metadata sent
119 // before the first risk statement, without a second buffer or replay.
120 const bool enabled = std::isfinite(value) && value > 0.0;
121 if (key == "intraday_cap_skip_noop_market_fills")
122 configuration_.skip_noop_market = enabled;
123 else if (key == "intraday_cap_defer_pooc_close")
124 configuration_.defer_pooc_close = enabled;
125 else if (key == "intraday_cap_count_pooc_full_close_fills")
126 configuration_.count_pooc_full_close = enabled;
127 }
128 static bool uses_chart_clock(const std::string& session) {
129 return !(session.size() >= 9 && session[4] == '-'
130 && hhmm_to_minutes(session.substr(0, 4)) >= 0
131 && hhmm_to_minutes(session.substr(5, 4)) >= 0);
132 }
133 static OrderRiskDay risk_day(const CapClock& clock) {
134 if (!uses_chart_clock(clock.session)) {
136 clock.timestamp, clock.timezone, clock.session)};
137 }
138 // Preserve the existing fallback, including its omitted year.
139 return {clock.chart_day * 100 + clock.chart_month};
140 }
142 if (!active()) return Placement::Allow;
143 budget_.enter_day(risk_day(clock));
144 return budget_.latched() ? Placement::Deny : Placement::Allow;
145 }
146 void decline(uint64_t incarnation) { budget_.decline(incarnation); }
147 AttemptOrigin origin(const CapClock& clock, const Calculation& c,
148 uint64_t incarnation, uint64_t latest_fill) const {
149 return configuration_.count_pooc_full_close
150 && budget_.can_inherit(risk_day(clock), c.bar, incarnation, latest_fill)
152 }
153 Admission pre_dispatch(const CapClock& clock, const Calculation& context,
154 const MatchedAttempt& attempt, uint64_t latest_fill) {
155 if (!active()) return {};
156 if (configuration_.skip_noop_market && attempt.kind == OrderKind::Market
157 && attempt.live_side != Side::Flat
158 && (attempt.live_side == Side::Long) == attempt.is_long
159 && attempt.live_entries >= attempt.pyramiding) {
160 decline(attempt.incarnation);
161 return {Dispatch::Decline, std::nullopt};
162 }
163 const auto day = risk_day(clock);
164 const auto result = budget_.admit_matched_attempt(
165 day, configuration_.limit, context.bar,
166 configuration_.count_pooc_full_close ? attempt.incarnation : 0,
167 latest_fill);
168 if (result == QuotaAdmission::Blocked)
169 return {Dispatch::Decline, std::nullopt};
170 if (result == QuotaAdmission::ReachedLimit)
171 return {Dispatch::Allow, QuotaTrigger{day, budget_.charged_slots()}};
172 return {};
173 }
176 budget_.expire_transfer();
177 }
179 return active() && configuration_.count_pooc_full_close && full
180 && ordinary(c) && !c.coof_scheduler && c.fifo
182 }
183 void committed_close(const CapClock& clock, const Calculation& c, Side before,
184 uint64_t fill, const std::vector<ContinuationCandidate>& candidates) {
185 const ContinuationCandidate* selected = nullptr;
186 for (const auto& candidate : candidates) {
187 if (candidate.kind != OrderKind::Market || candidate.created_bar != c.bar
188 || candidate.is_long == (before == Side::Long)) continue;
189 if (!selected || candidate.created_seq < selected->created_seq)
190 selected = &candidate;
191 }
192 budget_.count_committed_close(risk_day(clock), configuration_.limit,
193 fill, c.bar, selected ? selected->incarnation : 0);
194 }
196 const MatchedAttempt& attempt, Side side,
197 int64_t cycle, Prices prices) {
198 if (!admission.trigger) return {};
199 if (side == Side::Flat) { budget_.latch(); return {}; }
200 const uint64_t action = next_action_++;
201 broker::PositionCloseRequest request{action, cycle, c.bar,
202 "Close Position (Max number of filled orders in one day)"};
203 if (configuration_.defer_pooc_close && ordinary(c)
204 && attempt.kind == OrderKind::Market && attempt.created_bar == c.bar) {
205 budget_.latch();
206 due_cause_ = CloseCause{action, admission.trigger->day,
207 admission.trigger->charged_slots, c.bar, attempt.incarnation};
208 return CloseNextOpen{std::move(request)};
209 }
210 double price = prices.fill;
211 if (attempt.kind == OrderKind::Market || attempt.kind == OrderKind::Entry) {
212 if (side == Side::Long && prices.fill > prices.open) price = prices.high;
213 else if (side == Side::Short && prices.fill < prices.open) price = prices.low;
214 }
215 return CloseNow{std::move(request), price};
216 }
217 // The synchronous simulator attempted the requested close. The existing
218 // cap latches even if no economic effect occurred; flatness is read from
219 // actual position state, never inferred from this notification.
220 void after_immediate_close_attempt() { budget_.latch(); }
221 void ordinary_open(int bar) {
222 budget_.expire_transfer();
223 if (due_cause_ && bar > due_cause_->trigger_bar) due_cause_.reset();
224 }
225 void source_batch_end() { budget_.expire_transfer(); }
226 void reset_run() {
227 budget_ = {};
228 due_cause_.reset();
229 next_action_ = 1;
230 }
231private:
232 static bool ordinary(const Calculation& c) {
233 return c.process_on_close && !c.calc_on_fills && !c.magnifier
234 && !c.stream_warmup && c.stream_idle;
235 }
236 CapAttachment attachment_;
237 CapConfiguration configuration_;
238 IntradayOrderBudget budget_;
239 std::optional<CloseCause> due_cause_;
240 uint64_t next_action_ = 1;
241};
242
243} // 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)
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