Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FeatureEngineering/MarketStructure/trend_regime.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def apply(
if self.vol_required:
high_vol = (
(struct_vol.get(
"bos_bull_struct_vol", pd.Series(False, index=idx)) == "high")
"bos_bull_struct_vol", pd.Series(False, index=idx)) == "high")
| (struct_vol.get(
"bos_bear_struct_vol", pd.Series(False, index=idx)) == "high")
| (struct_vol.get(
Expand Down
39 changes: 8 additions & 31 deletions Strategies/Samplestrategy.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import pandas as pd
import talib.abstract as ta

from core.backtesting.reporting.core.context import ContextSpec
from core.backtesting.reporting.core.metrics import ExpectancyMetric, MaxDrawdownMetric
from core.reporting.core.context import ContextSpec
from core.reporting.core.metrics import ExpectancyMetric, MaxDrawdownMetric
from FeatureEngineering.MarketStructure.engine import MarketStructureEngine
from core.strategy.base import BaseStrategy
from core.strategy.informatives import informative

class Samplestrategy(BaseStrategy):
strategy_name = "Sample Strategy (Report)"

def __init__(
self,
Expand All @@ -28,21 +29,9 @@ def populate_indicators_M30(self, df):
# --- minimum techniczne
df["atr"] = ta.ATR(df, 14)

# --- market structure HTF
df = MarketStructureEngine.apply(
df,
features=[
"pivots",
"price_action",
"follow_through",
"structural_vol",
"trend_regime",
],
)

# --- bias flags (czytelne na GitHubie)
df["bias_long"] = df["trend_regime"] == "trend_up"
df["bias_short"] = df["trend_regime"] == "trend_down"



return df

Expand All @@ -53,17 +42,7 @@ def populate_indicators(self):
# --- base indicators
df["atr"] = ta.ATR(df, 14)

# --- market structure
df = MarketStructureEngine.apply(
df,
features=[
"pivots",
"price_action",
"follow_through",
"structural_vol",
"trend_regime",
],
)


df['low_15'] = df['low'].rolling(15).min()
df['high_15'] = df['high'].rolling(15).max()
Expand Down Expand Up @@ -135,19 +114,17 @@ def populate_entry_trend(self):
axis=1
)

print(df["signal_entry"].notna().sum())

self.df = df




return df

def build_report_config(self):
def build_report_spec(self):
return (
super()
.build_report_config()
.build_report_spec()
.add_metric(ExpectancyMetric())
.add_metric(MaxDrawdownMetric())
.add_context(
Expand Down
20 changes: 20 additions & 0 deletions backtest_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,25 @@


if __name__ == "__main__":
import cProfile
import pstats
from pathlib import Path
from datetime import datetime

run_path = Path(
f"results/run_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
)
run_path.mkdir(parents=True, exist_ok=True)

profile_path = run_path / "profile_full.prof"

profiler = cProfile.Profile()
profiler.enable()

BacktestRunner(cfg).run()

profiler.disable()
profiler.dump_stats(profile_path)

print(f"[PROFILE] saved to {profile_path}")

127 changes: 77 additions & 50 deletions config/backtest.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import logging
from enum import Enum
from pathlib import Path

from config.logger_config import LoggerConfig

logging.basicConfig(level=logging.INFO)

# ==================================================
# DATA
# DATA SOURCE & TIME
# ==================================================

MARKET_DATA_PATH = "market_data"
BACKTEST_DATA_BACKEND = "dukascopy" # "dukascopy"
BACKTEST_DATA_BACKEND = "dukascopy" # "dukascopy", "csv", ...

SERVER_TIMEZONE = "UTC"

TIMERANGE = {
"start": "2025-12-15",
"end": "2025-12-31",
"start": "2025-01-01",
"end": "2025-12-29",
}

BACKTEST_MODE = "single" # "single" | "split"
Expand All @@ -22,92 +28,113 @@
"FINAL": ("2025-12-24", "2025-12-31"),
}

# Missing data handling metadata (report/UI)
# Examples:
# - "Forward-fill OHLC gaps"
# - "Drop candles with gaps"
# - "Leave gaps (no fill)"
# Metadata only (for reports / README)
MISSING_DATA_HANDLING = "Forward-fill OHLC gaps"

SERVER_TIMEZONE = "UTC"

# ==================================================
# STRATEGY
# STRATEGY DEFINITION
# ==================================================

# If None, report will fall back to STRATEGY_CLASS.
STRATEGY_NAME = "Sample "
STRATEGY_CLASS = "Samplestrategyreport"

# Optional short description (used in BacktestConfigSection)
STRATEGY_NAME = "Sample Strategy"
STRATEGY_DESCRIPTION = "Sample strategy for dashboard showcase"

# Strategy class locator (string used by your loader)
STRATEGY_CLASS = "Samplestrategyreport"
STARTUP_CANDLE_COUNT = 600

SYMBOLS = [
"XAUUSD",
"EURUSD"
]

TIMEFRAME = "M1"


# ==================================================
# EXECUTION (SIMULATED)
# EXECUTION MODEL (SIMULATED)
# ==================================================

INITIAL_BALANCE = 10_000

# Slippage assumed in "pips" for FX-like instruments in current convention.
SLIPPAGE = 0.1

# Optional: separate slippage for entry/exit (metadata now; logic later)
SLIPPAGE_ENTRY = None # if None -> use SLIPPAGE
SLIPPAGE_EXIT = None # if None -> use SLIPPAGE

MAX_RISK_PER_TRADE = 0.005

# Execution delay metadata (not implemented yet)
# Could be "None", "1 bar", "200ms", etc.
EXECUTION_DELAY = "None"
# Slippage (metadata + future logic)
SLIPPAGE = 0.1
SLIPPAGE_ENTRY = None # if None -> SLIPPAGE
SLIPPAGE_EXIT = None # if None -> SLIPPAGE

# Execution / order semantics (metadata for now)
EXECUTION_DELAY = "None" # e.g. "1 bar", "200ms"

# Order types (metadata now; logic later)
# Use: "market" | "limit"
ENTRY_ORDER_TYPE_DEFAULT = "market"
ENTRY_ORDER_TYPE_DEFAULT = "market" # "market" | "limit"
EXIT_ORDER_TYPE_DEFAULT = "limit"
TP_ORDER_TYPE_DEFAULT = "limit"

EXIT_OVERRIDES_DESC = (
"SL/BE/EOD treated as market exits; "
"TP exits treated as limit (strategy-dependent)."
)

# explanation for report (avoid wrong claims)
EXIT_OVERRIDES_DESC = "SL/BE/EOD treated as market exits; TP exits treated as limit (strategy-dependent)."
SPREAD_MODEL = "fixed_cost_overlay" # or "bid_ask_simulation"

# Spread model metadata:
# - "fixed_cost_overlay" (current reality: compute spread_usd_* from per-instrument fixed spreads)
# - "bid_ask_simulation" (future: actual bid/ask price simulation in fills)
SPREAD_MODEL = "fixed_cost_overlay"
# ==================================================
# CAPITAL & RISK MODEL (REPORTING / FUTURE)
# ==================================================

POSITION_SIZING_MODEL = "Risk-based sizing (position_sizer)"

LEVERAGE = "1x"

MAX_CONCURRENT_POSITIONS = None # None = unlimited (diagnostic)

CAPITAL_FLOOR = None # e.g. 5000

# ==================================================
# CAPITAL MODEL (REPORT / FUTURE CONTROLS)
# REPORTING & ANALYTICS
# ==================================================

# Position sizing model label for report
POSITION_SIZING_MODEL = "Risk-based sizing (position_sizer)"
# Enable / disable reporting entirely
ENABLE_REPORT = True

# (FUTURE IDEA)Leverage is metadata unless you model margin / leverage constraints
LEVERAGE = "1x"
# ---- STDOUT rendering control ----
class StdoutMode(str, Enum):
OFF = "off"
CONSOLE = "console"
FILE = "file"
BOTH = "both"

REPORT_STDOUT_MODE = StdoutMode.OFF

# (FUTURE IDEA) Concurrency
MAX_CONCURRENT_POSITIONS = None # None => Unlimited (diagnostic)
# Used only if FILE or BOTH
REPORT_STDOUT_FILE = "results/stdout_report.txt"

# (FUTURE IDEA) Kill-switch / capital floor
CAPITAL_FLOOR = None # e.g. 5000, or None
# ---- Dashboard / persistence ----
GENERATE_DASHBOARD = True
PERSIST_REPORT = True

# Fail if no trades (research safety)
REPORT_FAIL_ON_EMPTY = True

# ==================================================
# OUTPUT / UI
# RUNTIME / DEBUG
# ==================================================

SAVE_TRADES_CSV = False
PLOT_ONLY = False
PLOT_ONLY = False # Skip backtest, just plots
SAVE_TRADES_CSV = False # Legacy / debug only

from config.logger_config import LoggerConfig

LOGGER_CONFIG = LoggerConfig(
stdout=True,
file=True,
timing=True,
profiling=False,
log_dir=Path("results/logs"),
)

PROFILING = True

USE_MULTIPROCESSING_STRATEGIES = False
USE_MULTIPROCESSING_BACKTESTS = True

MAX_WORKERS_STRATEGIES = None # None = os.cpu_count()
MAX_WORKERS_BACKTESTS = None
Loading
Loading