Skip to content

Commit 0223fc5

Browse files
authored
Merge pull request #23 from folg-code/tests/strategy
Tests/strategy
2 parents d3bb8e8 + 4e27516 commit 0223fc5

46 files changed

Lines changed: 1890 additions & 1401 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Strategies/Samplestrategy.py

Lines changed: 16 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import pandas as pd
22
import talib.abstract as ta
33

4-
from Strategies.utils.decorators import informative
54
from core.backtesting.reporting.core.context import ContextSpec
65
from core.backtesting.reporting.core.metrics import ExpectancyMetric, MaxDrawdownMetric
7-
from core.strategy.BaseStrategy import BaseStrategy
86
from FeatureEngineering.MarketStructure.engine import MarketStructureEngine
9-
7+
from core.strategy.base import BaseStrategy
8+
from core.strategy.informatives import informative
109

1110
class Samplestrategy(BaseStrategy):
1211

@@ -15,13 +14,11 @@ def __init__(
1514
df,
1615
symbol,
1716
startup_candle_count,
18-
provider,
1917
):
2018
super().__init__(
2119
df=df,
2220
symbol=symbol,
2321
startup_candle_count=startup_candle_count,
24-
provider=provider,
2522
)
2623

2724
@informative("M30")
@@ -100,85 +97,31 @@ def populate_entry_trend(self):
10097
# LONG MEAN REVERSION SETUP
10198
# =====================
10299

103-
mr_env = True
104-
105-
setup_mr_long = (
106-
mr_env
107-
& ((df['close'] < df["bos_bull_level"]) | (df['close'] < df["mss_bull_level"]) )
108-
)
109100

110-
trigger_mr_long = (
111-
setup_mr_long
112-
& (df["close"] > df["open"])
113-
)
114101

115102
# =====================
116103
# SHORT CONTINUATION SETUP
117104
# =====================
118-
setup_continuation_short = (
119-
#df["bias_short_M30"]
120-
(df["trend_regime"] == "trend_down")
121-
& df["bos_bear_event"]
122-
& df["bos_bear_ft_valid"]
123-
# & (df["bos_bear_struct_vol"] == "high")
124-
)
125105

126-
trigger_continuation_short = (
127-
setup_continuation_short
128-
& (df["close"] < df["open"])
129-
)
130-
131-
# =====================
132-
# SHORT MEAN REVERSION SETUP
133-
# =====================
134-
setup_mr_short = (
135-
mr_env
136-
& ((df['close'] > df["bos_bull_level"]) | (df['close'] > df["mss_bull_level"]) )
137-
)
138-
139-
trigger_mr_short = (
140-
setup_mr_short
141-
& (df["close"] < df["open"])
142-
)
143-
144-
# =====================
145-
# SIGNALS (PRIORITY)
146-
# =====================
147-
148-
# =====================
149-
# SIGNALS (PRIORITY)
150-
# =====================
106+
setup_mr_long = df["close"] > df["open"]
107+
setup_continuation_short = df["close"] < df["open"]
151108

152109
df["signal_entry"] = None
153110

154-
# --- CONTINUATION FIRST ---
155-
df.loc[trigger_continuation_long, "signal_entry"] = pd.Series(
156-
[{"direction": "long", "tag": "bos_continuation_long"}]
157-
* trigger_continuation_long.sum(),
158-
index=df.index[trigger_continuation_long],
159-
)
111+
idx = df.index[setup_mr_long]
160112

161-
df.loc[trigger_continuation_short, "signal_entry"] = pd.Series(
162-
[{"direction": "short", "tag": "bos_continuation_short"}]
163-
* trigger_continuation_short.sum(),
164-
index=df.index[trigger_continuation_short],
113+
df.loc[idx, "signal_entry"] = pd.Series(
114+
[{"direction": "long", "tag": "long"}] * len(idx),
115+
index=idx
165116
)
166117

167-
free = df["signal_entry"].isna()
118+
idx = df.index[setup_continuation_short]
168119

169-
df.loc[trigger_mr_long & free, "signal_entry"] = pd.Series(
170-
[{"direction": "long", "tag": "mean_reversion_long"}]
171-
* (trigger_mr_long & free).sum(),
172-
index=df.index[trigger_mr_long & free],
120+
df.loc[idx, "signal_entry"] = pd.Series(
121+
[{"direction": "short", "tag": "short"}] * len(idx),
122+
index=idx
173123
)
174124

175-
df.loc[trigger_mr_short & free, "signal_entry"] = pd.Series(
176-
[{"direction": "short", "tag": "mean_reversion_short"}]
177-
* (trigger_mr_short & free).sum(),
178-
index=df.index[trigger_mr_short & free],
179-
)
180-
181-
182125
df["levels"] = None
183126

184127

@@ -192,10 +135,13 @@ def populate_entry_trend(self):
192135
axis=1
193136
)
194137

138+
print(df["signal_entry"].notna().sum())
195139

196140
self.df = df
197141

198142

143+
144+
199145
return df
200146

201147
def build_report_config(self):
@@ -224,6 +170,7 @@ def populate_exit_trend(self):
224170
self.df["signal_exit"] = None
225171
self.df["custom_stop_loss"] = None
226172

173+
227174
def calculate_levels(self, signals, close, sl_long, sl_short):
228175

229176
if not isinstance(signals, dict):

config/live.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@
3333
STARTUP_CANDLE_COUNT = 600
3434
MAX_RISK_PER_TRADE = 0.005
3535

36-
SERVER_TIMEZONE = "UTC"
36+
SERVER_TIMEZONE = "UTC"
37+
38+
EXIT_EXECUTION = {
39+
"TP1": "ENGINE", # ENGINE | BROKER | DISABLED
40+
"TP2": "BROKER", # ENGINE | BROKER | DISABLED
41+
"BE_ON_TP1": True, # move SL to BE when TP1 is reached
42+
}

core/backtesting/backtester.py

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import traceback
2-
from typing import Optional
2+
from typing import Optional, Any
33
import pandas as pd
44
from concurrent.futures import ProcessPoolExecutor, as_completed
55
import os
@@ -14,15 +14,16 @@
1414
from core.domain.cost.cost_engine import TradeCostEngine, InstrumentCtx
1515
from core.backtesting.trade_factory import TradeFactory
1616
from core.domain.risk.sizing import position_size
17+
from core.strategy.plan_builder import PlanBuildContext
1718

1819

1920
class Backtester:
2021
def __init__(self,
21-
slippage: float = 0.0,
22+
strategy,
2223
execution_policy: Optional[ExecutionPolicy] = None,
2324
cost_engine: Optional[TradeCostEngine] = None
2425
):
25-
self.slippage = slippage
26+
self.strategy = strategy
2627
self.execution_policy = execution_policy or ExecutionPolicy()
2728
self.cost_engine = cost_engine or TradeCostEngine(self.execution_policy)
2829

@@ -66,65 +67,70 @@ def _instrument_ctx(symbol: str) -> InstrumentCtx:
6667
def _backtest_single_symbol(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame:
6768
trades = []
6869

69-
df = df.copy()
70-
df["time"] = df["time"].dt.tz_localize(None)
71-
70+
time_arr = df["time"].dt.tz_localize(None).values
7271
high_arr = df["high"].values
7372
low_arr = df["low"].values
7473
close_arr = df["close"].values
75-
time_arr = df["time"].values
7674

77-
signal_arr = df["signal_entry"].values
78-
levels_arr = df["levels"].values
75+
# instrument ctx do slippage itp.
76+
ctx_inst = self._instrument_ctx(symbol)
77+
78+
# 1) Build vector-friendly plans once (shared logic)
79+
ctx = PlanBuildContext(
80+
symbol=symbol,
81+
strategy_name=type(self.strategy).__name__,
82+
strategy_config=self.strategy.strategy_config,
83+
)
84+
plans = self.strategy.build_trade_plans_backtest(df=df, ctx=ctx, allow_managed_in_backtest=False)
85+
86+
# 2) Precompute arrays used in the hot loop
87+
plan_valid = plans["plan_valid"].values
88+
plan_dir = plans["plan_direction"].values
89+
plan_tag = plans["plan_entry_tag"].values
90+
plan_sl = plans["plan_sl"].values.astype(float)
91+
plan_tp1 = plans["plan_tp1"].values.astype(float)
92+
plan_tp2 = plans["plan_tp2"].values.astype(float)
7993

80-
ctx = self._instrument_ctx(symbol)
81-
point_size = ctx.point_size
82-
pip_value = ctx.pip_value
94+
plan_sl_tag = plans["plan_sl_tag"].values.astype(str)
95+
plan_tp1_tag = plans["plan_tp1_tag"].values.astype(str)
96+
plan_tp2_tag = plans["plan_tp2_tag"].values.astype(str)
8397

8498
n = len(df)
8599

86100
for direction in ("long", "short"):
87101
dir_flag = 1 if direction == "long" else -1
88-
last_exit_by_tag = {}
102+
last_exit_by_tag: dict[str, Any] = {}
89103

90104
for entry_pos in range(n):
91-
sig = signal_arr[entry_pos]
92-
if not isinstance(sig, dict) or sig.get("direction") != direction:
105+
if not plan_valid[entry_pos]:
106+
continue
107+
if plan_dir[entry_pos] != direction:
93108
continue
94109

95-
entry_tag = sig["tag"]
110+
entry_tag = str(plan_tag[entry_pos])
96111
entry_time = time_arr[entry_pos]
97112

98113
last_exit = last_exit_by_tag.get(entry_tag)
99114
if last_exit is not None and last_exit > entry_time:
100115
continue
101116

102-
levels = levels_arr[entry_pos]
103-
if not isinstance(levels, dict):
104-
continue
105-
106-
sl = (levels.get("SL") or levels.get(0))["level"]
107-
tp1 = (levels.get("TP1") or levels.get(1))["level"]
108-
tp2 = (levels.get("TP2") or levels.get(2))["level"]
117+
sl = float(plan_sl[entry_pos])
118+
tp1 = float(plan_tp1[entry_pos])
119+
tp2 = float(plan_tp2[entry_pos])
109120

110-
level_tags = {
111-
"SL": (levels.get("SL") or levels.get(0))["tag"],
112-
"TP1": (levels.get("TP1") or levels.get(1))["tag"],
113-
"TP2": (levels.get("TP2") or levels.get(2))["tag"],
114-
}
121+
level_tags = {"SL": plan_sl_tag[entry_pos], "TP1": plan_tp1_tag[entry_pos], "TP2": plan_tp2_tag[entry_pos]}
115122

116123
entry_price = float(close_arr[entry_pos])
117124

118-
# legacy behavior: entry slippage on exec price (kept as-is)
119-
entry_price += ctx.slippage_abs if direction == "long" else -ctx.slippage_abs
125+
entry_price += ctx_inst.slippage_abs if direction == "long" else -ctx_inst.slippage_abs
120126

121127
size = position_size(
122128
entry_price=entry_price,
123129
stop_price=sl,
124130
max_risk=MAX_RISK_PER_TRADE,
125-
account_size=INITIAL_BALANCE,
126-
point_size=point_size,
127-
pip_value=pip_value,
131+
account_size=INITIAL_BALANCE, # lub INITIAL_BALANCE
132+
point_size=ctx_inst.point_size,
133+
pip_value=ctx_inst.pip_value,
128134
)
129135

130136
(
@@ -145,7 +151,7 @@ def _backtest_single_symbol(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame
145151
low_arr,
146152
close_arr,
147153
time_arr,
148-
ctx.slippage_abs,
154+
ctx_inst.slippage_abs,
149155
)
150156

151157
exit_result = ExitProcessor.process(
@@ -161,10 +167,12 @@ def _backtest_single_symbol(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame
161167
tp1=tp1,
162168
tp2=tp2,
163169
position_size=size,
164-
point_size=point_size,
165-
pip_value=pip_value,
170+
point_size=ctx_inst.point_size,
171+
pip_value=ctx_inst.pip_value,
166172
)
167173

174+
175+
168176
trade_dict = TradeFactory.create_trade(
169177
symbol=symbol,
170178
direction=direction,
@@ -175,22 +183,18 @@ def _backtest_single_symbol(self, df: pd.DataFrame, symbol: str) -> pd.DataFrame
175183
sl=sl,
176184
tp1=tp1,
177185
tp2=tp2,
178-
point_size=point_size,
179-
pip_value=pip_value,
186+
point_size=ctx_inst.point_size,
187+
pip_value=ctx_inst.pip_value,
180188
exit_result=exit_result,
181189
level_tags=level_tags,
182190
)
183191

184-
self.cost_engine.enrich(trade_dict, df=df, ctx=ctx)
192+
self.cost_engine.enrich(trade_dict, df=df, ctx=ctx_inst)
185193

186194
trades.append(trade_dict)
187195
last_exit_by_tag[entry_tag] = exit_time
188196

189197
print(f"✅ Finished backtest for {symbol}, {len(trades)} trades.")
190-
191-
df = pd.DataFrame(trades)
192-
193-
print(df.loc[df['financing_usd_total'] > 0])
194198
return pd.DataFrame(trades)
195199

196200
# -----------------------------

core/backtesting/reporting/runner.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ def run(self):
5858
strategy=self.strategy,
5959
)
6060

61-
print(ctx.trades.columns)
6261

6362
# ==================================================
6463
# BUILD REPORT (SECTIONS)

core/backtesting/runner.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from core.backtesting.backtester import Backtester
1616
from core.backtesting.plotting.plot import TradePlotter
1717

18-
from core.strategy.runner import run_strategy_single
19-
from core.strategy.strategy_loader import load_strategy_class
18+
from core.backtesting.strategy_runner import run_strategy_single
19+
from core.live_trading.strategy_loader import load_strategy_class
2020

2121

2222
class BacktestRunner:
@@ -31,7 +31,6 @@ class BacktestRunner:
3131

3232
def __init__(self, cfg):
3333
self.config = cfg
34-
self.provider = None
3534

3635
# 🔑 STRATEGY CONTRACT
3736
self.strategy = None # reference strategy (for reporting config)
@@ -167,7 +166,7 @@ def _run_backtest_window(self, start, end, label):
167166
if df_slice.empty:
168167
raise RuntimeError(f"No signals in window: {label}")
169168

170-
backtester = Backtester(slippage=self.config.SLIPPAGE)
169+
backtester = Backtester(strategy=self.strategy)
171170
trades = backtester.run_backtest(df_slice)
172171
trades["window"] = label
173172
return trades

0 commit comments

Comments
 (0)