52constexpr std::int64_t kQuarter = 15LL * 60LL * 1000LL;
53constexpr double kUnits = 750.0;
54constexpr double kEntryPrice = 213.05;
55constexpr double kFeePercent = 0.1;
56constexpr double kCapital = 3000000.0;
57constexpr double kMaintenance = 0.10;
60constexpr double kBase = kCapital - kUnits * kEntryPrice * 150.0 * kFeePercent / 100.0;
63double level(
double fx) {
64 return (kEntryPrice * kUnits * fx - kBase) / (kUnits * fx * (1.0 - kMaintenance));
67bool near(
double value,
double expected,
double tolerance) {
68 return std::isfinite(value) && std::fabs(value - expected) <= tolerance;
80 std::int64_t time_ms = 0;
82 int interval_index = -1;
84 mutable std::vector<Offered> offered;
85 mutable std::optional<pineforge::NativeMarginRequirementView> roll_view;
86 std::optional<double> level_bar2;
87 std::optional<double> level_bar4;
88 std::optional<no::MarginCallEvent> margin_call;
93 void on_native_run_begin()
override { bar_ = -1; }
95 void on_native_bar(
const pineforge::Bar&,
96 const pineforge::NativeDecisionContext&)
override {
98 if (bar_ == 0)
submit({no::Transact{kUnits},
"entry",
"fx-roll"});
99 if (bar_ == 2) level_bar2 = native_liquidation_price();
100 if (bar_ == 4) level_bar4 = native_liquidation_price();
103 bool margin_check_allowed(
const pineforge::NativeMarginCheckPoint& point)
const override {
105 row.kind = point.
kind;
108 row.mark = point.
mark;
112 offered.push_back(row);
118 std::optional<pineforge::NativeMarginDecision> resolve_margin_requirement(
119 const pineforge::NativeMarginRequirementView& view)
const override {
120 if (view.
kind == pineforge::NativeMarginCheckKind::FxRoll && !roll_view) roll_view = view;
124 void on_native_margin_call(
const no::MarginCallEvent& event)
override {
146 spec.
fee_kind = pineforge::NativeFeeKind::Percent;
157 margin.
sizing = pineforge::NativeLiquidationSizing::Flatten;
158 margin.
check = pineforge::NativeLiquidationCheck::PathAdverseExtreme;
167 {212.95, 213.10, 212.85, 213.02, 900.0, 0 * kQuarter},
168 {213.05, 213.20, 212.90, 213.10, 1200.0, 1 * kQuarter},
169 {213.10, 213.25, 212.95, 213.15, 900.0, 2 * kQuarter},
170 {213.15, 213.30, 212.90, 213.00, 900.0, 3 * kQuarter},
171 {213.00, 213.10, 212.75, 212.80, 1200.0, 4 * kQuarter},
172 {212.60, 212.70, 208.40, 208.90, 3000.0, 5 * kQuarter},
173 {208.90, 209.20, 208.60, 209.00, 900.0, 6 * kQuarter},
188std::vector<FxRollExample::Offered> rolls_of(
const FxRollExample& host) {
189 std::vector<FxRollExample::Offered> rolls;
190 for (
const auto& row : host.offered) {
191 if (row.kind == pineforge::NativeMarginCheckKind::FxRoll) rolls.push_back(row);
197void run(FxRollExample& host,
bool step) {
198 const auto setup = host.configure_native(
make_spec());
199 assert(setup.status == pineforge::NativeSetupStatus::Applied);
200 const auto staged = host.configure_native_fx_curve(curve(step));
201 assert(staged.status == pineforge::NativeSetupStatus::Applied);
203 assert(host.native_state().kind == pineforge::NativeLifecycleKind::Completed);
210 FxRollExample stepped;
214 const auto rolls = rolls_of(stepped);
215 std::printf(
"FxRoll points: %zu\n", rolls.size());
216 assert(rolls.size() == 1);
217 const auto& roll = rolls[0];
218 std::printf(
" at t=%lld (bar 4's open), bar %d's %s: %g units in %zu lot, mark %.2f\n",
219 static_cast<long long>(roll.time_ms), roll.interval_index,
220 roll.phase == pineforge::NativePathPhase::Close ?
"Close" :
"?",
221 roll.units, roll.lots, roll.mark);
222 assert(roll.time_ms == 4 * kQuarter && roll.interval_index == 3
223 && roll.phase == pineforge::NativePathPhase::Close);
224 assert(roll.units == kUnits && roll.lots == 1 && roll.mark == 212.90);
230 assert(stepped.roll_view.has_value());
231 std::printf(
" requirement at the roll: equity %.4f, required %.4f\n",
232 stepped.roll_view->equity, stepped.roll_view->required);
233 assert(near(stepped.roll_view->equity, kBase - 18000.0, 1e-6));
234 assert(near(stepped.roll_view->required, kUnits * 212.90 * 160.0 * kMaintenance, 1e-6));
235 assert(stepped.roll_view->equity > stepped.roll_view->required);
238 std::printf(
"liquidation level: bar 2 %.6f (hand %.6f at 150), bar 4 %.6f (hand %.6f at 160)\n",
239 stepped.level_bar2.value_or(-1.0), level(150.0),
240 stepped.level_bar4.value_or(-1.0), level(160.0));
241 assert(stepped.level_bar2 && near(*stepped.level_bar2, level(150.0), 1e-9));
242 assert(stepped.level_bar4 && near(*stepped.level_bar4, level(160.0), 1e-9));
246 assert(stepped.margin_call.has_value());
247 const auto& call = *stepped.margin_call;
248 std::printf(
"margin call: %g units at %.6f, position %g -> %g, ticket %s\n", call.units,
249 call.mark, call.position_before, call.position_after,
250 call.request().label.c_str());
251 assert(call.units == kUnits && call.position_before == kUnits && call.position_after == 0.0);
252 assert(near(call.mark, level(160.0), 1e-9) && call.request().label ==
"margin-call");
253 assert(stepped.trade_count() == 1);
254 const auto& trade = stepped.get_trade(0);
255 const double entry_fee = kUnits * kEntryPrice * 150.0 * kFeePercent / 100.0;
256 const double exit_fee = kUnits * trade.exit_price * 160.0 * kFeePercent / 100.0;
257 std::printf(
" row: %s -> %s, commission %.4f, pnl %.4f\n", trade.entry_id.c_str(),
258 trade.exit_id.c_str(), trade.commission, trade.pnl);
259 assert(trade.exit_id ==
"margin-call" && trade.exit_price == call.mark);
260 assert(near(trade.commission, entry_fee + exit_fee, 1e-6));
261 assert(near(trade.pnl, (trade.exit_price - kEntryPrice) * kUnits * 160.0 - entry_fee - exit_fee,
267 std::printf(
"no step: FxRoll points %zu, level at bar 4 %.6f, margin call %s, closed trades %d\n",
268 rolls_of(flat).size(), flat.level_bar4.value_or(-1.0),
269 flat.margin_call ?
"yes" :
"none", flat.trade_count());
270 assert(rolls_of(flat).empty());
271 assert(flat.level_bar4 && near(*flat.level_bar4, level(150.0), 1e-9));
272 assert(!flat.margin_call.has_value() && flat.trade_count() == 0);
273 assert(flat.physical_position().signed_units == kUnits);
276 std::printf(
"FxRoll units=%g mark=%.2f no-step FxRoll points: %zu, margin calls: %d "
277 "closed trades: %d\n",
278 roll.units, roll.mark, rolls_of(flat).size(), flat.margin_call ? 1 : 0,
279 stepped.trade_count());
The public native host: an abstract subclass of BacktestEngine with no PineScript on it.
static const pf_bar_t kBars[]
static pf_native_run_spec_v1 make_spec(void)
NativeMarginCheckKind
Which kernel check point is about to test the maintenance requirement.
NativePathPhase
Which leg of a modeled OHLC walk a point sits on.
static void submit(struct host_state *state, uint32_t intent, double value, uint32_t trigger, double price, const char *label)
NativeMarginCheckKind kind
NativePhysicalPosition position
native_order::MatchCursor cursor
NativeMarginCheckKind kind
NativePathPhase path_phase
int64_t effective_time_ms
An immutable account-currency FX curve: parallel arrays of effective-from timestamps in epoch millise...
std::vector< double > account_per_quote
std::vector< std::int64_t > effective_from_ms
A generic per-side broker margin model (L4).
NativeLiquidationCheck check
NativeLiquidationSizing sizing
std::optional< double > maintenance_long
std::string liquidation_comment
std::string liquidation_label
One complete setup value, staged/copied by NativeStrategyHost before it is applied at begin.
std::optional< double > quantity_grid
std::optional< NativeMarginModel > margin
Opt-in generic margin model.
native_order::RunIdentity identity