bbstrader 0.3.4__py3-none-any.whl → 0.3.6__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.

Potentially problematic release.


This version of bbstrader might be problematic. Click here for more details.

Files changed (38) hide show
  1. bbstrader/__init__.py +10 -1
  2. bbstrader/__main__.py +5 -0
  3. bbstrader/apps/_copier.py +3 -3
  4. bbstrader/btengine/strategy.py +113 -38
  5. bbstrader/compat.py +18 -10
  6. bbstrader/config.py +0 -16
  7. bbstrader/core/scripts.py +4 -3
  8. bbstrader/metatrader/account.py +51 -26
  9. bbstrader/metatrader/analysis.py +30 -16
  10. bbstrader/metatrader/copier.py +136 -58
  11. bbstrader/metatrader/trade.py +39 -45
  12. bbstrader/metatrader/utils.py +5 -4
  13. bbstrader/models/factors.py +17 -13
  14. bbstrader/models/ml.py +96 -49
  15. bbstrader/models/nlp.py +83 -66
  16. bbstrader/trading/execution.py +39 -22
  17. bbstrader/tseries.py +103 -127
  18. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/METADATA +29 -46
  19. bbstrader-0.3.6.dist-info/RECORD +62 -0
  20. bbstrader-0.3.6.dist-info/top_level.txt +3 -0
  21. docs/conf.py +56 -0
  22. tests/__init__.py +0 -0
  23. tests/engine/__init__.py +1 -0
  24. tests/engine/test_backtest.py +58 -0
  25. tests/engine/test_data.py +536 -0
  26. tests/engine/test_events.py +300 -0
  27. tests/engine/test_execution.py +219 -0
  28. tests/engine/test_portfolio.py +307 -0
  29. tests/metatrader/__init__.py +0 -0
  30. tests/metatrader/test_account.py +1769 -0
  31. tests/metatrader/test_rates.py +292 -0
  32. tests/metatrader/test_risk_management.py +700 -0
  33. tests/metatrader/test_trade.py +439 -0
  34. bbstrader-0.3.4.dist-info/RECORD +0 -49
  35. bbstrader-0.3.4.dist-info/top_level.txt +0 -1
  36. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/WHEEL +0 -0
  37. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/entry_points.txt +0 -0
  38. {bbstrader-0.3.4.dist-info → bbstrader-0.3.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,307 @@
1
+ import warnings
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from queue import Queue
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ import pandas as pd
8
+ import pytest
9
+
10
+ from bbstrader.btengine.event import FillEvent, MarketEvent, OrderEvent, SignalEvent
11
+ from bbstrader.btengine.portfolio import Portfolio
12
+
13
+ warnings.filterwarnings("ignore")
14
+ warnings.filterwarnings("ignore", category=FutureWarning)
15
+
16
+
17
+ class MockDataHandler:
18
+ """A mock DataHandler to control market data during tests."""
19
+
20
+ def __init__(self, symbol_list, initial_data):
21
+ self.symbol_list = symbol_list
22
+ self.latest_symbol_data = initial_data
23
+
24
+ def get_latest_bar_datetime(self, symbol):
25
+ return self.latest_symbol_data[symbol]["datetime"]
26
+
27
+ def get_latest_bar_value(self, symbol, val_type):
28
+ # val_type can be 'adj_close' or 'close'
29
+ return self.latest_symbol_data[symbol]["price"]
30
+
31
+ def update_bar(self, symbol, dt, price):
32
+ self.latest_symbol_data[symbol]["datetime"] = dt
33
+ self.latest_symbol_data[symbol]["price"] = price
34
+
35
+
36
+ # PYTEST FIXTURES
37
+ @pytest.fixture
38
+ def basic_portfolio():
39
+ """Fixture to create a fresh Portfolio instance for each test."""
40
+ symbol_list = ["AAPL", "GOOG"]
41
+ start_date = datetime(2023, 1, 1)
42
+ initial_capital = 100000.0
43
+ initial_data = {
44
+ "AAPL": {"datetime": start_date, "price": 150.0},
45
+ "GOOG": {"datetime": start_date, "price": 100.0},
46
+ }
47
+
48
+ events_queue = Queue()
49
+ mock_bars = MockDataHandler(symbol_list, initial_data)
50
+
51
+ portfolio = Portfolio(
52
+ bars=mock_bars,
53
+ events=events_queue,
54
+ start_date=start_date,
55
+ initial_capital=initial_capital,
56
+ print_stats=False, # Disable printing/plotting during tests
57
+ )
58
+ return portfolio
59
+
60
+
61
+ # --- TEST CASES ---
62
+ def test_initialization(basic_portfolio):
63
+ """Tests that the portfolio is initialized with the correct state."""
64
+ p = basic_portfolio
65
+ assert p.initial_capital == 100000.0
66
+ assert p.symbol_list == ["AAPL", "GOOG"]
67
+ assert p.current_positions == {"AAPL": 0, "GOOG": 0}
68
+
69
+ expected_holdings = {
70
+ "AAPL": 0.0,
71
+ "GOOG": 0.0,
72
+ "Cash": 100000.0,
73
+ "Commission": 0.0,
74
+ "Total": 100000.0,
75
+ }
76
+ assert p.current_holdings == expected_holdings
77
+
78
+ assert len(p.all_positions) == 1
79
+ initial_positions = p.all_positions[0]
80
+ assert initial_positions["Datetime"] == datetime(2023, 1, 1)
81
+
82
+ assert initial_positions["AAPL"] == 0
83
+ assert initial_positions["GOOG"] == 0
84
+ assert "Total" not in initial_positions
85
+
86
+ assert len(p.all_holdings) == 1
87
+ assert p.all_holdings[0]["Total"] == 100000.0
88
+
89
+
90
+ def test_tf_mapping_and_initialization_error():
91
+ """Tests the timeframe mapping and ensures invalid timeframes raise errors."""
92
+ mock_bars = MagicMock()
93
+ mock_bars.symbol_list = ["DUMMY"]
94
+
95
+ # Test a valid timeframe
96
+ p = Portfolio(
97
+ mock_bars, Queue(), datetime.now(), time_frame="5m", session_duration=6.5
98
+ )
99
+ assert p.tf == int(252 * (60 / 5) * 6.5)
100
+
101
+ # Test another valid timeframe
102
+ p_d1 = Portfolio(mock_bars, Queue(), datetime.now(), time_frame="D1")
103
+ assert p_d1.tf == 252
104
+
105
+ # Test that an unsupported timeframe raises a ValueError
106
+ with pytest.raises(ValueError, match="Timeframe not supported"):
107
+ Portfolio(mock_bars, Queue(), datetime.now(), time_frame="UnsupportedTF")
108
+
109
+
110
+ def test_update_timeindex(basic_portfolio):
111
+ """Tests that portfolio history is correctly updated on a new market event."""
112
+ p = basic_portfolio
113
+ new_date = datetime(2023, 1, 2)
114
+ p.bars.update_bar("AAPL", new_date, 155.0)
115
+ p.bars.update_bar("GOOG", new_date, 102.0)
116
+
117
+ # Give the portfolio a position to make the test more meaningful
118
+ p.current_positions["AAPL"] = 10
119
+
120
+ p.update_timeindex(MarketEvent())
121
+
122
+ assert len(p.all_positions) == 2
123
+ assert p.all_positions[1]["Datetime"] == new_date
124
+ assert p.all_positions[1]["AAPL"] == 10 # Records position from previous bar
125
+
126
+ assert len(p.all_holdings) == 2
127
+ market_value = 10 * 155.0
128
+ expected_total = 100000.0 + market_value
129
+ assert p.all_holdings[1]["Total"] == expected_total
130
+ assert p.all_holdings[1]["AAPL"] == market_value
131
+
132
+
133
+ def test_update_fill_buy(basic_portfolio):
134
+ """Tests that a BUY FillEvent correctly updates positions and holdings."""
135
+ p = basic_portfolio
136
+ fill_event = FillEvent(
137
+ datetime.now(), "AAPL", "TEST_EXCHANGE", 10, "BUY", None, commission=5.0
138
+ )
139
+
140
+ p.update_fill(fill_event)
141
+
142
+ assert p.current_positions["AAPL"] == 10
143
+ cost = 10 * 150.0
144
+ assert p.current_holdings["Cash"] == 100000.0 - cost - 5.0
145
+ assert p.current_holdings["Commission"] == 5.0
146
+ assert p.current_holdings["AAPL"] == cost
147
+
148
+
149
+ def test_update_fill_sell_short(basic_portfolio):
150
+ """Tests that a SELL (short) FillEvent correctly updates state."""
151
+ p = basic_portfolio
152
+ fill_event = FillEvent(
153
+ datetime.now(), "GOOG", "TEST_EXCHANGE", 20, "SELL", None, commission=7.0
154
+ )
155
+
156
+ p.update_fill(fill_event)
157
+
158
+ assert p.current_positions["GOOG"] == -20
159
+ proceeds = 20 * 100.0
160
+ assert p.current_holdings["Cash"] == 100000.0 + proceeds - 7.0
161
+ assert p.current_holdings["GOOG"] == -proceeds
162
+
163
+
164
+ @pytest.mark.parametrize(
165
+ "signal_type, initial_pos, expected_direction, expected_quantity",
166
+ [
167
+ ("LONG", 0, "BUY", 50),
168
+ ("SHORT", 0, "SELL", 30),
169
+ ("EXIT", 100, "SELL", 100),
170
+ ("EXIT", -75, "BUY", 75),
171
+ ("EXIT", 0, None, 0), # No order if exiting from a flat position
172
+ ("LONG", 10, "BUY", 50), # New LONG signal ignores existing long position
173
+ ],
174
+ )
175
+ def test_generate_order(
176
+ basic_portfolio, signal_type, initial_pos, expected_direction, expected_quantity
177
+ ):
178
+ """Tests order generation logic for various signal types and positions."""
179
+ p = basic_portfolio
180
+ p.current_positions["AAPL"] = initial_pos
181
+
182
+ quantity = 50 if signal_type == "LONG" else 30
183
+ signal = SignalEvent(
184
+ 1, "AAPL", datetime.now(), signal_type, quantity=quantity, strength=1.0
185
+ )
186
+
187
+ order = p.generate_order(signal)
188
+
189
+ if expected_direction is None:
190
+ assert order is None
191
+ else:
192
+ assert isinstance(order, OrderEvent)
193
+ assert order.direction == expected_direction
194
+ assert order.quantity == expected_quantity
195
+ assert order.order_type == "MKT"
196
+
197
+
198
+ def test_update_signal_puts_order_on_queue(basic_portfolio):
199
+ """Tests that update_signal correctly generates and queues an order."""
200
+ p = basic_portfolio
201
+ signal = SignalEvent(1, "GOOG", datetime.now(), "LONG", quantity=100, strength=0.5)
202
+
203
+ assert p.events.qsize() == 0
204
+ p.update_signal(signal)
205
+ assert p.events.qsize() == 1
206
+
207
+ order = p.events.get()
208
+ assert isinstance(order, OrderEvent)
209
+ assert order.symbol == "GOOG"
210
+ assert order.direction == "BUY"
211
+ assert order.quantity == 50 # 100 * 0.5
212
+
213
+ @pytest.mark.filterwarnings("ignore")
214
+ @patch("bbstrader.btengine.performance.plt.show")
215
+ @patch("bbstrader.btengine.portfolio.plot_performance")
216
+ @patch("bbstrader.btengine.portfolio.plot_returns_and_dd")
217
+ @patch("bbstrader.btengine.portfolio.plot_monthly_yearly_returns")
218
+ @patch("bbstrader.btengine.portfolio.show_qs_stats")
219
+ @patch("bbstrader.btengine.portfolio.qs.plots.monthly_heatmap")
220
+ @patch("pandas.DataFrame.to_csv")
221
+ def test_output_summary_stats(
222
+ mock_to_csv,
223
+ mock_qs_heatmap,
224
+ mock_show_qs,
225
+ mock_plot_monthly,
226
+ mock_plot_ret_dd,
227
+ mock_plot_perf,
228
+ mock_plt_show,
229
+ basic_portfolio,
230
+ ):
231
+ """Tests performance calculation and that reporting functions are called without side effects."""
232
+ p = basic_portfolio
233
+ p.print_stats = True
234
+ p.strategy_name = "Test Strategy"
235
+ p.output_dir = "test_results"
236
+
237
+ # Manually create a simple history for the equity curve
238
+ tz = pd.Timestamp.utcnow().tzinfo
239
+ p.all_holdings = [
240
+ {
241
+ "Datetime": datetime(2023, 1, 1, tzinfo=tz),
242
+ "Total": 100000.0,
243
+ "Commission": 0.0,
244
+ "Cash": 100000.0,
245
+ "AAPL": 0,
246
+ "GOOG": 0,
247
+ },
248
+ {
249
+ "Datetime": datetime(2023, 1, 2, tzinfo=tz),
250
+ "Total": 101000.0,
251
+ "Commission": 0.0,
252
+ "Cash": 100000.0,
253
+ "AAPL": 0,
254
+ "GOOG": 0,
255
+ },
256
+ {
257
+ "Datetime": datetime(2023, 1, 3, tzinfo=tz),
258
+ "Total": 100500.0,
259
+ "Commission": 0.0,
260
+ "Cash": 100000.0,
261
+ "AAPL": 0,
262
+ "GOOG": 0,
263
+ },
264
+ {
265
+ "Datetime": datetime(2023, 1, 4, tzinfo=tz),
266
+ "Total": 102000.0,
267
+ "Commission": 0.0,
268
+ "Cash": 100000.0,
269
+ "AAPL": 0,
270
+ "GOOG": 0,
271
+ },
272
+ ]
273
+
274
+ p.create_equity_curve_dataframe()
275
+
276
+ p.equity_curve["Returns"] = p.equity_curve["Returns"].fillna(0.0)
277
+ p.equity_curve["Equity Curve"] = p.equity_curve["Equity Curve"].fillna(1.0)
278
+
279
+ p.equity_curve["Drawdown"] = 0.0
280
+
281
+ # Call the method under test
282
+ stats = p.output_summary_stats()
283
+
284
+ # Assertions
285
+ stats_dict = dict(stats)
286
+ assert stats_dict["Total Return"] == "2.00%"
287
+ assert "Sharpe Ratio" in stats_dict
288
+ assert "Max Drawdown" in stats_dict
289
+
290
+ # Verify that our mocked (and problematic) functions were called
291
+ mock_plot_perf.assert_called_once()
292
+ mock_plot_ret_dd.assert_called_once()
293
+ mock_plot_monthly.assert_called_once()
294
+ mock_qs_heatmap.assert_called_once()
295
+ mock_show_qs.assert_called_once()
296
+ mock_to_csv.assert_called_once()
297
+
298
+ # Verify plots are never actually shown
299
+ mock_plt_show.assert_not_called()
300
+
301
+ # 4. Verify the path for the saved CSV file
302
+ call_args, _ = mock_to_csv.call_args
303
+ file_path = call_args[0]
304
+ assert isinstance(file_path, Path)
305
+ assert "test_results" in str(file_path)
306
+ assert "Test_Strategy" in str(file_path)
307
+ assert str(file_path).endswith("_equities.csv")
File without changes