ghosalquant 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. ghosalquant/__init__.py +10 -0
  2. ghosalquant/backtest_ui/__init__.py +7 -0
  3. ghosalquant/backtest_ui/__main__.py +10 -0
  4. ghosalquant/backtest_ui/app.py +148 -0
  5. ghosalquant/backtest_ui/runner.py +109 -0
  6. ghosalquant/backtester/__init__.py +7 -0
  7. ghosalquant/backtester/backtester_base.py +129 -0
  8. ghosalquant/backtester/dataframe_backtester.py +95 -0
  9. ghosalquant/bench/__init__.py +7 -0
  10. ghosalquant/bench/benchmark_buy_and_hold.py +51 -0
  11. ghosalquant/bench/benchmark_trend_vol_target.py +179 -0
  12. ghosalquant/brokerage/__init__.py +26 -0
  13. ghosalquant/brokerage/broker_base.py +123 -0
  14. ghosalquant/brokerage/builder.py +42 -0
  15. ghosalquant/brokerage/commissions.py +38 -0
  16. ghosalquant/brokerage/fills.py +470 -0
  17. ghosalquant/brokerage/ibkr.py +496 -0
  18. ghosalquant/brokerage/open_orders.py +27 -0
  19. ghosalquant/brokerage/order_book_sim.py +77 -0
  20. ghosalquant/brokerage/simulated.py +80 -0
  21. ghosalquant/brokerage/simulation.py +303 -0
  22. ghosalquant/compatibility/legacybacktester.py +253 -0
  23. ghosalquant/compatibility/plotManager.py +71 -0
  24. ghosalquant/compatibility/test_strategy.py +63 -0
  25. ghosalquant/compatibility/trade.py +70 -0
  26. ghosalquant/core/__init__.py +59 -0
  27. ghosalquant/core/blackscholes.py +169 -0
  28. ghosalquant/core/contracts.py +155 -0
  29. ghosalquant/core/metadata_keys.py +59 -0
  30. ghosalquant/core/types.py +454 -0
  31. ghosalquant/core/volatility.py +163 -0
  32. ghosalquant/data_layer/__init__.py +29 -0
  33. ghosalquant/data_layer/cboe_datasource.py +150 -0
  34. ghosalquant/data_layer/dataLayer.py +127 -0
  35. ghosalquant/data_layer/dataframe_datasource.py +76 -0
  36. ghosalquant/data_layer/datasource_base.py +18 -0
  37. ghosalquant/data_layer/datasource_factory.py +55 -0
  38. ghosalquant/data_layer/dolt_datasource.py +159 -0
  39. ghosalquant/data_layer/listEditor.py +372 -0
  40. ghosalquant/data_layer/list_datasource.py +27 -0
  41. ghosalquant/data_layer/live_alpaca_datasource.py +190 -0
  42. ghosalquant/data_layer/live_ibkr_datasource.py +246 -0
  43. ghosalquant/data_layer/live_polygon_datasource.py +264 -0
  44. ghosalquant/data_layer/merge.py +52 -0
  45. ghosalquant/data_layer/nse_datasource.py +126 -0
  46. ghosalquant/data_layer/parquet_polygon_datasource.py +108 -0
  47. ghosalquant/data_layer/recording_datasource.py +251 -0
  48. ghosalquant/data_layer/shocks.py +99 -0
  49. ghosalquant/data_layer/staticUpdater.py +408 -0
  50. ghosalquant/data_layer/yfinance_datasource.py +60 -0
  51. ghosalquant/data_layer/yfinance_multi_datasource.py +120 -0
  52. ghosalquant/execution/__init__.py +16 -0
  53. ghosalquant/execution/context.py +54 -0
  54. ghosalquant/execution/execution_system.py +533 -0
  55. ghosalquant/execution/sinks.py +122 -0
  56. ghosalquant/execution/strategy_base.py +109 -0
  57. ghosalquant/metrics/__init__.py +25 -0
  58. ghosalquant/metrics/gates.py +150 -0
  59. ghosalquant/metrics/risk_adjusted.py +196 -0
  60. ghosalquant/portfolio/__init__.py +30 -0
  61. ghosalquant/portfolio/attribution.py +81 -0
  62. ghosalquant/portfolio/basic_portfolio.py +373 -0
  63. ghosalquant/portfolio/hedging.py +87 -0
  64. ghosalquant/portfolio/lots.py +63 -0
  65. ghosalquant/portfolio/marks.py +58 -0
  66. ghosalquant/portfolio/portfolio_base.py +104 -0
  67. ghosalquant/portfolio/valuation.py +694 -0
  68. ghosalquant/recording/__init__.py +111 -0
  69. ghosalquant/recording/events.py +204 -0
  70. ghosalquant/recording/intervals.py +236 -0
  71. ghosalquant/recording/recorder.py +531 -0
  72. ghosalquant/recording/storage.py +607 -0
  73. ghosalquant/risk/__init__.py +56 -0
  74. ghosalquant/risk/builder.py +151 -0
  75. ghosalquant/risk/cash_allocation.py +285 -0
  76. ghosalquant/risk/risk_manager.py +109 -0
  77. ghosalquant/risk/rules/rule_confidence_sizer.py +32 -0
  78. ghosalquant/risk/rules/rule_correlation_filter.py +105 -0
  79. ghosalquant/risk/rules/rule_drawdown_killswitch.py +75 -0
  80. ghosalquant/risk/rules/rule_integer_quantity.py +27 -0
  81. ghosalquant/risk/rules/rule_kelly_sizer.py +74 -0
  82. ghosalquant/risk/rules/rule_max_open_positions.py +27 -0
  83. ghosalquant/risk/rules/rule_max_symbol_exposure.py +118 -0
  84. ghosalquant/risk/rules/rule_regime_filter.py +50 -0
  85. ghosalquant/risk/rules/rule_signal_quantity_cap.py +28 -0
  86. ghosalquant/risk/rules/rule_symbol_capital_sizer.py +78 -0
  87. ghosalquant/risk/rules/rule_tick_rounding.py +40 -0
  88. ghosalquant/risk/sizing.py +46 -0
  89. ghosalquant/risk/types.py +69 -0
  90. ghosalquant/state/__init__.py +23 -0
  91. ghosalquant/state/atomic.py +74 -0
  92. ghosalquant/state/event_store.py +249 -0
  93. ghosalquant/state/json_store.py +85 -0
  94. ghosalquant/util/backtester_utils.py +118 -0
  95. ghosalquant/util/config.py +522 -0
  96. ghosalquant/util/deprecation.py +19 -0
  97. ghosalquant/util/rounding.py +25 -0
  98. ghosalquant/util/visualisation.py +162 -0
  99. ghosalquant-0.1.0.dist-info/METADATA +113 -0
  100. ghosalquant-0.1.0.dist-info/RECORD +104 -0
  101. ghosalquant-0.1.0.dist-info/WHEEL +5 -0
  102. ghosalquant-0.1.0.dist-info/entry_points.txt +2 -0
  103. ghosalquant-0.1.0.dist-info/licenses/LICENSE +21 -0
  104. ghosalquant-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,10 @@
1
+ from ghosalquant.execution.strategy_base import OrderBookStrategy, Strategy
2
+ import ghosalquant.core.types as types
3
+
4
+ __version__ = "0.1.0"
5
+
6
+ __all__ = [
7
+ "OrderBookStrategy",
8
+ "Strategy",
9
+ "types"
10
+ ]
@@ -0,0 +1,7 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def project_root() -> Path:
5
+ """The source checkout when run from one, otherwise the directory the UI was started in."""
6
+ root = Path(__file__).resolve().parents[3]
7
+ return root if (root / "pyproject.toml").exists() else Path.cwd()
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+ import sys
3
+
4
+ from .runner import run_from_config
5
+
6
+ if __name__ == "__main__":
7
+ if len(sys.argv) < 3:
8
+ print("Usage: python -m ghosalquant.backtest_ui <config.yaml> <strategy.py>")
9
+ raise SystemExit(2)
10
+ raise SystemExit(run_from_config(sys.argv[1], sys.argv[2]))
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import threading
5
+ import subprocess
6
+ from pathlib import Path
7
+ from queue import Queue, Empty
8
+
9
+ import customtkinter as ctk
10
+ from tkinter import filedialog, messagebox
11
+
12
+ from . import project_root
13
+
14
+ """
15
+ WARNING: VIBED because I hate UI development and this is a prototype for now
16
+
17
+ This is a rough experimental prototype of a UI to abstract backtesting from QRs.
18
+ """
19
+
20
+
21
+ class App(ctk.CTk):
22
+ def __init__(self) -> None:
23
+ super().__init__()
24
+ self.title("GhosalQuant Backtest (Prototype)")
25
+ self.geometry("900x600")
26
+
27
+ self.root = project_root()
28
+ self.default_cfg = self.root / "examples" / "config.yaml"
29
+
30
+ self.cfg_var = ctk.StringVar(value=str(self.default_cfg) if self.default_cfg.exists() else "")
31
+
32
+ self.log_q: Queue[str] = Queue()
33
+
34
+ top = ctk.CTkFrame(self)
35
+ top.pack(fill="x", padx=12, pady=12)
36
+
37
+ row1 = ctk.CTkFrame(top)
38
+ row1.pack(fill="x")
39
+
40
+ row2 = ctk.CTkFrame(top)
41
+ row2.pack(fill="x")
42
+
43
+ # Config + Run
44
+ ctk.CTkLabel(row1, text="Config:").pack(side="left", padx=(10, 8), pady=10)
45
+ self.cfg_entry = ctk.CTkEntry(row1, textvariable=self.cfg_var, width=560)
46
+ self.cfg_entry.pack(side="left", padx=8, pady=10)
47
+ ctk.CTkButton(row1, text="Browse…", command=self.browse).pack(side="left", padx=8, pady=10)
48
+ self.run_btn = ctk.CTkButton(row1, text="Run", command=self.run)
49
+ self.run_btn.pack(side="left", padx=8, pady=10)
50
+
51
+ # Strategy
52
+ self.strategy_var = ctk.StringVar(value="")
53
+ ctk.CTkLabel(row2, text="Strategy:").pack(side="left", padx=(10, 8), pady=10)
54
+ self.strategy_entry = ctk.CTkEntry(row2, textvariable=self.strategy_var, width=560)
55
+ self.strategy_entry.pack(side="left", padx=8, pady=10)
56
+ ctk.CTkButton(row2, text="Browse…", command=self.browse_strategy).pack(side="left", padx=8, pady=10)
57
+
58
+ self.log = ctk.CTkTextbox(self, wrap="none")
59
+ self.log.pack(fill="both", expand=True, padx=12, pady=(0, 12))
60
+
61
+ self.after(100, self.drain_logs)
62
+
63
+ def browse(self) -> None:
64
+ p = filedialog.askopenfilename(
65
+ title="Select config.yaml",
66
+ filetypes=[("YAML files", "*.yaml *.yml"), ("All files", "*.*")]
67
+ )
68
+ if p:
69
+ self.cfg_var.set(p)
70
+
71
+ def browse_strategy(self) -> None:
72
+ p = filedialog.askopenfilename(
73
+ title="Select strategy .py",
74
+ filetypes=[("Python files", "*.py"), ("All files", "*.*")]
75
+ )
76
+ if p:
77
+ self.strategy_var.set(p)
78
+
79
+ def append(self, line: str) -> None:
80
+ self.log.insert("end", line + "\n")
81
+ self.log.see("end")
82
+
83
+ def drain_logs(self) -> None:
84
+ try:
85
+ while True:
86
+ self.append(self.log_q.get_nowait())
87
+ except Empty:
88
+ pass
89
+ self.after(100, self.drain_logs)
90
+
91
+ def run(self) -> None:
92
+ cfg = Path(self.cfg_var.get().strip())
93
+ if not cfg.exists():
94
+ messagebox.showerror("Missing config", f"Config not found:\n{cfg}")
95
+ return
96
+
97
+ strategy = Path(self.strategy_var.get().strip())
98
+ if not strategy.exists():
99
+ messagebox.showerror("Missing strategy", f"Strategy not found:\n{strategy}")
100
+ self.run_btn.configure(state="normal")
101
+ return
102
+
103
+ self.run_btn.configure(state="disabled")
104
+ self.append(f"Running with config: {cfg}")
105
+
106
+ strategy = Path(self.strategy_var.get().strip()) # add a strategy picker var in UI
107
+ cmd = [sys.executable, "-m", "ghosalquant.backtest_ui", str(cfg), str(strategy)]
108
+
109
+ def worker() -> None:
110
+ try:
111
+ proc = subprocess.Popen(
112
+ cmd,
113
+ cwd=str(self.root),
114
+ stdout=subprocess.PIPE,
115
+ stderr=subprocess.STDOUT,
116
+ text=True,
117
+ )
118
+ assert proc.stdout is not None
119
+ for line in proc.stdout:
120
+ self.log_q.put(line.rstrip("\n"))
121
+ rc = proc.wait()
122
+ self.log_q.put(f"[done] exit code = {rc}")
123
+ except Exception as e:
124
+ self.log_q.put(f"[error] {e}")
125
+ finally:
126
+ self.log_q.put("__enable__")
127
+
128
+ def enable_later() -> None:
129
+ # wait for the marker then re-enable the button in UI thread
130
+ while True:
131
+ msg = self.log_q.get()
132
+ if msg == "__enable__":
133
+ self.run_btn.configure(state="normal")
134
+ break
135
+ else:
136
+ self.append(msg)
137
+
138
+ threading.Thread(target=worker, daemon=True).start()
139
+ threading.Thread(target=enable_later, daemon=True).start()
140
+
141
+
142
+ def main() -> None:
143
+ ctk.set_appearance_mode("System")
144
+ App().mainloop()
145
+
146
+
147
+ if __name__ == "__main__":
148
+ main()
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Any, Type
6
+ import importlib.util
7
+
8
+ import yaml
9
+ import yfinance as yf
10
+ from matplotlib import pyplot as plt
11
+
12
+ from ghosalquant.backtester.dataframe_backtester import DataFrameBacktester
13
+ from ghosalquant.execution.strategy_base import Strategy
14
+ from ghosalquant.util.visualisation import plot_equity_curve
15
+
16
+ from . import project_root
17
+
18
+ def _safe_stem(s: str) -> str:
19
+ # sanitiser
20
+ chars: list[str] = []
21
+ for ch in s:
22
+ if ch.isalnum() or ch in ("-", "_"):
23
+ chars.append(ch)
24
+ else:
25
+ chars.append("_")
26
+
27
+ stem = "".join(chars).strip("_")
28
+ return stem or "run"
29
+
30
+
31
+ def _load_strategy_from_file(path: Path) -> Strategy:
32
+ spec = importlib.util.spec_from_file_location(path.stem, str(path))
33
+ if spec is None or spec.loader is None:
34
+ raise RuntimeError(f"Could not import strategy file: {path}")
35
+
36
+ mod = importlib.util.module_from_spec(spec)
37
+ spec.loader.exec_module(mod) # type: ignore[assignment]
38
+
39
+ # Explicit export as STRATEGY_CLASS
40
+ if hasattr(mod, "STRATEGY_CLASS"):
41
+ cls = getattr(mod, "STRATEGY_CLASS")
42
+ if isinstance(cls, type) and issubclass(cls, Strategy):
43
+ return cls()
44
+ raise TypeError("STRATEGY_CLASS exists but is not a Strategy subclass.")
45
+
46
+ # As a fallback, find first Strategy subclass in module
47
+ for obj in mod.__dict__.values():
48
+ if isinstance(obj, type) and issubclass(obj, Strategy) and obj is not Strategy:
49
+ return obj()
50
+
51
+ raise ValueError(
52
+ "No strategy found. Define STRATEGY_CLASS = YourStrategy, "
53
+ "or include a Strategy subclass in the file."
54
+ )
55
+
56
+
57
+ def run_from_config(config_path: str | Path, strategy_path: str | Path) -> int:
58
+ cfg_path = Path(config_path).resolve()
59
+ strat_path = Path(strategy_path).resolve()
60
+
61
+ if not cfg_path.exists():
62
+ print(f"Config not found: {cfg_path}")
63
+ return 2
64
+ if not strat_path.exists():
65
+ print(f"Strategy not found: {strat_path}")
66
+ return 2
67
+
68
+ cfg: dict[str, Any] = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
69
+ system = cfg.get("SYSTEM", {}) or {}
70
+ tr = cfg.get("TIME_RANGE", {}) or {}
71
+
72
+ symbols = system.get("symbols", ["AAPL"]) or ["AAPL"]
73
+ symbol = symbols[0] # prototype: single symbol
74
+
75
+ start_date = tr.get("start_date", "2024-01-01")
76
+ end_date = tr.get("end_date", "2025-12-31")
77
+
78
+ print("UI Runner: DataFrameBacktester")
79
+ print(f"Config: {cfg_path}")
80
+ print(f"Strategy: {strat_path}")
81
+ print(f"Symbol: {symbol}")
82
+ print(f"Range: {start_date} -> {end_date}")
83
+
84
+ df = yf.download(symbol, start=start_date, end=end_date, auto_adjust=True, progress=False)
85
+
86
+ strategy = _load_strategy_from_file(strat_path)
87
+ backtester = DataFrameBacktester(strategy=strategy, dataframe=df, config_path=str(cfg_path))
88
+
89
+ kpms = backtester.run()
90
+ print("\nKEY PERFORMANCE METRICS:\n" + str(kpms))
91
+ print("REALISED PNL:", backtester.realised_pnl)
92
+
93
+ # Plot and save to file
94
+ plot_equity_curve(backtester.equity_curve, show_realised_unrealised=True)
95
+
96
+ out_dir = project_root() / "ui_outputs"
97
+ out_dir.mkdir(parents=True, exist_ok=True)
98
+
99
+ strategy_name = _safe_stem(strat_path.stem)
100
+ symbol_name = _safe_stem(symbol)
101
+ now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
102
+ out_path = out_dir / f"equity_curve_{now}_{symbol_name}_{strategy_name}.png"
103
+
104
+ plt.savefig(out_path, dpi=150, bbox_inches="tight")
105
+ plt.close()
106
+
107
+ print(f"Saved plot: {out_path}")
108
+
109
+ return 0
@@ -0,0 +1,7 @@
1
+ from .backtester_base import Backtester
2
+ from .dataframe_backtester import DataFrameBacktester
3
+
4
+ __all__ = [
5
+ "Backtester",
6
+ "DataFrameBacktester",
7
+ ]
@@ -0,0 +1,129 @@
1
+ import asyncio
2
+ from typing import final, Iterable
3
+
4
+ from ..data_layer.datasource_factory import datasource_from_config
5
+ from ..brokerage.builder import build_broker
6
+ from ..execution.strategy_base import Strategy
7
+ from ..execution.execution_system import ExecutionSystem
8
+ from ..data_layer.datasource_base import DataSource
9
+ from ..execution.sinks import SignalSink
10
+ from ..core.contracts import ContractRegistry
11
+ from ..core.types import Order, BacktestKPMs, EquityPoint
12
+ from ..portfolio.basic_portfolio import BasicPortfolio
13
+ from ..portfolio.portfolio_base import Portfolio
14
+ from ..brokerage.broker_base import Broker
15
+ from ..risk.builder import build_risk_manager
16
+ from ..risk.risk_manager import RiskManager
17
+ from ..util.backtester_utils import calculate_kpms
18
+ from ..util.config import Config, load_config
19
+
20
+
21
+ class Backtester:
22
+ def __init__(
23
+ self,
24
+ *,
25
+ strategy: Strategy,
26
+ data_src: DataSource,
27
+ signal_destination: SignalSink | None = None,
28
+ portfolio: Portfolio | None = None,
29
+ risk_manager: RiskManager | None = None,
30
+ broker: Broker | None = None,
31
+ config: Config | None = None,
32
+ contracts: ContractRegistry | None = None,
33
+ ) -> None:
34
+ self.config = config
35
+
36
+ if portfolio is None:
37
+ portfolio = BasicPortfolio(init_capital=10_000.0, contracts=contracts)
38
+
39
+ if risk_manager is None and config is not None and config.system.use_risk_manager:
40
+ risk_manager = build_risk_manager(config.risk)
41
+
42
+ self.execution_system = ExecutionSystem(
43
+ data_src = data_src,
44
+ strategy = strategy,
45
+ signal_destination = signal_destination,
46
+ risk_manager = risk_manager,
47
+ portfolio = portfolio,
48
+ broker = broker,
49
+ contracts = contracts,
50
+ )
51
+
52
+ self._portfolio = self.execution_system.portfolio
53
+
54
+ @classmethod
55
+ def from_config(cls,
56
+ *,
57
+ strategy: Strategy,
58
+ config_path: str,
59
+
60
+ # Optional overrides
61
+ signal_destination: SignalSink | None = None,
62
+ portfolio: Portfolio | None = None,
63
+ risk_manager: RiskManager | None = None,
64
+ init_capital: float = 10_000.0,
65
+ contracts: ContractRegistry | None = None,
66
+ ) -> "Backtester":
67
+
68
+ cfg = load_config(config_path)
69
+ data_src = datasource_from_config(cfg, contracts=contracts)
70
+
71
+ if portfolio is None:
72
+ portfolio = BasicPortfolio(init_capital=float(init_capital), contracts=contracts)
73
+
74
+ if risk_manager is None and cfg.system.use_risk_manager:
75
+ risk_manager = build_risk_manager(cfg.risk)
76
+
77
+ return cls(
78
+ strategy=strategy,
79
+ data_src=data_src,
80
+ signal_destination=signal_destination,
81
+ portfolio=portfolio,
82
+ risk_manager=risk_manager,
83
+ broker=build_broker(cfg.broker, contracts=contracts),
84
+ config=cfg,
85
+ contracts=contracts,
86
+ )
87
+
88
+ # ----- Properties ----- #
89
+ @property
90
+ def equity_curve(self) -> Iterable[EquityPoint]:
91
+ return self._portfolio.equity_curve
92
+
93
+ @property
94
+ def realised_pnl(self) -> float:
95
+ return self._portfolio.realised_pnl
96
+
97
+ @property
98
+ def unrealised_pnl(self) -> float:
99
+ return self._portfolio.unrealised_pnl
100
+
101
+ @property
102
+ def total_pnl(self) -> float:
103
+ return self.realised_pnl + self.unrealised_pnl
104
+
105
+ @property
106
+ def kpms(self) -> BacktestKPMs:
107
+ if not self._portfolio:
108
+ raise ValueError("Cannot produce KPMs without an initialised Portfolio")
109
+
110
+ """Calculate and return key performance metrics after completing a backtest."""
111
+ return calculate_kpms(self._portfolio)
112
+
113
+ @final
114
+ async def run_async(self) -> BacktestKPMs:
115
+ await self.execution_system.run()
116
+
117
+ if not self._portfolio:
118
+ raise ValueError("Cannot produce KPMs without an initialised Portfolio")
119
+
120
+ return calculate_kpms(self._portfolio)
121
+
122
+ @final
123
+ def run(self) -> BacktestKPMs:
124
+ try:
125
+ asyncio.get_running_loop()
126
+ except RuntimeError:
127
+ return asyncio.run(self.run_async())
128
+
129
+ raise RuntimeError("Cannot run backtest synchronously within an existing event loop. Please use run_async() instead.")
@@ -0,0 +1,95 @@
1
+ import asyncio
2
+ from typing import Iterable
3
+
4
+ from pandas import DataFrame
5
+ import pandas as pd
6
+
7
+ from ..backtester.backtester_base import Backtester
8
+ from ..brokerage.builder import build_broker
9
+ from ..core.types import Order, EquityPoint, BacktestKPMs
10
+ from ..execution.strategy_base import Strategy
11
+ from ..data_layer.dataframe_datasource import DataFrameDataSource
12
+ from ..execution.sinks import DataFrameSink
13
+ from ..portfolio.basic_portfolio import BasicPortfolio
14
+ from ..util.backtester_utils import output_to_excel, calculate_kpms
15
+ from ..util.config import load_config
16
+
17
+ class DataFrameBacktester(Backtester):
18
+ def __init__(
19
+ self,
20
+ strategy: Strategy,
21
+ dataframe: DataFrame,
22
+ config_path: str,
23
+
24
+ timestamp_col: str | None = None,
25
+ # DataFrame format:
26
+ *,
27
+ symbol: str | None = None,
28
+ open_col: str = "Open",
29
+ high_col: str = "High",
30
+ low_col: str = "Low",
31
+ close_col: str = "Close",
32
+ volume_col: str = "Volume",
33
+
34
+ # Additional Columns
35
+ signal_col: str = "Signal",
36
+ confidence_col: str = "Confidence",
37
+ quantity_col: str = "Quantity",
38
+
39
+ # Backtesting
40
+ init_capital: float = 10_000.0
41
+ ) -> None:
42
+ config = load_config(config_path)
43
+
44
+ df = dataframe.copy()
45
+
46
+ if isinstance(df.columns, pd.MultiIndex): # Flatten MultiIndex if present
47
+ required = {open_col, high_col, low_col, close_col, volume_col}
48
+
49
+ levels = [set(map(str, df.columns.get_level_values(i))) for i in range(df.columns.nlevels)]
50
+ level_with_required = None
51
+ for i, vals in enumerate(levels):
52
+ if required.issubset(vals):
53
+ level_with_required = i
54
+ break
55
+
56
+ if level_with_required is not None:
57
+ df.columns = df.columns.get_level_values(level_with_required)
58
+
59
+ # Duplicate the initial DataFrame and initialise signal and confidence columns
60
+ self._return_df = df
61
+ self._return_df[signal_col] = pd.NA
62
+ self._return_df[confidence_col] = 0.0
63
+ self._return_df[quantity_col] = 0.0
64
+
65
+ # Backtesting variables
66
+ self._symbols: list[str] = config.system.symbols
67
+ self._portfolio = BasicPortfolio(init_capital=init_capital)
68
+
69
+ super().__init__(
70
+ strategy=strategy,
71
+ data_src=DataFrameDataSource(
72
+ dataframe=df,
73
+ symbol= config.system.symbols[0],
74
+ timestamp_col= timestamp_col,
75
+ open_col= open_col,
76
+ high_col= high_col,
77
+ low_col= low_col,
78
+ close_col= close_col,
79
+ volume_col= volume_col
80
+ ),
81
+ signal_destination=DataFrameSink(
82
+ dataframe= self._return_df,
83
+ timestamp_col= timestamp_col,
84
+ signal_col= signal_col,
85
+ confidence_col= confidence_col,
86
+ quantity_col= quantity_col
87
+ ),
88
+ portfolio= self._portfolio,
89
+ broker=build_broker(config.broker),
90
+ config= config,
91
+ )
92
+
93
+ @property
94
+ def dataframe(self) -> DataFrame:
95
+ return self._return_df
@@ -0,0 +1,7 @@
1
+ from .benchmark_buy_and_hold import BenchmarkBuyAndHold
2
+ from .benchmark_trend_vol_target import BenchmarkTrendVolTarget
3
+
4
+ __all__ = [
5
+ "BenchmarkBuyAndHold",
6
+ "BenchmarkTrendVolTarget",
7
+ ]
@@ -0,0 +1,51 @@
1
+ from ..execution.strategy_base import Strategy
2
+ from ..core.types import Bar, Signal, SignalType
3
+
4
+ class BenchmarkBuyAndHold(Strategy):
5
+ """
6
+ Buy and hold benchmark strategy.
7
+ - On a bar received with an unseen symbol, sends a BUY signal.
8
+ - After entering, emits no further signals for that symbol
9
+ """
10
+
11
+ _USE_CATASTROPHIC_SL: bool = True
12
+ _CATASTROPHIC_SL_PERCENT: float = 0.35 # 35% below entry in case of extreme drawdown
13
+
14
+ def __init__(self):
15
+ super().__init__()
16
+ self._entered: dict[str, bool] = {}
17
+ self._entry_price: dict[str, float] = {}
18
+
19
+ def on_start(self):
20
+ ...
21
+
22
+ def on_end(self):
23
+ ...
24
+
25
+ def on_market_data(self, bar: Bar) -> Signal | None:
26
+ symbol = bar.symbol
27
+
28
+ if not self._entered.get(symbol, False):
29
+ self._entered[symbol] = True
30
+ self._entry_price[symbol] = float(bar.close)
31
+
32
+ confidence = 1.0
33
+ quantity = super().calculate_quantity(confidence=confidence)
34
+
35
+ metadata: dict[str, float] = {}
36
+
37
+ if self._USE_CATASTROPHIC_SL:
38
+ sl = bar.close * (1.0 - self._CATASTROPHIC_SL_PERCENT)
39
+ metadata["stop_loss"] = sl
40
+
41
+ return Signal(
42
+ time_stamp=bar.time_stamp,
43
+ symbol=symbol,
44
+ signal_type=SignalType.BUY,
45
+ ref_price=bar.close,
46
+ confidence=confidence,
47
+ quantity=quantity,
48
+ metadata=metadata if metadata else None,
49
+ )
50
+
51
+ return None