feat: 24/7 synthetic rollover calibration, stop-limit orders, MT5 wave model, and open prices mode fixes
This commit is contained in:
@@ -159,6 +159,8 @@ public:
|
|||||||
|
|
||||||
account_ = std::make_unique<Account>(acct_cfg);
|
account_ = std::make_unique<Account>(acct_cfg);
|
||||||
book_ = std::make_unique<OrderBook>(*account_, sym_);
|
book_ = std::make_unique<OrderBook>(*account_, sym_);
|
||||||
|
book_->set_open_prices_mode(cfg.tick_mode == TickMode::OPEN_PRICES);
|
||||||
|
last_swap_day_ = -1;
|
||||||
|
|
||||||
// Configure latency simulation
|
// Configure latency simulation
|
||||||
OrderBook::LatencyConfig lat_cfg{};
|
OrderBook::LatencyConfig lat_cfg{};
|
||||||
@@ -490,12 +492,30 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void _apply_swaps() {
|
void _apply_swaps() {
|
||||||
if (sym_.swap_mode == 0) return;
|
if (!cfg_.enable_swaps || sym_.swap_mode == 0) return;
|
||||||
for (const auto& pos : book_->active_positions()) {
|
if (!bars_ || current_bar_idx_ >= n_bars_) return;
|
||||||
double swap_per_lot = pos.is_buy() ? sym_.swap_long : sym_.swap_short;
|
|
||||||
if (std::abs(swap_per_lot) < 1e-10) continue;
|
int64_t current_day = bars_[current_bar_idx_].time / 86400;
|
||||||
double swap_charge = swap_per_lot * pos.volume;
|
if (last_swap_day_ == -1) {
|
||||||
account_->apply_pnl(swap_charge);
|
last_swap_day_ = current_day;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_day > last_swap_day_) {
|
||||||
|
// Day rollover occurred: charge swap for each calendar day passed
|
||||||
|
for (int64_t d = last_swap_day_ + 1; d <= current_day; ++d) {
|
||||||
|
// If swap_rollover3days is disabled (-1), it's a 24/7 continuous market (e.g. Deriv Synthetics, Crypto):
|
||||||
|
// 1x daily swap is charged every single day including Saturday and Sunday.
|
||||||
|
// If swap_rollover3days >= 0 (e.g. 3 for Wednesday in Forex): weekends have no swap, rollover day carries 3x.
|
||||||
|
double multiplier = 1.0;
|
||||||
|
if (sym_.swap_rollover3days >= 0) {
|
||||||
|
int dow = static_cast<int>((d + 4) % 7);
|
||||||
|
if (dow == 0 || dow == 6) continue; // No weekend swap for closed traditional markets
|
||||||
|
if (dow == sym_.swap_rollover3days) multiplier = 3.0; // Triple swap day
|
||||||
|
}
|
||||||
|
book_->apply_swap(multiplier);
|
||||||
|
}
|
||||||
|
last_swap_day_ = current_day;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,6 +541,7 @@ private:
|
|||||||
std::vector<double> spread_storage_;
|
std::vector<double> spread_storage_;
|
||||||
int32_t current_bar_idx_ = 0;
|
int32_t current_bar_idx_ = 0;
|
||||||
int32_t current_tick_in_bar_ = 0;
|
int32_t current_tick_in_bar_ = 0;
|
||||||
|
int64_t last_swap_day_ = -1;
|
||||||
bool is_done_ = false;
|
bool is_done_ = false;
|
||||||
bool initialized_ = false;
|
bool initialized_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
+146
-12
@@ -53,6 +53,25 @@ public:
|
|||||||
const LatencyConfig& latency_config() const { return latency_; }
|
const LatencyConfig& latency_config() const { return latency_; }
|
||||||
const std::vector<InFlightOrder>& in_flight_orders() const { return in_flight_orders_; }
|
const std::vector<InFlightOrder>& in_flight_orders() const { return in_flight_orders_; }
|
||||||
|
|
||||||
|
void set_open_prices_mode(bool b) { is_open_prices_mode_ = b; }
|
||||||
|
bool is_open_prices_mode() const { return is_open_prices_mode_; }
|
||||||
|
|
||||||
|
// MT5 Rollover swap application
|
||||||
|
void apply_swap(double multiplier = 1.0) {
|
||||||
|
if (symbol_.swap_mode == 0 || std::abs(multiplier) < 1e-6) return;
|
||||||
|
for (auto& pos : active_positions_) {
|
||||||
|
double raw = pos.is_buy() ? symbol_.swap_long : symbol_.swap_short;
|
||||||
|
if (std::abs(raw) < 1e-12) continue;
|
||||||
|
double charge = 0.0;
|
||||||
|
if (symbol_.swap_mode == 1) { // Points
|
||||||
|
charge = (raw * symbol_.point / symbol_.trade_tick_size) * symbol_.trade_tick_value * pos.volume;
|
||||||
|
} else { // Money / Currency
|
||||||
|
charge = raw * pos.volume;
|
||||||
|
}
|
||||||
|
pos.swap += charge * multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void set_transaction_callback(TransactionCallback cb) {
|
void set_transaction_callback(TransactionCallback cb) {
|
||||||
on_transaction_ = cb;
|
on_transaction_ = cb;
|
||||||
}
|
}
|
||||||
@@ -89,7 +108,8 @@ public:
|
|||||||
|
|
||||||
switch (req.action) {
|
switch (req.action) {
|
||||||
case TRADE_ACTION_DEAL: {
|
case TRADE_ACTION_DEAL: {
|
||||||
if (latency_.mode != LatencyMode::NONE) {
|
// MT5 explicitly discards execution delay in OPEN_PRICES testing mode
|
||||||
|
if (latency_.mode != LatencyMode::NONE && !is_open_prices_mode_) {
|
||||||
int64_t delay = _compute_delay_ms();
|
int64_t delay = _compute_delay_ms();
|
||||||
if (delay > 0) {
|
if (delay > 0) {
|
||||||
InFlightOrder ifo{};
|
InFlightOrder ifo{};
|
||||||
@@ -195,28 +215,61 @@ public:
|
|||||||
void evaluate_bar_extremes(const MqlRates& bar, double spread, int32_t bar_index) {
|
void evaluate_bar_extremes(const MqlRates& bar, double spread, int32_t bar_index) {
|
||||||
double ask_high = bar.high + spread;
|
double ask_high = bar.high + spread;
|
||||||
double ask_low = bar.low + spread;
|
double ask_low = bar.low + spread;
|
||||||
|
bool bull_bar = (bar.close >= bar.open);
|
||||||
|
|
||||||
for (auto it = active_positions_.begin(); it != active_positions_.end(); ) {
|
for (auto it = active_positions_.begin(); it != active_positions_.end(); ) {
|
||||||
bool closed = false;
|
bool closed = false;
|
||||||
if (it->is_buy()) {
|
if (it->is_buy()) {
|
||||||
// Check SL first (conservative stop-first model)
|
bool sl_hit = (it->sl > EPSILON_PRICE && bar.low <= it->sl);
|
||||||
if (it->sl > EPSILON_PRICE && bar.low <= it->sl) {
|
bool tp_hit = (it->tp > EPSILON_PRICE && bar.high >= it->tp);
|
||||||
|
|
||||||
|
if (sl_hit && tp_hit) {
|
||||||
|
// Both SL and TP touched in same bar:
|
||||||
|
// In MT5's wave model:
|
||||||
|
// Bullish bar (dip then rally): Low came first -> SL triggered.
|
||||||
|
// Bearish bar (spike then sell-off): High came first -> TP triggered.
|
||||||
|
if (bull_bar) {
|
||||||
|
double exit_p = (bar.open <= it->sl) ? bar.open : it->sl;
|
||||||
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_SL);
|
||||||
|
} else {
|
||||||
|
double exit_p = (bar.open >= it->tp) ? bar.open : it->tp;
|
||||||
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_TP);
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
} else if (sl_hit) {
|
||||||
double exit_p = (bar.open <= it->sl) ? bar.open : it->sl;
|
double exit_p = (bar.open <= it->sl) ? bar.open : it->sl;
|
||||||
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_SL);
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_SL);
|
||||||
closed = true;
|
closed = true;
|
||||||
} else if (it->tp > EPSILON_PRICE && bar.high >= it->tp) {
|
} else if (tp_hit) {
|
||||||
double exit_p = (bar.open >= it->tp) ? bar.open : it->tp;
|
double exit_p = (bar.open >= it->tp) ? bar.open : it->tp;
|
||||||
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_TP);
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_TP);
|
||||||
closed = true;
|
closed = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Sell position exit executed on Ask
|
// Sell position exit executed on Ask
|
||||||
if (it->sl > EPSILON_PRICE && ask_high >= it->sl) {
|
bool sl_hit = (it->sl > EPSILON_PRICE && ask_high >= it->sl);
|
||||||
|
bool tp_hit = (it->tp > EPSILON_PRICE && ask_low <= it->tp);
|
||||||
|
|
||||||
|
if (sl_hit && tp_hit) {
|
||||||
|
// Both SL and TP touched in same bar for Sell:
|
||||||
|
// Bearish bar (spike then drop): High/Ask peaked first -> SL triggered.
|
||||||
|
// Bullish bar (dip then rally): Low/Ask dipped first -> TP triggered.
|
||||||
|
if (!bull_bar) {
|
||||||
|
double ask_open = bar.open + spread;
|
||||||
|
double exit_p = (ask_open >= it->sl) ? ask_open : it->sl;
|
||||||
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_SL);
|
||||||
|
} else {
|
||||||
|
double ask_open = bar.open + spread;
|
||||||
|
double exit_p = (ask_open <= it->tp) ? ask_open : it->tp;
|
||||||
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_TP);
|
||||||
|
}
|
||||||
|
closed = true;
|
||||||
|
} else if (sl_hit) {
|
||||||
double ask_open = bar.open + spread;
|
double ask_open = bar.open + spread;
|
||||||
double exit_p = (ask_open >= it->sl) ? ask_open : it->sl;
|
double exit_p = (ask_open >= it->sl) ? ask_open : it->sl;
|
||||||
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_SL);
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_SL);
|
||||||
closed = true;
|
closed = true;
|
||||||
} else if (it->tp > EPSILON_PRICE && ask_low <= it->tp) {
|
} else if (tp_hit) {
|
||||||
double ask_open = bar.open + spread;
|
double ask_open = bar.open + spread;
|
||||||
double exit_p = (ask_open <= it->tp) ? ask_open : it->tp;
|
double exit_p = (ask_open <= it->tp) ? ask_open : it->tp;
|
||||||
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_TP);
|
_close_position_internal(it, exit_p, it->volume, DEAL_REASON_TP);
|
||||||
@@ -390,18 +443,36 @@ private:
|
|||||||
if (req.tp > EPSILON_PRICE) it->tp = req.tp;
|
if (req.tp > EPSILON_PRICE) it->tp = req.tp;
|
||||||
} else {
|
} else {
|
||||||
// Opposite direction: reduce or reverse
|
// Opposite direction: reduce or reverse
|
||||||
if (vol < it->volume) {
|
bool pos_was_buy = it->is_buy();
|
||||||
|
double close_vol = std::min(it->volume, vol);
|
||||||
|
double price_diff = pos_was_buy ? (fill_price - it->price_open) : (it->price_open - fill_price);
|
||||||
|
double gross_profit = symbol_.price_pnl(price_diff, close_vol);
|
||||||
|
double exit_comm = symbol_.commission_cost(close_vol) * 0.5;
|
||||||
|
double realized_swap = (it->volume > EPSILON_PRICE) ? it->swap * (close_vol / it->volume) : 0.0;
|
||||||
|
it->swap -= realized_swap;
|
||||||
|
|
||||||
|
double net_realized = gross_profit - exit_comm + realized_swap;
|
||||||
|
account_.apply_pnl(net_realized);
|
||||||
|
|
||||||
|
deal.entry = (vol >= it->volume - EPSILON_PRICE) ? (vol > it->volume + EPSILON_PRICE ? DEAL_ENTRY_INOUT : DEAL_ENTRY_OUT) : DEAL_ENTRY_OUT;
|
||||||
|
deal.profit = gross_profit - exit_comm;
|
||||||
|
deal.swap = realized_swap;
|
||||||
|
deal.commission += exit_comm;
|
||||||
|
|
||||||
|
if (vol < it->volume - EPSILON_PRICE) {
|
||||||
it->volume -= vol;
|
it->volume -= vol;
|
||||||
} else if (std::abs(vol - it->volume) < EPSILON_PRICE) {
|
} else if (std::abs(vol - it->volume) <= EPSILON_PRICE) {
|
||||||
// Fully closed
|
// Fully closed
|
||||||
active_positions_.erase(it);
|
active_positions_.erase(it);
|
||||||
} else {
|
} else {
|
||||||
// Reversed
|
// Reversed: remaining volume becomes new position in opposite direction
|
||||||
|
double rem_vol = vol - it->volume;
|
||||||
it->type = is_buy ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
|
it->type = is_buy ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
|
||||||
it->volume = vol - it->volume;
|
it->volume = rem_vol;
|
||||||
it->price_open = fill_price;
|
it->price_open = fill_price;
|
||||||
it->sl = req.sl;
|
it->sl = req.sl;
|
||||||
it->tp = req.tp;
|
it->tp = req.tp;
|
||||||
|
it->swap = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,8 +693,17 @@ private:
|
|||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
void _evaluate_pending_orders(int32_t bar_index) {
|
void _evaluate_pending_orders(int32_t bar_index) {
|
||||||
for (auto it = active_orders_.begin(); it != active_orders_.end(); ) {
|
for (auto it = active_orders_.begin(); it != active_orders_.end(); ) {
|
||||||
// Expiration check
|
// Expiration check: handle GTC, DAY, and SPECIFIED timestamps
|
||||||
if (it->time_expiration > 0 && current_tick_.time >= it->time_expiration) {
|
bool expired = false;
|
||||||
|
if (it->time_type == ORDER_TIME_DAY) {
|
||||||
|
int64_t placed_day = it->time_setup / 86400;
|
||||||
|
int64_t current_day = current_tick_.time / 86400;
|
||||||
|
if (current_day > placed_day) expired = true;
|
||||||
|
} else if (it->time_expiration > 0 && current_tick_.time >= it->time_expiration) {
|
||||||
|
expired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expired) {
|
||||||
it->state = ORDER_STATE_EXPIRED;
|
it->state = ORDER_STATE_EXPIRED;
|
||||||
history_orders_.push_back(*it);
|
history_orders_.push_back(*it);
|
||||||
it = active_orders_.erase(it);
|
it = active_orders_.erase(it);
|
||||||
@@ -646,6 +726,42 @@ private:
|
|||||||
} else if (ot == ORDER_TYPE_SELL_STOP && current_tick_.bid <= it->price_open) {
|
} else if (ot == ORDER_TYPE_SELL_STOP && current_tick_.bid <= it->price_open) {
|
||||||
triggered = true;
|
triggered = true;
|
||||||
fill_price = current_tick_.bid;
|
fill_price = current_tick_.bid;
|
||||||
|
} else if (ot == ORDER_TYPE_BUY_STOP_LIMIT && current_tick_.ask >= it->price_open) {
|
||||||
|
// MT5: Once stop price is reached, convert to pending Buy Limit at price_stoplimit
|
||||||
|
it->type = ORDER_TYPE_BUY_LIMIT;
|
||||||
|
it->price_open = it->price_stoplimit;
|
||||||
|
it->state = ORDER_STATE_PLACED;
|
||||||
|
if (on_transaction_) {
|
||||||
|
MqlTradeTransaction trans{};
|
||||||
|
trans.order = it->ticket;
|
||||||
|
trans.symbol = it->symbol;
|
||||||
|
trans.type = TRADE_TRANSACTION_ORDER_UPDATE;
|
||||||
|
trans.order_type = it->type;
|
||||||
|
trans.order_state = it->state;
|
||||||
|
trans.price = it->price_open;
|
||||||
|
trans.volume = it->volume_current;
|
||||||
|
on_transaction_(trans);
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
} else if (ot == ORDER_TYPE_SELL_STOP_LIMIT && current_tick_.bid <= it->price_open) {
|
||||||
|
// MT5: Once stop price is reached, convert to pending Sell Limit at price_stoplimit
|
||||||
|
it->type = ORDER_TYPE_SELL_LIMIT;
|
||||||
|
it->price_open = it->price_stoplimit;
|
||||||
|
it->state = ORDER_STATE_PLACED;
|
||||||
|
if (on_transaction_) {
|
||||||
|
MqlTradeTransaction trans{};
|
||||||
|
trans.order = it->ticket;
|
||||||
|
trans.symbol = it->symbol;
|
||||||
|
trans.type = TRADE_TRANSACTION_ORDER_UPDATE;
|
||||||
|
trans.order_type = it->type;
|
||||||
|
trans.order_state = it->state;
|
||||||
|
trans.price = it->price_open;
|
||||||
|
trans.volume = it->volume_current;
|
||||||
|
on_transaction_(trans);
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggered) {
|
if (triggered) {
|
||||||
@@ -736,6 +852,23 @@ private:
|
|||||||
// Internal: Stop-Out Execution
|
// Internal: Stop-Out Execution
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
void _handle_stopout(int32_t bar_index) {
|
void _handle_stopout(int32_t bar_index) {
|
||||||
|
if (!account_.is_stopout() || active_positions_.empty()) return;
|
||||||
|
|
||||||
|
// MT5: Cancel and delete all pending orders on stopout trigger
|
||||||
|
for (auto& ord : active_orders_) {
|
||||||
|
ord.state = ORDER_STATE_CANCELED;
|
||||||
|
history_orders_.push_back(ord);
|
||||||
|
if (on_transaction_) {
|
||||||
|
MqlTradeTransaction trans{};
|
||||||
|
trans.order = ord.ticket;
|
||||||
|
trans.symbol = ord.symbol;
|
||||||
|
trans.type = TRADE_TRANSACTION_ORDER_DELETE;
|
||||||
|
trans.order_state = ORDER_STATE_CANCELED;
|
||||||
|
on_transaction_(trans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active_orders_.clear();
|
||||||
|
|
||||||
while (account_.is_stopout() && !active_positions_.empty()) {
|
while (account_.is_stopout() && !active_positions_.empty()) {
|
||||||
account_.record_stopout();
|
account_.record_stopout();
|
||||||
|
|
||||||
@@ -798,6 +931,7 @@ private:
|
|||||||
uint64_t rng_state_ = 42;
|
uint64_t rng_state_ = 42;
|
||||||
std::vector<InFlightOrder> in_flight_orders_;
|
std::vector<InFlightOrder> in_flight_orders_;
|
||||||
int32_t current_bar_index_ = 0;
|
int32_t current_bar_index_ = 0;
|
||||||
|
bool is_open_prices_mode_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace edge
|
} // namespace edge
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ struct SymbolSpec {
|
|||||||
double swap_long; // per lot per night (currency)
|
double swap_long; // per lot per night (currency)
|
||||||
double swap_short;
|
double swap_short;
|
||||||
int32_t swap_mode; // 0=off, 1=points, 2=money
|
int32_t swap_mode; // 0=off, 1=points, 2=money
|
||||||
|
int32_t swap_rollover3days = -1; // -1 = disabled (24/7 Deriv synthetics), 3 = Wednesday (Forex)
|
||||||
|
|
||||||
// Convenience helpers
|
// Convenience helpers
|
||||||
double normalize_price(double price) const {
|
double normalize_price(double price) const {
|
||||||
|
|||||||
@@ -216,7 +216,8 @@ PYBIND11_MODULE(edge_tester, m) {
|
|||||||
.def_readwrite("trade_freeze_level", &SymbolSpec::trade_freeze_level)
|
.def_readwrite("trade_freeze_level", &SymbolSpec::trade_freeze_level)
|
||||||
.def_readwrite("swap_long", &SymbolSpec::swap_long)
|
.def_readwrite("swap_long", &SymbolSpec::swap_long)
|
||||||
.def_readwrite("swap_short", &SymbolSpec::swap_short)
|
.def_readwrite("swap_short", &SymbolSpec::swap_short)
|
||||||
.def_readwrite("swap_mode", &SymbolSpec::swap_mode);
|
.def_readwrite("swap_mode", &SymbolSpec::swap_mode)
|
||||||
|
.def_readwrite("swap_rollover3days", &SymbolSpec::swap_rollover3days);
|
||||||
|
|
||||||
// ---- TesterConfig ----
|
// ---- TesterConfig ----
|
||||||
py::class_<TesterConfig>(m, "TesterConfig")
|
py::class_<TesterConfig>(m, "TesterConfig")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ General-Purpose MetaTrader 5 Strategy Tester Replica (Python Interface).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import edge_tester as tester_api
|
import edge_tester as tester_api
|
||||||
from .symbol_spec import make_symbol_spec
|
from .symbol_spec import make_symbol_spec, create_symbol_spec
|
||||||
from .optimizer import StrategyOptimizer
|
from .optimizer import StrategyOptimizer
|
||||||
|
|
||||||
# Expose core tester classes directly at edge_engine top level
|
# Expose core tester classes directly at edge_engine top level
|
||||||
@@ -22,6 +22,9 @@ LatencyMode = tester_api.LatencyMode
|
|||||||
OptimizationCriterion = tester_api.OptimizationCriterion
|
OptimizationCriterion = tester_api.OptimizationCriterion
|
||||||
OptimizationAlgorithm = tester_api.OptimizationAlgorithm
|
OptimizationAlgorithm = tester_api.OptimizationAlgorithm
|
||||||
|
|
||||||
|
INIT_SUCCEEDED = tester_api.INIT_SUCCEEDED
|
||||||
|
INIT_FAILED = tester_api.INIT_FAILED
|
||||||
|
|
||||||
__version__ = "2.0.0"
|
__version__ = "2.0.0"
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Strategy",
|
"Strategy",
|
||||||
@@ -36,5 +39,8 @@ __all__ = [
|
|||||||
"OptimizationAlgorithm",
|
"OptimizationAlgorithm",
|
||||||
"StrategyOptimizer",
|
"StrategyOptimizer",
|
||||||
"make_symbol_spec",
|
"make_symbol_spec",
|
||||||
|
"create_symbol_spec",
|
||||||
|
"INIT_SUCCEEDED",
|
||||||
|
"INIT_FAILED",
|
||||||
"tester_api",
|
"tester_api",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -39,6 +39,16 @@ def _eval_pass(
|
|||||||
if hasattr(spec, k):
|
if hasattr(spec, k):
|
||||||
setattr(spec, k, v)
|
setattr(spec, k, v)
|
||||||
|
|
||||||
|
# Optional preparation hook for strategies that precalculate buffers
|
||||||
|
if hasattr(strat, "prepare"):
|
||||||
|
strat.prepare(
|
||||||
|
bars_dict["open"],
|
||||||
|
bars_dict["high"],
|
||||||
|
bars_dict["low"],
|
||||||
|
bars_dict["close"],
|
||||||
|
spec
|
||||||
|
)
|
||||||
|
|
||||||
# Build config
|
# Build config
|
||||||
cfg = et.TesterConfig()
|
cfg = et.TesterConfig()
|
||||||
for k, v in cfg_dict.items():
|
for k, v in cfg_dict.items():
|
||||||
@@ -129,7 +139,9 @@ class StrategyOptimizer:
|
|||||||
"high": df["high"].to_numpy(np.float64),
|
"high": df["high"].to_numpy(np.float64),
|
||||||
"low": df["low"].to_numpy(np.float64),
|
"low": df["low"].to_numpy(np.float64),
|
||||||
"close": df["close"].to_numpy(np.float64),
|
"close": df["close"].to_numpy(np.float64),
|
||||||
"time": df["time"].to_numpy(np.int64) if "time" in df.columns else np.zeros(n, np.int64),
|
"time": (df["time"].astype("int64") // 10**9).to_numpy(np.int64)
|
||||||
|
if pd.api.types.is_datetime64_any_dtype(df["time"])
|
||||||
|
else df["time"].to_numpy(np.int64) if "time" in df.columns else np.zeros(n, np.int64),
|
||||||
"volume": df["tick_volume"].to_numpy(np.int64) if "tick_volume" in df.columns else np.zeros(n, np.int64),
|
"volume": df["tick_volume"].to_numpy(np.int64) if "tick_volume" in df.columns else np.zeros(n, np.int64),
|
||||||
"spread": df["spread"].to_numpy(np.int32) if "spread" in df.columns else np.zeros(n, np.int32),
|
"spread": df["spread"].to_numpy(np.int32) if "spread" in df.columns else np.zeros(n, np.int32),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,3 +70,26 @@ def make_symbol_spec(name: str, try_mt5: bool = False) -> Dict[str, Any]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return spec
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
def create_symbol_spec(name: str, try_mt5: bool = False):
|
||||||
|
"""Instantiate and populate a C++ SymbolSpec object for the given instrument."""
|
||||||
|
import edge_tester as et
|
||||||
|
d = make_symbol_spec(name, try_mt5=try_mt5)
|
||||||
|
sym = et.SymbolSpec()
|
||||||
|
sym.name = name
|
||||||
|
sym.point = float(d.get("point", 0.01))
|
||||||
|
sym.digits = int(d.get("digits", 2))
|
||||||
|
sym.spread_val = float(d.get("spread_val", 0.0))
|
||||||
|
sym.trade_tick_size = float(d.get("trade_tick_size", 0.01))
|
||||||
|
sym.trade_tick_value = float(d.get("trade_tick_value", 0.01))
|
||||||
|
sym.volume_min = float(d.get("volume_min", 0.01))
|
||||||
|
sym.volume_max = float(d.get("volume_max", 100.0))
|
||||||
|
sym.volume_step = float(d.get("volume_step", 0.01))
|
||||||
|
sym.commission_per_lot = float(d.get("commission_per_lot", 0.0))
|
||||||
|
sym.trade_stops_level = float(d.get("trade_stops_level", 0.0))
|
||||||
|
sym.trade_freeze_level = float(d.get("trade_freeze_level", 0.0))
|
||||||
|
sym.swap_long = float(d.get("swap_long", 0.0))
|
||||||
|
sym.swap_short = float(d.get("swap_short", 0.0))
|
||||||
|
return sym
|
||||||
|
|
||||||
|
|||||||
@@ -191,3 +191,53 @@ class TestAdvancedMT5Features:
|
|||||||
assert len(results) > 0
|
assert len(results) > 0
|
||||||
assert "fitness" in results[0]
|
assert "fitness" in results[0]
|
||||||
|
|
||||||
|
def test_latency_discarded_in_open_prices(self):
|
||||||
|
bars = _generate_test_bars(30)
|
||||||
|
spec = _make_spec()
|
||||||
|
cfg = et.TesterConfig()
|
||||||
|
cfg.tick_mode = et.TickMode.OPEN_PRICES
|
||||||
|
cfg.latency_mode = et.LatencyMode.FIXED
|
||||||
|
cfg.latency_fixed_ms = 5000 # Latency configured
|
||||||
|
|
||||||
|
tester = et.Tester()
|
||||||
|
strat = SimpleBuyerStrategy()
|
||||||
|
res = tester.run(
|
||||||
|
strat,
|
||||||
|
bars["open"], bars["high"], bars["low"], bars["close"],
|
||||||
|
bars["time"], bars["volume"], bars["spread"],
|
||||||
|
spec, cfg
|
||||||
|
)
|
||||||
|
# Latency should be discarded in open prices mode, trade executes immediately on bar 0
|
||||||
|
assert res.total_trades > 0
|
||||||
|
assert len(res.deals) >= 2
|
||||||
|
|
||||||
|
def test_swap_rollover_schedule(self):
|
||||||
|
# Generate bars spanning multiple days (every bar is 4 hours = 14400s)
|
||||||
|
n = 42 # 7 days
|
||||||
|
t0 = 1700006400 # 2023-11-15 00:00:00 (Wednesday!)
|
||||||
|
times = np.array([t0 + i * 14400 for i in range(n)], dtype=np.int64)
|
||||||
|
opens = np.full(n, 1000.0, dtype=np.float64)
|
||||||
|
highs = np.full(n, 1005.0, dtype=np.float64)
|
||||||
|
lows = np.full(n, 995.0, dtype=np.float64)
|
||||||
|
closes = np.full(n, 1000.0, dtype=np.float64)
|
||||||
|
vols = np.full(n, 100, dtype=np.int64)
|
||||||
|
spreads = np.full(n, 10, dtype=np.int32)
|
||||||
|
|
||||||
|
spec = _make_spec()
|
||||||
|
spec.swap_mode = 2 # currency mode
|
||||||
|
spec.swap_long = -10.0 # -$10/night
|
||||||
|
spec.swap_short = 5.0
|
||||||
|
|
||||||
|
cfg = et.TesterConfig()
|
||||||
|
cfg.enable_swaps = True
|
||||||
|
cfg.tick_mode = et.TickMode.OPEN_PRICES
|
||||||
|
|
||||||
|
tester = et.Tester()
|
||||||
|
strat = SimpleBuyerStrategy()
|
||||||
|
res = tester.run(strat, opens, highs, lows, closes, times, vols, spreads, spec, cfg)
|
||||||
|
assert res.total_trades > 0
|
||||||
|
# Position was held across days, should accumulate negative swap
|
||||||
|
deals = res.deals
|
||||||
|
exit_deal = deals[-1]
|
||||||
|
assert exit_deal.swap < 0.0, f"Expected negative swap, got {exit_deal.swap}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user