py-backtesting-lib 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 (42) hide show
  1. Backtesting/__init__.py +0 -0
  2. Backtesting/analysis/__init__.py +11 -0
  3. Backtesting/analysis/persistence/__init__.py +5 -0
  4. Backtesting/analysis/persistence/event_sink.py +133 -0
  5. Backtesting/analysis/reports/__init__.py +5 -0
  6. Backtesting/analysis/reports/console_report.py +73 -0
  7. Backtesting/analysis/result.py +42 -0
  8. Backtesting/definition/__init__.py +7 -0
  9. Backtesting/definition/configs/__init__.py +8 -0
  10. Backtesting/definition/configs/base.py +31 -0
  11. Backtesting/definition/core/__init__.py +22 -0
  12. Backtesting/definition/core/domain_models.py +165 -0
  13. Backtesting/definition/experiment.py +142 -0
  14. Backtesting/definition/plugins/__init__.py +3 -0
  15. Backtesting/definition/plugins/base_interfaces.py +126 -0
  16. Backtesting/definition/plugins/plugin_registry.py +26 -0
  17. Backtesting/execution/__init__.py +22 -0
  18. Backtesting/execution/books/__init__.py +11 -0
  19. Backtesting/execution/books/order_book.py +130 -0
  20. Backtesting/execution/books/position_book.py +136 -0
  21. Backtesting/execution/books/trade_book.py +88 -0
  22. Backtesting/execution/engine.py +57 -0
  23. Backtesting/execution/event.py +44 -0
  24. Backtesting/execution/market_data.py +108 -0
  25. Backtesting/execution/pipeline.py +105 -0
  26. Backtesting/execution/portfolio.py +72 -0
  27. Backtesting/experiments/base.yaml +32 -0
  28. Backtesting/main.py +148 -0
  29. Backtesting/plugins/__init__.py +13 -0
  30. Backtesting/plugins/brokers/__init__.py +3 -0
  31. Backtesting/plugins/brokers/instant_market_fill.py +26 -0
  32. Backtesting/plugins/market_data/__init__.py +3 -0
  33. Backtesting/plugins/market_data/dummy_data_provider.py +32 -0
  34. Backtesting/plugins/money_managers/__init__.py +3 -0
  35. Backtesting/plugins/money_managers/fixed_quality.py +36 -0
  36. Backtesting/plugins/strategies/SMA_crossovr.py +52 -0
  37. Backtesting/plugins/strategies/__init__.py +3 -0
  38. py_backtesting_lib-0.1.0.dist-info/METADATA +281 -0
  39. py_backtesting_lib-0.1.0.dist-info/RECORD +42 -0
  40. py_backtesting_lib-0.1.0.dist-info/WHEEL +4 -0
  41. py_backtesting_lib-0.1.0.dist-info/entry_points.txt +2 -0
  42. py_backtesting_lib-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,108 @@
1
+ # Backtesting/execution/market_data.py
2
+ # Market data providers for the execution engine
3
+
4
+ import csv
5
+ from pathlib import Path
6
+ from typing import Iterator, List
7
+ from decimal import Decimal
8
+ from datetime import datetime
9
+ from ..definition import MarketBar, MarketDataProvider
10
+
11
+
12
+ class CSVMarketDataProvider(MarketDataProvider):
13
+ """
14
+ Provides market data from a local CSV file.
15
+ Assumes columns: timestamp, open, high, low, close, volume
16
+ """
17
+ def __init__(
18
+ self,
19
+ csv_file_path: str,
20
+ symbol: str,
21
+ timestamp_col: str = "timestamp",
22
+ open_col: str = "open",
23
+ high_col: str = "high",
24
+ low_col: str = "low",
25
+ close_col: str = "close",
26
+ volume_col: str = "volume",
27
+ ):
28
+ self.path = Path(csv_file_path)
29
+ if not self.path.exists():
30
+ raise FileNotFoundError(f"Market data CSV file not found: {csv_file_path}")
31
+
32
+ self.symbol = symbol
33
+ self.ts_col = timestamp_col
34
+ self.o_col = open_col
35
+ self.h_col = high_col
36
+ self.l_col = low_col
37
+ self.c_col = close_col
38
+ self.v_col = volume_col
39
+
40
+ self.rows = []
41
+ with self.path.open(newline="", encoding="utf-8") as f:
42
+ reader = csv.DictReader(f)
43
+ required_cols = {self.ts_col, self.o_col, self.h_col, self.l_col, self.c_col, self.v_col}
44
+ missing = required_cols - set(reader.fieldnames or [])
45
+ if missing:
46
+ raise ValueError(
47
+ f"CSV file {csv_file_path} is missing required columns: {missing}"
48
+ )
49
+
50
+ for line_num, row in enumerate(reader, start=2):
51
+ try:
52
+ timestamp = datetime.fromisoformat(row[self.ts_col].strip())
53
+ except Exception as e:
54
+ raise ValueError(
55
+ f"Could not parse timestamp column '{self.ts_col}' in {csv_file_path} "
56
+ f"on line {line_num}: {e}"
57
+ )
58
+
59
+ self.rows.append({
60
+ "timestamp": timestamp,
61
+ "open": Decimal(str(row[self.o_col]).strip()),
62
+ "high": Decimal(str(row[self.h_col]).strip()),
63
+ "low": Decimal(str(row[self.l_col]).strip()),
64
+ "close": Decimal(str(row[self.c_col]).strip()),
65
+ "volume": Decimal(str(row[self.v_col]).strip()),
66
+ })
67
+
68
+ self.rows.sort(key=lambda x: x["timestamp"])
69
+
70
+ def get_bars(self) -> Iterator[MarketBar]:
71
+ """
72
+ Yields MarketBar objects from the loaded CSV data.
73
+ """
74
+ for row in self.rows:
75
+ yield MarketBar(
76
+ symbol=self.symbol,
77
+ timestamp=row["timestamp"],
78
+ open=row["open"],
79
+ high=row["high"],
80
+ low=row["low"],
81
+ close=row["close"],
82
+ volume=row["volume"],
83
+ )
84
+
85
+
86
+ def create_dummy_market_data(num_bars: int, start_price: Decimal = Decimal("1.0000")) -> List[MarketBar]:
87
+ """
88
+ Creates a simple sequence of dummy market bars for testing.
89
+ Price increases by 0.0001 per bar.
90
+ """
91
+ bars = []
92
+ current_price = start_price
93
+
94
+ for i in range(num_bars):
95
+ timestamp = datetime(2022, 1, 1, i)
96
+ bar = MarketBar(
97
+ symbol="DUMMY",
98
+ timestamp=timestamp,
99
+ open=current_price,
100
+ high=current_price + Decimal("0.0001"),
101
+ low=current_price - Decimal("0.0001"),
102
+ close=current_price + Decimal("0.0001") * (i % 2),
103
+ volume=Decimal("1000"),
104
+ )
105
+ bars.append(bar)
106
+ current_price += Decimal("0.0001")
107
+
108
+ return bars
@@ -0,0 +1,105 @@
1
+ # Backtesting/execution/pipeline.py
2
+ # Coordinates the flow: Strategy -> Signal -> MM -> Order -> Broker -> Fill
3
+
4
+ from decimal import Decimal
5
+ from ..definition import EventType, SignalGenerated, OrderSubmitted, OrderFilled, OrderCancelled, FillExecuted, MarketBarReceived # Import specific events if needed
6
+ from Backtesting.execution.event import EventBus
7
+ from .books.order_book import OrderBook
8
+ from .books.position_book import PositionBook
9
+ from .books.trade_book import TradeBook
10
+ from .portfolio import Portfolio
11
+ from ..definition.core.domain_models import Fill, MarketBar # Import core models from definition
12
+ from ..definition.plugins import Strategy, MoneyManager, Broker # Import plugin interfaces
13
+
14
+
15
+ class ExecutionPipeline:
16
+ """
17
+ Orchestrates the core trading logic pipeline: Market Data -> Strategy -> Signal -> MM -> Order -> Broker -> Fill.
18
+ Integrates with Books, Portfolio, and EventBus.
19
+ """
20
+ def __init__(self, strategy: Strategy, money_manager: MoneyManager, broker: Broker,
21
+ order_book: OrderBook, position_book: PositionBook, trade_book: TradeBook,
22
+ portfolio: Portfolio, event_bus: EventBus):
23
+ self.strategy = strategy
24
+ self.money_manager = money_manager
25
+ self.broker = broker
26
+ self.order_book = order_book
27
+ self.position_book = position_book
28
+ self.trade_book = trade_book
29
+ self.portfolio = portfolio
30
+ self.event_bus = event_bus
31
+
32
+ def process_bar(self, bar: MarketBar):
33
+ """
34
+ Processes a single market bar through the entire pipeline.
35
+ """
36
+ # 1. Mark-to-market positions based on the new bar's price BEFORE generating signals
37
+ self.portfolio.update_market_price(bar, self.position_book)
38
+ self.position_book.update_market_price(bar)
39
+
40
+ # 2. Strategy generates signals based on the bar
41
+ signals = self.strategy.on_data(bar)
42
+
43
+ for signal in signals:
44
+ # 3. Emit SignalGenerated event
45
+ signal_event = SignalGenerated(signal=signal)
46
+ self.event_bus.publish(signal_event)
47
+
48
+ # 4. Money Manager sizes the signal into an order
49
+ # Pass current portfolio state if needed by MM
50
+ portfolio_state = {"cash": self.portfolio.cash} # Simplified state
51
+ orders = self.money_manager.size_order(signal, portfolio_state)
52
+
53
+ for order in orders:
54
+ # 5. Add order to OrderBook
55
+ self.order_book.add_order(order)
56
+
57
+ # 6. Emit OrderSubmitted event
58
+ order_event = OrderSubmitted(order=order)
59
+ self.event_bus.publish(order_event)
60
+
61
+ # 7. Broker simulates the order execution against the current bar
62
+ fills = self.broker.process_order(order, bar)
63
+
64
+ for fill in fills:
65
+ # 8. Apply fill to books and portfolio
66
+ self.apply_fill(fill)
67
+
68
+ def apply_fill(self, fill: Fill):
69
+ order = self.order_book.get_order_by_event(fill.order_id)
70
+ if not order:
71
+ print(f"Warning: Fill {fill.fill_id} references unknown order {fill.order_id}. Cannot archive trade.")
72
+ order = None
73
+
74
+ # Get position state *before* applying fill
75
+ position_before = Decimal('0')
76
+ existing_pos = self.position_book.get_position(fill.symbol)
77
+ if existing_pos:
78
+ position_before = existing_pos.net_size
79
+
80
+ # ✅ CAPTURE ENTRY PRICE BEFORE MUTATION
81
+ entry_price = existing_pos.avg_entry_price if existing_pos else Decimal('0')
82
+
83
+ # Apply fill to OrderBook
84
+ self.order_book.apply_fill(fill)
85
+
86
+ # Apply fill to PositionBook (this mutates the position, setting avg_entry_price to 0 if closed)
87
+ self.position_book.apply_fill(fill)
88
+
89
+ # Get position state *after* applying fill
90
+ position_after = Decimal('0')
91
+ updated_pos = self.position_book.get_position(fill.symbol)
92
+ if updated_pos:
93
+ position_after = updated_pos.net_size
94
+
95
+ # Apply fill to Portfolio
96
+ self.portfolio.apply_fill(fill)
97
+
98
+ # Archive trade if position closed (crossed zero)
99
+ if order and (position_before > 0 and position_after <= 0) or (position_before < 0 and position_after >= 0):
100
+ # Use the captured entry_price (not mutated)
101
+ self.trade_book.apply_fill(fill, order, position_before, position_after, entry_price)
102
+
103
+ # Emit FillExecuted event
104
+ fill_event = FillExecuted(fill=fill)
105
+ self.event_bus.publish(fill_event)
@@ -0,0 +1,72 @@
1
+
2
+ from decimal import Decimal
3
+ from .books import PositionBook
4
+ from ..definition import Fill as EventFill, MarketBar as EventMarketBar, OrderSide
5
+
6
+ class Portfolio:
7
+ """
8
+ Tracks the account's financial state: cash, equity, margin usage.
9
+ Equity is a derived value: cash + sum(unrealized_pnl of all positions).
10
+ State is updated only via the apply_* methods, driven by events from the EventBus.
11
+ """
12
+ def __init__(self, initial_cash: Decimal):
13
+ self._initial_cash = initial_cash
14
+ self._cash = initial_cash
15
+ self._margin_used = Decimal('0') # Placeholder for future margin calculation logic
16
+
17
+ def apply_fill(self, fill: EventFill):
18
+ """
19
+ Updates the cash balance based on a Fill event (cost of purchase or proceeds from sale).
20
+ Commission is subtracted from cash.
21
+ """
22
+ # Calculate the cost/proceeds of the fill
23
+ fill_value = fill.quantity * fill.fill_price
24
+ if fill.side == OrderSide.BUY:
25
+ # Buying reduces cash: fill cost + commission
26
+ self._cash -= fill_value + fill.commission
27
+ print(f"DEBUG: Portfolio cash decreased by {fill_value + fill.commission} (value {fill_value} + commission {fill.commission}) due to buy fill. New cash: {self._cash}")
28
+ elif fill.side == OrderSide.SELL:
29
+ # Selling increases cash: fill proceeds - commission
30
+ self._cash += fill_value - fill.commission
31
+ print(f"DEBUG: Portfolio cash increased by {fill_value - fill.commission} (value {fill_value} - commission {fill.commission}) due to sell fill. New cash: {self._cash}")
32
+
33
+ def update_market_price(self, bar: EventMarketBar, position_book: PositionBook):
34
+ """
35
+ Updates the total equity by marking positions to market.
36
+ Does not directly change cash.
37
+ Requires the PositionBook to get unrealized PnL.
38
+ """
39
+ # Equity = Cash + Unrealized PnL from all positions
40
+ # The cash component remains unchanged here.
41
+ # The unrealized PnL is updated within the PositionBook itself via its update_market_price method.
42
+ # Therefore, calculating equity involves summing the UPLs from the PositionBook.
43
+ unrealized_pnl_total = Decimal('0')
44
+ for position in position_book.get_all_positions().values():
45
+ unrealized_pnl_total += position.unrealized_pnl
46
+
47
+ equity = self._cash + unrealized_pnl_total
48
+ print(f"DEBUG: Portfolio equity updated to {equity} (Cash: {self._cash}, UPL: {unrealized_pnl_total})")
49
+
50
+ @property
51
+ def cash(self) -> Decimal:
52
+ """Read-only property for the current cash balance."""
53
+ return self._cash
54
+
55
+ def get_equity(self, position_book: PositionBook) -> Decimal:
56
+ """
57
+ Calculates and returns the total equity.
58
+ Equity = Cash + Unrealized PnL from all open positions.
59
+ Requires the PositionBook to get unrealized PnL.
60
+ """
61
+ unrealized_pnl_total = Decimal('0')
62
+ for position in position_book.get_all_positions().values():
63
+ unrealized_pnl_total += position.unrealized_pnl
64
+ return self._cash + unrealized_pnl_total
65
+
66
+ def get_margin_usage(self) -> Decimal:
67
+ """Placeholder for margin calculation."""
68
+ return self._margin_used
69
+
70
+ def get_initial_cash(self) -> Decimal:
71
+ """Returns the initial cash balance."""
72
+ return self._initial_cash
@@ -0,0 +1,32 @@
1
+ # experiments/beta_test.yaml
2
+ experiment:
3
+ name: "Beta Test - SMA Crossover Synthetic"
4
+ metadata:
5
+ author: "Zhiro"
6
+ version: "1.0"
7
+
8
+ configs:
9
+ strategy:
10
+ name: "SMA_Crossover_Strategy"
11
+ params:
12
+ fast_period: 5
13
+ slow_period: 10
14
+ symbol: "DUMMY"
15
+
16
+ money_management:
17
+ name: "Fixed_Qty_MoneyManager"
18
+ params:
19
+ trade_quantity: 10.0
20
+
21
+ broker:
22
+ name: "Simple_Market_Broker"
23
+ params:
24
+ commission_per_trade: 1.50
25
+ initial_capital: 100000
26
+
27
+ market_data:
28
+ name: "Dummy_Market_Data"
29
+ params:
30
+ num_bars: 300
31
+ symbol: "DUMMY"
32
+ start_price: 100.0
Backtesting/main.py ADDED
@@ -0,0 +1,148 @@
1
+
2
+ import typer
3
+ import uuid
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+ from decimal import Decimal
7
+ from .definition import Experiment
8
+ from .definition.plugins.plugin_registry import resolve, PluginNotFoundError
9
+ from .execution.engine import ExecutionEngine
10
+ from .execution.market_data import CSVMarketDataProvider
11
+ from .analysis.result import ExperimentResult
12
+ from .analysis.reports.console_report import ConsoleTradeReport
13
+ from .analysis.persistence.event_sink import DuckDBEventSink
14
+ from .definition import Trade
15
+ from . import plugins # it will initialize default plugins
16
+
17
+ app = typer.Typer()
18
+
19
+ @app.command()
20
+ def run(config_file: str = typer.Option(..., "--config", "-c", help="Path to the experiment YAML configuration file.")):
21
+ """
22
+ Runs a backtesting experiment defined by the given YAML file.
23
+ """
24
+ config_path = Path(config_file)
25
+ if not config_path.exists():
26
+ typer.echo(f"Error: Configuration file not found: {config_file}", err=True)
27
+ raise typer.Exit(code=1)
28
+
29
+ try:
30
+ # 1. Load and validate the experiment definition
31
+ experiment_def = Experiment.from_yaml(config_file)
32
+ experiment_def.validate()
33
+ print(f"Loaded experiment: {experiment_def.name}")
34
+
35
+ # 2. Resolve plugin classes
36
+ strategy_cls = resolve(experiment_def.strategy_config.name)
37
+ mm_cls = resolve(experiment_def.money_management_config.name)
38
+ broker_cls = resolve(experiment_def.broker_config.name)
39
+ market_data_cls = resolve(experiment_def.market_data_config.name)
40
+
41
+ strategy_params = experiment_def.strategy_config.params or {}
42
+ mm_params = experiment_def.money_management_config.params or {}
43
+ broker_params = experiment_def.broker_config.params or {}
44
+ market_data_params = experiment_def.market_data_config.params or {}
45
+
46
+ # Instantiate plugins
47
+ strategy = strategy_cls()
48
+ money_manager = mm_cls()
49
+ broker = broker_cls()
50
+
51
+ market_data_provider = market_data_cls(**market_data_params)
52
+
53
+ # Initialize plugins with their guaranteed-dict configs
54
+ strategy.initialize(strategy_params)
55
+ money_manager.initialize(mm_params)
56
+ broker.initialize(broker_params)
57
+
58
+ # 3. Extract initial capital safely using the guaranteed dict
59
+ initial_capital = Decimal(str(broker_params.get('initial_capital', 100000)))
60
+
61
+ # 4. Create unique run ID and start timestamp
62
+ run_id = str(uuid.uuid4())
63
+ start_time = datetime.now()
64
+
65
+ # 5. Initialize Event Sink and subscribe to the engine's event bus
66
+ event_sink = DuckDBEventSink()
67
+
68
+ engine = ExecutionEngine(
69
+ strategy=strategy,
70
+ money_manager=money_manager,
71
+ broker=broker,
72
+ initial_cash=initial_capital,
73
+ market_data_provider=market_data_provider
74
+ )
75
+ event_sink.subscribe_to_engine(engine.event_bus, run_id) # Pass the bus from the engine instance
76
+
77
+ # 6. Run the engine
78
+ print("Starting backtest...")
79
+ engine.run()
80
+ end_time = datetime.now()
81
+ print("Backtest completed.")
82
+
83
+ # 7. Construct the ExperimentResult aggregate
84
+ # Final state snapshots
85
+ portfolio_snap = {
86
+ "cash": engine.portfolio.cash,
87
+ "equity": engine.portfolio.get_equity(engine.position_book),
88
+ "margin_used": engine.portfolio.get_margin_usage(),
89
+ "initial_cash": engine.portfolio.get_initial_cash()
90
+ }
91
+
92
+ pos_snap = {}
93
+ for symbol, pos in engine.position_book.get_all_positions().items():
94
+ pos_snap[symbol] = {
95
+ "net_size": pos.net_size,
96
+ "avg_entry_price": pos.avg_entry_price,
97
+ "unrealized_pnl": pos.unrealized_pnl
98
+ }
99
+
100
+ trade_snap = [trade.__dict__ for trade in engine.trade_book.get_closed_trades()] # Serialize Trade objects
101
+
102
+ event_log_query_result = event_sink.query_events(experiment_id=run_id).fetchall()
103
+
104
+ event_log_serialized = [
105
+ {"id": row[0], "experiment_id": row[1], "timestamp": row[2], "type": row[3], "data": row[4]}
106
+ for row in event_log_query_result
107
+ ]
108
+
109
+
110
+ result = ExperimentResult(
111
+ experiment_id=run_id,
112
+ experiment_name=experiment_def.name,
113
+ started_at=start_time,
114
+ finished_at=end_time,
115
+ portfolio_snapshot=portfolio_snap,
116
+ position_book_snapshot=pos_snap,
117
+ trade_book_snapshot=trade_snap,
118
+ event_log=event_log_serialized # Note: This fetches from DB, which might be redundant if DB is primary output
119
+ )
120
+
121
+ # 8. Generate and print the report
122
+ report = ConsoleTradeReport()
123
+ report.generate_report(result)
124
+
125
+ # 9. Close resources
126
+ event_sink.close_connection()
127
+
128
+ print(f"\nExperiment '{experiment_def.name}' (ID: {run_id}) completed successfully.")
129
+ print(f"Results summary printed to console.")
130
+ print(f"Full event log persisted to database.")
131
+
132
+ except FileNotFoundError as e:
133
+ typer.echo(f"Error: {e}", err=True)
134
+ raise typer.Exit(code=1)
135
+ except ValueError as e: # Catch errors from CSV provider, YAML parsing, etc.
136
+ typer.echo(f"Configuration Error: {e}", err=True)
137
+ raise typer.Exit(code=1)
138
+ except PluginNotFoundError as e:
139
+ typer.echo(f"Plugin Error: {e}", err=True)
140
+ raise typer.Exit(code=1)
141
+ except Exception as e:
142
+ typer.echo(f"An unexpected error occurred during execution: {e}", err=True)
143
+ typer.echo("Check logs for details.", err=True)
144
+ raise typer.Exit(code=1) # Propagate to trigger fail-fast behavior
145
+
146
+
147
+ if __name__ == "__main__":
148
+ app()
@@ -0,0 +1,13 @@
1
+ # plugins/__init__.py
2
+ from Backtesting.definition.plugins.plugin_registry import register
3
+
4
+ from .strategies import SMACrossoverStrategy
5
+ from .money_managers import FixedQtyMoneyManager
6
+ from .brokers import SimpleMarketBroker
7
+ from .market_data import DummyMarketDataProvider
8
+
9
+ # Register all plugins so the Experiment Aggregate can resolve them
10
+ register("SMA_Crossover_Strategy", SMACrossoverStrategy)
11
+ register("Fixed_Qty_MoneyManager", FixedQtyMoneyManager)
12
+ register("Simple_Market_Broker", SimpleMarketBroker)
13
+ register("Dummy_Market_Data", DummyMarketDataProvider)
@@ -0,0 +1,3 @@
1
+ from .instant_market_fill import SimpleMarketBroker
2
+
3
+ __all__ = ["SimpleMarketBroker"]
@@ -0,0 +1,26 @@
1
+ from decimal import Decimal
2
+ from typing import List
3
+ import uuid
4
+ from Backtesting.definition.plugins import Broker
5
+ from Backtesting.definition.core.domain_models import Order, MarketBar, Fill
6
+
7
+ class SimpleMarketBroker(Broker):
8
+ def initialize(self, config: dict):
9
+ self.commission = Decimal(str(config.get('commission_per_trade', '1.50')))
10
+
11
+ def reset(self):
12
+ pass
13
+
14
+ def process_order(self, order: Order, current_market_bar: MarketBar) -> List[Fill]:
15
+ # Simple market fill at the current bar's close price
16
+ fill = Fill(
17
+ fill_id=str(uuid.uuid4()),
18
+ order_id=order.order_id,
19
+ symbol=order.symbol,
20
+ side=order.side,
21
+ quantity=order.quantity,
22
+ fill_price=current_market_bar.close,
23
+ fill_time=current_market_bar.timestamp,
24
+ commission=self.commission
25
+ )
26
+ return [fill]
@@ -0,0 +1,3 @@
1
+ from .dummy_data_provider import DummyMarketDataProvider
2
+
3
+ __all__ = ["DummyMarketDataProvider"]
@@ -0,0 +1,32 @@
1
+ import math
2
+ from decimal import Decimal
3
+ from datetime import datetime, timedelta
4
+ from typing import Iterator
5
+ from Backtesting.execution.market_data import MarketDataProvider
6
+ from Backtesting.definition.core.domain_models import MarketBar
7
+
8
+
9
+ class DummyMarketDataProvider(MarketDataProvider):
10
+ """Generates a synthetic sine-wave market to guarantee SMA crossovers."""
11
+
12
+ def __init__(self, num_bars: int = 50, symbol: str = "DUMMY", start_price: Decimal = Decimal("100.0"), **kwargs):
13
+ self.num_bars = int(num_bars)
14
+ self.symbol = symbol
15
+ self.start_price = Decimal(str(start_price))
16
+
17
+ def get_bars(self) -> Iterator[MarketBar]:
18
+ base_time = datetime(2023, 1, 1)
19
+ for i in range(self.num_bars):
20
+ # Create a predictable wave: goes up, then down, triggering crossovers
21
+ trend = math.sin(i / 4.0) * 15
22
+ price = self.start_price + Decimal(str(trend))
23
+
24
+ yield MarketBar(
25
+ symbol=self.symbol,
26
+ timestamp=base_time + timedelta(hours=i),
27
+ open=price,
28
+ high=price + Decimal("1.0"),
29
+ low=price - Decimal("1.0"),
30
+ close=price,
31
+ volume=Decimal("1000")
32
+ )
@@ -0,0 +1,3 @@
1
+ from .fixed_quality import FixedQtyMoneyManager
2
+
3
+ __all__ = ["FixedQtyMoneyManager"]
@@ -0,0 +1,36 @@
1
+ from decimal import Decimal
2
+ from typing import List
3
+ import uuid
4
+ from Backtesting.definition.plugins import MoneyManager
5
+ from Backtesting.definition.core.domain_models import Signal, Order, OrderType, OrderSide, OrderStatus, SignalType
6
+
7
+ class FixedQtyMoneyManager(MoneyManager):
8
+ def initialize(self, config: dict):
9
+ self.trade_quantity = Decimal(str(config.get('trade_quantity', '10.0')))
10
+
11
+ def reset(self):
12
+ pass
13
+
14
+ def size_order(self, signal: Signal, current_portfolio_state: dict) -> List[Order]:
15
+ if signal.signal_type == SignalType.BUY_LONG:
16
+ side = OrderSide.BUY
17
+ elif signal.signal_type == SignalType.EXIT_LONG:
18
+ side = OrderSide.SELL
19
+ else:
20
+ return [] # Ignore shorts for this simple beta test
21
+
22
+ order = Order(
23
+ order_id=str(uuid.uuid4()),
24
+ signal_id=signal.id,
25
+ symbol=signal.symbol,
26
+ order_type=OrderType.MARKET,
27
+ side=side,
28
+ quantity=self.trade_quantity,
29
+ price=None,
30
+ stop_price=None,
31
+ status=OrderStatus.SUBMITTED,
32
+ filled_quantity=Decimal('0'),
33
+ average_fill_price=Decimal('0'),
34
+ commission=Decimal('0')
35
+ )
36
+ return [order]
@@ -0,0 +1,52 @@
1
+ from collections import deque
2
+ from decimal import Decimal
3
+ from typing import List
4
+ import uuid
5
+ from Backtesting.definition.plugins import Strategy
6
+ from Backtesting.definition.core.domain_models import MarketBar, Signal, SignalType
7
+
8
+
9
+ class SMACrossoverStrategy(Strategy):
10
+ def initialize(self, config: dict):
11
+ self.fast_period = int(config.get('fast_period', 5))
12
+ self.slow_period = int(config.get('slow_period', 10))
13
+ self.symbol = config.get('symbol', 'DUMMY')
14
+
15
+ self.prices = deque(maxlen=self.slow_period)
16
+ self.prev_fast_sma = None
17
+ self.prev_slow_sma = None
18
+
19
+ def reset(self):
20
+ self.prices.clear()
21
+ self.prev_fast_sma = None
22
+ self.prev_slow_sma = None
23
+
24
+ def on_data(self, bar: MarketBar) -> List[Signal]:
25
+ self.prices.append(bar.close)
26
+
27
+ # Wait until we have enough data to calculate the slow SMA
28
+ if len(self.prices) < self.slow_period:
29
+ return []
30
+
31
+ # Calculate SMAs using Decimal math to prevent drift
32
+ fast_sma = sum(list(self.prices)[-self.fast_period:], Decimal('0')) / self.fast_period
33
+ slow_sma = sum(self.prices, Decimal('0')) / self.slow_period
34
+
35
+ signals = []
36
+ if self.prev_fast_sma is not None and self.prev_slow_sma is not None:
37
+ # Golden Cross (Buy)
38
+ if self.prev_fast_sma <= self.prev_slow_sma and fast_sma > slow_sma:
39
+ signals.append(Signal(
40
+ id=str(uuid.uuid4()), symbol=self.symbol,
41
+ signal_type=SignalType.BUY_LONG, strength=Decimal('1.0')
42
+ ))
43
+ # Death Cross (Exit)
44
+ elif self.prev_fast_sma >= self.prev_slow_sma and fast_sma < slow_sma:
45
+ signals.append(Signal(
46
+ id=str(uuid.uuid4()), symbol=self.symbol,
47
+ signal_type=SignalType.EXIT_LONG, strength=Decimal('1.0')
48
+ ))
49
+
50
+ self.prev_fast_sma = fast_sma
51
+ self.prev_slow_sma = slow_sma
52
+ return signals
@@ -0,0 +1,3 @@
1
+ from .SMA_crossovr import SMACrossoverStrategy
2
+
3
+ __all__ = ["SMACrossoverStrategy"]