Repository navigation
Expand file tree
/
Copy pathengine_risk.cpp
More file actions
151 lines (138 loc) · 6.12 KB
/
Copy pathengine_risk.cpp
File metadata and controls
151 lines (138 loc) · 6.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/*
* engine_risk.cpp — risk management + per-trade extreme tracking.
*
* Carved out of engine.cpp during the v0.1 file-split (phase 6) so
* the BacktestEngine implementation becomes navigable.
*
* check_risk_allow_entry - gate entries by direction / position cap / halt
* update_risk_state - check drawdown / intraday loss / consecutive
* loss thresholds; latch risk_halted_ when hit
* update_per_trade_extremes - per-pyramid-entry MFE/MAE tracking from H/L/C
*
* All functions are BacktestEngine instance methods; they access the
* engine's private state declared in <pineforge/engine.hpp>.
*/
#include <pineforge/engine.hpp>
#include <ctime>
#include "timezone.hpp"
namespace pineforge {
// See declaration in include/pineforge/engine.hpp. Used only by the
// intraday-day rollover gates below and the analogous gates in
// engine_fills.cpp / engine_orders.cpp. When ``chart_timezone_`` is
// empty we keep the cheap UTC fast path; otherwise we route through
// ``ScopedTimezone`` + ``localtime_r`` so IANA names like "Asia/Taipei"
// resolve correctly (POSIX-numeric offsets inside the same string syntax
// would silently disagree with the rest of the engine's TZ handling).
BacktestEngine::BarTime BacktestEngine::_decompose_bar_time_chart_tz() const {
if (chart_timezone_.empty() || chart_timezone_ == "UTC" ||
chart_timezone_ == "Etc/UTC") {
return _decompose_bar_time();
}
time_t secs = static_cast<time_t>(current_bar_.timestamp / 1000);
struct tm tm_buf {};
{
pine_tz::ScopedTimezone guard(chart_timezone_);
localtime_r(&secs, &tm_buf);
}
BarTime bt;
bt.year = tm_buf.tm_year + 1900;
bt.month = tm_buf.tm_mon + 1;
bt.dayofmonth = tm_buf.tm_mday;
bt.hour = tm_buf.tm_hour;
bt.minute = tm_buf.tm_min;
bt.second = tm_buf.tm_sec;
bt.dayofweek = tm_buf.tm_wday + 1;
bt.weekofyear = (tm_buf.tm_yday + 7 - ((tm_buf.tm_wday + 6) % 7)) / 7;
return bt;
}
bool BacktestEngine::check_risk_allow_entry(bool is_long) const {
if (risk_halted_) return false;
if (risk_direction_ == RiskDirection::LONG_ONLY && !is_long) return false;
if (risk_direction_ == RiskDirection::SHORT_ONLY && is_long) return false;
if (risk_max_position_size_ > 0.0 && position_qty_ >= risk_max_position_size_) return false;
return true;
}
void BacktestEngine::update_risk_state() {
if (risk_halted_) return;
// Check max_drawdown
if (risk_max_drawdown_ > 0.0) {
double threshold = risk_max_drawdown_;
if (risk_max_drawdown_is_pct_) {
// percent_of_equity: threshold is pct% of peak equity
threshold = max_equity_ * (risk_max_drawdown_ / 100.0);
}
if (max_drawdown_ >= threshold) {
risk_halted_ = true;
return;
}
}
// Check max_intraday_loss
if (risk_max_intraday_loss_ > 0.0) {
BarTime bt = _decompose_bar_time_chart_tz();
int cur_day = bt.dayofmonth * 100 + bt.month;
if (cur_day != intraday_pnl_day_) {
intraday_pnl_day_ = cur_day;
intraday_pnl_ = 0.0;
}
double intraday_threshold = risk_max_intraday_loss_;
if (risk_max_intraday_loss_is_pct_) {
double eq = initial_capital_ + net_profit_sum_ + open_profit(current_bar_.close);
intraday_threshold = eq * (risk_max_intraday_loss_ / 100.0);
}
if (intraday_pnl_ < 0.0 && (-intraday_pnl_) >= intraday_threshold) {
risk_halted_ = true;
return;
}
}
// Check max_cons_loss_days
if (risk_max_cons_loss_days_ > 0 && cons_loss_day_count_ >= risk_max_cons_loss_days_) {
risk_halted_ = true;
return;
}
}
// Tracks favorable (max_runup / MFE) and adverse (max_drawdown / MAE) price
// excursion per open pyramid entry.
//
// We sample three representative prices per call — high, low, close — so a
// single daily bar fully captures both extremes without requiring tick-level
// resolution. During bar magnifier the high/low are running_high/running_low
// of the sampled path so no double-counting occurs, and close is the current
// sampled price.
void BacktestEngine::update_per_trade_extremes() {
bool is_long = (position_side_ == PositionSide::LONG);
double hi = current_bar_.high;
double lo = current_bar_.low;
double cl = current_bar_.close;
for (auto& pe : pyramid_entries_) {
// Intrabar-fill masks: on the bar a priced entry filled mid-bar, an
// extreme that the assumed OHLC path reaches BEFORE the fill is not
// part of this trade's excursion — substitute the fill price (zero
// excursion) for that extreme. Post-fill path beyond the masked
// extreme is still captured by the close sample below. Later bars
// (entry_bar_index != bar_index_) always sample the full range.
double pe_hi = hi;
double pe_lo = lo;
if (pe.entry_bar_index == bar_index_) {
if (pe.skip_entry_bar_high) pe_hi = pe.price;
if (pe.skip_entry_bar_low) pe_lo = pe.price;
}
// Favorable price: long -> high, short -> low
double fav_px = is_long ? pe_hi : pe_lo;
double adv_px = is_long ? pe_lo : pe_hi;
double favorable = is_long ? (fav_px - pe.price) * pe.qty
: (pe.price - fav_px) * pe.qty;
double adverse = is_long ? (pe.price - adv_px) * pe.qty
: (adv_px - pe.price) * pe.qty;
if (favorable > pe.max_runup) pe.max_runup = favorable;
if (adverse > pe.max_drawdown) pe.max_drawdown = adverse;
// Also consider close — in the magnifier path high/low include the
// running extremes but the final sampled price matters for mid-bar
// exits that close the trade before the bar completes.
double closing = is_long ? (cl - pe.price) * pe.qty
: (pe.price - cl) * pe.qty;
if (closing > pe.max_runup) pe.max_runup = closing;
double closing_dd = -closing;
if (closing_dd > pe.max_drawdown) pe.max_drawdown = closing_dd;
}
}
} // namespace pineforge