2026-10-21 05:54:38 +01:00
|
|
|
"""
|
|
|
|
|
edge_engine/tests/test_advanced_features.py
|
|
|
|
|
|
|
|
|
|
Comprehensive tests for:
|
|
|
|
|
1. Latency & Delay Queue (Slippage Simulation)
|
|
|
|
|
2. Complete MT5 Tick Modes (OHLC_M1 4-tick intrabar wave)
|
|
|
|
|
3. InteractiveTester Bar-by-Bar and Tick-by-Tick visual replay
|
|
|
|
|
4. Custom Optimization Metric via Strategy::OnTester()
|
|
|
|
|
"""
|
|
|
|
|
import pytest
|
|
|
|
|
import numpy as np
|
|
|
|
|
import pandas as pd
|
|
|
|
|
import edge_tester as et
|
|
|
|
|
|
|
|
|
|
def _generate_test_bars(n: int = 100):
|
|
|
|
|
np.random.seed(42)
|
|
|
|
|
t0 = 1700000000
|
|
|
|
|
times = np.array([t0 + i * 300 for i in range(n)], dtype=np.int64)
|
|
|
|
|
price = 1000.0
|
|
|
|
|
opens, highs, lows, closes = [], [], [], []
|
|
|
|
|
for _ in range(n):
|
|
|
|
|
o = price
|
|
|
|
|
delta = np.random.randn() * 2.0
|
|
|
|
|
c = o + delta
|
|
|
|
|
h = max(o, c) + abs(np.random.randn())
|
|
|
|
|
l = min(o, c) - abs(np.random.randn())
|
|
|
|
|
opens.append(o)
|
|
|
|
|
highs.append(h)
|
|
|
|
|
lows.append(l)
|
|
|
|
|
closes.append(c)
|
|
|
|
|
price = c
|
|
|
|
|
return {
|
|
|
|
|
"open": np.array(opens, dtype=np.float64),
|
|
|
|
|
"high": np.array(highs, dtype=np.float64),
|
|
|
|
|
"low": np.array(lows, dtype=np.float64),
|
|
|
|
|
"close": np.array(closes, dtype=np.float64),
|
|
|
|
|
"time": times,
|
|
|
|
|
"volume": np.full(n, 100, dtype=np.int64),
|
|
|
|
|
"spread": np.full(n, 10, dtype=np.int32),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _make_spec():
|
|
|
|
|
s = et.SymbolSpec()
|
|
|
|
|
s.name = "TestSymbol"
|
|
|
|
|
s.point = 0.01
|
|
|
|
|
s.digits = 2
|
|
|
|
|
s.trade_tick_size = 0.01
|
|
|
|
|
s.trade_tick_value = 0.01
|
|
|
|
|
s.spread_val = 0.10
|
|
|
|
|
s.volume_min = 0.01
|
|
|
|
|
s.volume_max = 100.0
|
|
|
|
|
s.volume_step = 0.01
|
|
|
|
|
s.commission_per_lot = 0.0
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SimpleBuyerStrategy(et.Strategy):
|
|
|
|
|
def on_init(self):
|
|
|
|
|
self.entered = False
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
def on_bar(self):
|
|
|
|
|
if not self.entered and self.trade.positions_total() == 0:
|
|
|
|
|
self.trade.buy(1.0, 0.0, 0.0)
|
|
|
|
|
self.entered = True
|
|
|
|
|
|
|
|
|
|
def on_tester(self):
|
|
|
|
|
# Custom optimization metric: return ending balance minus initial
|
|
|
|
|
return self.trade.account_balance() - 10000.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAdvancedMT5Features:
|
|
|
|
|
def test_ohlc_m1_tick_mode(self):
|
|
|
|
|
bars = _generate_test_bars(50)
|
|
|
|
|
spec = _make_spec()
|
|
|
|
|
cfg = et.TesterConfig()
|
|
|
|
|
cfg.tick_mode = et.TickMode.OHLC_M1
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
assert res.total_trades > 0
|
|
|
|
|
assert len(res.deals) >= 2
|
|
|
|
|
assert len(res.balance_curve) == 50
|
|
|
|
|
|
|
|
|
|
def test_latency_delay_queue(self):
|
|
|
|
|
bars = _generate_test_bars(50)
|
|
|
|
|
spec = _make_spec()
|
|
|
|
|
cfg = et.TesterConfig()
|
|
|
|
|
cfg.tick_mode = et.TickMode.EVERY_TICK
|
|
|
|
|
cfg.latency_mode = et.LatencyMode.FIXED
|
|
|
|
|
cfg.latency_fixed_ms = 500 # 500ms delay
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
assert res.total_trades > 0
|
|
|
|
|
assert len(res.deals) >= 2
|
|
|
|
|
|
|
|
|
|
def test_interactive_tester_replay(self):
|
|
|
|
|
bars = _generate_test_bars(30)
|
|
|
|
|
spec = _make_spec()
|
|
|
|
|
cfg = et.TesterConfig()
|
|
|
|
|
cfg.tick_mode = et.TickMode.OPEN_PRICES
|
|
|
|
|
|
|
|
|
|
replay = et.InteractiveTester()
|
|
|
|
|
strat = SimpleBuyerStrategy()
|
|
|
|
|
ok = replay.init(
|
|
|
|
|
strat,
|
|
|
|
|
bars["open"], bars["high"], bars["low"], bars["close"],
|
|
|
|
|
bars["time"], bars["volume"], bars["spread"],
|
|
|
|
|
spec
|
|
|
|
|
)
|
|
|
|
|
assert ok is True
|
|
|
|
|
assert replay.current_bar() == 0
|
|
|
|
|
assert replay.total_bars() == 30
|
|
|
|
|
assert replay.is_done() is False
|
|
|
|
|
|
|
|
|
|
# Step 5 bars
|
|
|
|
|
for b in range(5):
|
|
|
|
|
replay.step_bar()
|
|
|
|
|
assert replay.current_bar() == 5
|
|
|
|
|
assert replay.account_balance() > 0.0
|
|
|
|
|
|
|
|
|
|
# Run remaining
|
|
|
|
|
replay.run_all()
|
|
|
|
|
assert replay.is_done() is True
|
|
|
|
|
res = replay.finish()
|
|
|
|
|
assert res.total_trades > 0
|
|
|
|
|
assert res.on_tester_value != 0.0
|
|
|
|
|
|
|
|
|
|
def test_on_tester_custom_fitness(self):
|
|
|
|
|
bars = _generate_test_bars(40)
|
|
|
|
|
spec = _make_spec()
|
|
|
|
|
cfg = et.TesterConfig()
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
assert "on_tester_value" in res.to_dict()
|
|
|
|
|
assert res.on_tester_value == pytest.approx(res.net_profit, rel=1e-3)
|
|
|
|
|
|
|
|
|
|
def test_strategy_optimizer_grid(self):
|
|
|
|
|
from edge_engine.optimizer import StrategyOptimizer
|
|
|
|
|
|
|
|
|
|
bars = _generate_test_bars(40)
|
|
|
|
|
df = pd.DataFrame(bars)
|
|
|
|
|
spec = _make_spec()
|
|
|
|
|
opt = StrategyOptimizer(
|
|
|
|
|
strategy_cls=SimpleBuyerStrategy,
|
|
|
|
|
symbol_spec=spec,
|
|
|
|
|
criterion=et.OptimizationCriterion.CUSTOM,
|
|
|
|
|
algorithm=et.OptimizationAlgorithm.EXHAUSTIVE,
|
|
|
|
|
max_workers=1,
|
|
|
|
|
)
|
|
|
|
|
param_grid = {"dummy_param": [1, 2, 3]}
|
|
|
|
|
results = opt.optimize(df, param_grid)
|
|
|
|
|
assert len(results) == 3
|
|
|
|
|
assert "fitness" in results[0]
|
|
|
|
|
|
|
|
|
|
def test_strategy_optimizer_genetic(self):
|
|
|
|
|
from edge_engine.optimizer import StrategyOptimizer
|
|
|
|
|
|
|
|
|
|
bars = _generate_test_bars(40)
|
|
|
|
|
df = pd.DataFrame(bars)
|
|
|
|
|
spec = _make_spec()
|
|
|
|
|
opt = StrategyOptimizer(
|
|
|
|
|
strategy_cls=SimpleBuyerStrategy,
|
|
|
|
|
symbol_spec=spec,
|
|
|
|
|
criterion=et.OptimizationCriterion.BALANCE_MAX,
|
|
|
|
|
algorithm=et.OptimizationAlgorithm.GENETIC,
|
|
|
|
|
max_workers=1,
|
|
|
|
|
)
|
|
|
|
|
param_grid = {"dummy_param": [10, 20, 30, 40, 50]}
|
|
|
|
|
results = opt.optimize(df, param_grid, generations=2, population_size=4)
|
|
|
|
|
assert len(results) > 0
|
|
|
|
|
assert "fitness" in results[0]
|
|
|
|
|
|
2026-10-28 01:55:56 +01:00
|
|
|
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}"
|
|
|
|
|
|