bbstrader 0.1.9__py3-none-any.whl → 0.1.91__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.
- bbstrader/__ini__.py +4 -2
- bbstrader/btengine/__init__.py +5 -5
- bbstrader/btengine/backtest.py +51 -10
- bbstrader/btengine/data.py +147 -55
- bbstrader/btengine/event.py +4 -1
- bbstrader/btengine/execution.py +125 -23
- bbstrader/btengine/performance.py +4 -7
- bbstrader/btengine/portfolio.py +34 -13
- bbstrader/btengine/strategy.py +466 -6
- bbstrader/config.py +111 -0
- bbstrader/metatrader/__init__.py +4 -4
- bbstrader/metatrader/account.py +348 -53
- bbstrader/metatrader/rates.py +232 -27
- bbstrader/metatrader/risk.py +34 -23
- bbstrader/metatrader/trade.py +321 -165
- bbstrader/metatrader/utils.py +2 -53
- bbstrader/models/factors.py +0 -0
- bbstrader/models/ml.py +0 -0
- bbstrader/models/optimization.py +0 -0
- bbstrader/trading/__init__.py +1 -1
- bbstrader/trading/execution.py +268 -164
- bbstrader/trading/scripts.py +57 -0
- bbstrader/trading/strategies.py +41 -65
- bbstrader/tseries.py +274 -39
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/METADATA +11 -3
- bbstrader-0.1.91.dist-info/RECORD +31 -0
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/WHEEL +1 -1
- bbstrader-0.1.9.dist-info/RECORD +0 -26
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/LICENSE +0 -0
- {bbstrader-0.1.9.dist-info → bbstrader-0.1.91.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from telegram import Bot
|
|
3
|
+
from notifypy import Notify
|
|
4
|
+
from telegram.error import TelegramError
|
|
5
|
+
|
|
6
|
+
__all__ = ['send_telegram_message', 'send_notification', 'send_message']
|
|
7
|
+
|
|
8
|
+
async def send_telegram_message(token, chat_id, text=''):
|
|
9
|
+
"""
|
|
10
|
+
Send a message to a telegram chat
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
token: str: Telegram bot token
|
|
14
|
+
chat_id: int or str or list: Chat id or list of chat ids
|
|
15
|
+
text: str: Message to send
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
bot = Bot(token=token)
|
|
19
|
+
if isinstance(chat_id, (int, str)):
|
|
20
|
+
chat_id = [chat_id]
|
|
21
|
+
for id in chat_id:
|
|
22
|
+
await bot.send_message(chat_id=id, text=text)
|
|
23
|
+
except TelegramError as e:
|
|
24
|
+
print(f"Error sending message: {e}")
|
|
25
|
+
|
|
26
|
+
def send_notification(title, message=''):
|
|
27
|
+
"""
|
|
28
|
+
Send a desktop notification
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
title: str: Title of the notification
|
|
32
|
+
message: str: Message of the notification
|
|
33
|
+
"""
|
|
34
|
+
notification = Notify(default_notification_application_name='bbstrading')
|
|
35
|
+
notification.title = title
|
|
36
|
+
notification.message = message
|
|
37
|
+
notification.send()
|
|
38
|
+
|
|
39
|
+
def send_message(title='SIGNAL', message='New signal',
|
|
40
|
+
notify_me=False, telegram=False, token=None, chat_id=None):
|
|
41
|
+
"""
|
|
42
|
+
Send a message to the user
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
title: str: Title of the message
|
|
46
|
+
message: str: Message of the message
|
|
47
|
+
notify_me: bool: Send a desktop notification
|
|
48
|
+
telegram: bool: Send a telegram message
|
|
49
|
+
token: str: Telegram bot token
|
|
50
|
+
chat_id: int or str or list: Chat id or list of chat ids
|
|
51
|
+
"""
|
|
52
|
+
if notify_me:
|
|
53
|
+
send_notification(title, message=message)
|
|
54
|
+
if telegram:
|
|
55
|
+
if token is None or chat_id is None:
|
|
56
|
+
raise ValueError('Token and chat_id must be provided')
|
|
57
|
+
asyncio.run(send_telegram_message(token, chat_id, text=message))
|
bbstrader/trading/strategies.py
CHANGED
|
@@ -15,10 +15,10 @@ from bbstrader.models.risk import HMMRiskManager
|
|
|
15
15
|
from bbstrader.models.risk import build_hmm_models
|
|
16
16
|
from bbstrader.btengine.backtest import BacktestEngine
|
|
17
17
|
from bbstrader.btengine.strategy import Strategy
|
|
18
|
+
from bbstrader.btengine.strategy import MT5Strategy
|
|
18
19
|
from bbstrader.btengine.execution import *
|
|
19
20
|
from bbstrader.btengine.data import *
|
|
20
|
-
from bbstrader.tseries import
|
|
21
|
-
KalmanFilterModel, ArimaGarchModel)
|
|
21
|
+
from bbstrader.tseries import KalmanFilterModel, ArimaGarchModel
|
|
22
22
|
from typing import Union, Optional, Literal, Dict, List
|
|
23
23
|
|
|
24
24
|
__all__ = [
|
|
@@ -37,6 +37,7 @@ def get_quantities(quantities, symbol_list):
|
|
|
37
37
|
elif isinstance(quantities, int):
|
|
38
38
|
return {symbol: quantities for symbol in symbol_list}
|
|
39
39
|
|
|
40
|
+
|
|
40
41
|
class SMAStrategy(Strategy):
|
|
41
42
|
"""
|
|
42
43
|
Carries out a basic Moving Average Crossover strategy bactesting with a
|
|
@@ -76,10 +77,7 @@ class SMAStrategy(Strategy):
|
|
|
76
77
|
"""
|
|
77
78
|
self.bars = bars
|
|
78
79
|
self.events = events
|
|
79
|
-
|
|
80
|
-
self.symbol_list = symbol_list
|
|
81
|
-
else:
|
|
82
|
-
self.symbol_list = self.bars.symbol_list
|
|
80
|
+
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
83
81
|
self.mode = mode
|
|
84
82
|
|
|
85
83
|
self.short_window = kwargs.get("short_window", 50)
|
|
@@ -102,12 +100,13 @@ class SMAStrategy(Strategy):
|
|
|
102
100
|
def get_backtest_data(self):
|
|
103
101
|
symbol_data = {symbol: None for symbol in self.symbol_list}
|
|
104
102
|
for s in self.symbol_list:
|
|
103
|
+
latest_bars = self.bars.get_latest_bars(s, N=self.long_window)
|
|
105
104
|
bar_date = self.bars.get_latest_bar_datetime(s)
|
|
106
105
|
bars = self.bars.get_latest_bars_values(
|
|
107
|
-
s, "
|
|
106
|
+
s, "adj_close", N=self.long_window
|
|
108
107
|
)
|
|
109
108
|
returns_val = self.bars.get_latest_bars_values(
|
|
110
|
-
s, "
|
|
109
|
+
s, "returns", N=self.risk_window
|
|
111
110
|
)
|
|
112
111
|
if len(bars) >= self.long_window and len(returns_val) >= self.risk_window:
|
|
113
112
|
regime = self.risk_models[s].which_trade_allowed(returns_val)
|
|
@@ -124,7 +123,7 @@ class SMAStrategy(Strategy):
|
|
|
124
123
|
for s, data in symbol_data.items():
|
|
125
124
|
signal = None
|
|
126
125
|
if data is not None:
|
|
127
|
-
price = self.bars.get_latest_bar_value(s, "
|
|
126
|
+
price = self.bars.get_latest_bar_value(s, "adj_close")
|
|
128
127
|
short_sma, long_sma, regime, bar_date = data
|
|
129
128
|
dt = bar_date
|
|
130
129
|
if regime == "LONG":
|
|
@@ -159,8 +158,8 @@ class SMAStrategy(Strategy):
|
|
|
159
158
|
symbol_data = {symbol: None for symbol in self.symbol_list}
|
|
160
159
|
for symbol in self.symbol_list:
|
|
161
160
|
sig_rate = Rates(symbol, self.tf, 0, self.risk_window)
|
|
162
|
-
hmm_data = sig_rate.
|
|
163
|
-
prices = sig_rate.
|
|
161
|
+
hmm_data = sig_rate.returns.values
|
|
162
|
+
prices = sig_rate.close.values
|
|
164
163
|
current_regime = self.risk_models[symbol].which_trade_allowed(hmm_data)
|
|
165
164
|
assert len(prices) >= self.long_window and len(hmm_data) >= self.risk_window
|
|
166
165
|
short_sma = np.mean(prices[-self.short_window:])
|
|
@@ -241,10 +240,7 @@ class ArimaGarchStrategy(Strategy):
|
|
|
241
240
|
"""
|
|
242
241
|
self.bars = bars
|
|
243
242
|
self.events = events
|
|
244
|
-
|
|
245
|
-
self.symbol_list = symbol_list
|
|
246
|
-
else:
|
|
247
|
-
self.symbol_list = self.bars.symbol_list
|
|
243
|
+
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
248
244
|
self.mode = mode
|
|
249
245
|
|
|
250
246
|
self.qty = get_quantities(
|
|
@@ -259,7 +255,6 @@ class ArimaGarchStrategy(Strategy):
|
|
|
259
255
|
self.long_market = {s : False for s in self.symbol_list}
|
|
260
256
|
self.short_market = {s : False for s in self.symbol_list}
|
|
261
257
|
|
|
262
|
-
|
|
263
258
|
def _build_arch_models(self, **kwargs) -> Dict[str, ArimaGarchModel]:
|
|
264
259
|
arch_models = {symbol: None for symbol in self.symbol_list}
|
|
265
260
|
for symbol in self.symbol_list:
|
|
@@ -280,10 +275,10 @@ class ArimaGarchStrategy(Strategy):
|
|
|
280
275
|
N = self.risk_window
|
|
281
276
|
dt = self.bars.get_latest_bar_datetime(symbol)
|
|
282
277
|
bars = self.bars.get_latest_bars_values(
|
|
283
|
-
symbol, "
|
|
278
|
+
symbol, "close", N=self.arima_window
|
|
284
279
|
)
|
|
285
280
|
returns = self.bars.get_latest_bars_values(
|
|
286
|
-
symbol, '
|
|
281
|
+
symbol, 'returns', N=self.risk_window
|
|
287
282
|
)
|
|
288
283
|
df = pd.DataFrame()
|
|
289
284
|
df['Close'] = bars[-M:]
|
|
@@ -303,7 +298,7 @@ class ArimaGarchStrategy(Strategy):
|
|
|
303
298
|
signal = None
|
|
304
299
|
prediction = self.arima_models[symbol].get_prediction(data)
|
|
305
300
|
regime = self.risk_models[symbol].which_trade_allowed(returns)
|
|
306
|
-
price = self.bars.get_latest_bar_value(symbol, "
|
|
301
|
+
price = self.bars.get_latest_bar_value(symbol, "adj_close")
|
|
307
302
|
|
|
308
303
|
# If we are short the market, check for an exit
|
|
309
304
|
if prediction > 0 and self.short_market[symbol]:
|
|
@@ -342,14 +337,15 @@ class ArimaGarchStrategy(Strategy):
|
|
|
342
337
|
rates = arch_data.get_rates_from_pos()
|
|
343
338
|
arch_returns = self.arima_models[symbol].load_and_prepare_data(rates)
|
|
344
339
|
window_data = arch_returns['diff_log_return'].iloc[-self.arima_window:]
|
|
345
|
-
hmm_returns = arch_data.
|
|
340
|
+
hmm_returns = arch_data.returns.values[-self.risk_window:]
|
|
346
341
|
symbol_data[symbol] = (window_data, hmm_returns)
|
|
347
342
|
return symbol_data
|
|
348
343
|
|
|
349
344
|
def create_live_signals(self):
|
|
350
345
|
signals = {symbol: None for symbol in self.symbol_list}
|
|
346
|
+
data = self.get_live_data()
|
|
351
347
|
for symbol in self.symbol_list:
|
|
352
|
-
symbol_data =
|
|
348
|
+
symbol_data = data[symbol]
|
|
353
349
|
if symbol_data is not None:
|
|
354
350
|
window_data, hmm_returns = symbol_data
|
|
355
351
|
prediction = self.arima_models[symbol].get_prediction(window_data)
|
|
@@ -403,14 +399,12 @@ class KalmanFilterStrategy(Strategy):
|
|
|
403
399
|
"""
|
|
404
400
|
self.bars = bars
|
|
405
401
|
self.events_queue = events
|
|
406
|
-
|
|
407
|
-
self.symbol_list = symbol_list
|
|
408
|
-
else:
|
|
409
|
-
self.symbol_list = self.bars.symbol_list
|
|
402
|
+
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
410
403
|
self.mode = mode
|
|
411
404
|
|
|
412
405
|
self.hmm_tiker = kwargs.get("hmm_tiker")
|
|
413
406
|
self._assert_tikers()
|
|
407
|
+
self.account = Account()
|
|
414
408
|
self.hmm_window = kwargs.get("hmm_window", 50)
|
|
415
409
|
self.qty = kwargs.get("quantity", 100)
|
|
416
410
|
self.tf = kwargs.get("time_frame", "D1")
|
|
@@ -437,8 +431,8 @@ class KalmanFilterStrategy(Strategy):
|
|
|
437
431
|
return
|
|
438
432
|
et, sqrt_Qt = etqt
|
|
439
433
|
theta = self.kl_model.theta
|
|
440
|
-
p1 = self.bars.get_latest_bar_value(self.tickers[1], "
|
|
441
|
-
p0 = self.bars.get_latest_bar_value(self.tickers[0], "
|
|
434
|
+
p1 = self.bars.get_latest_bar_value(self.tickers[1], "adj_close")
|
|
435
|
+
p0 = self.bars.get_latest_bar_value(self.tickers[0], "adj_close")
|
|
442
436
|
if et >= -sqrt_Qt and self.long_market:
|
|
443
437
|
print("CLOSING LONG: %s" % dt)
|
|
444
438
|
y_signal = SignalEvent(1, self.tickers[1], dt, "EXIT", price=p1)
|
|
@@ -481,14 +475,9 @@ class KalmanFilterStrategy(Strategy):
|
|
|
481
475
|
|
|
482
476
|
def calculate_livexy(self):
|
|
483
477
|
signals = {symbol: None for symbol in self.symbol_list}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
p0_data = p0_.get_close
|
|
488
|
-
p1_data = p1_.get_close
|
|
489
|
-
prices = np.array(
|
|
490
|
-
[p0_data.values[-1], p1_data.values[-1]]
|
|
491
|
-
)
|
|
478
|
+
p0_price = self.account.get_tick_info(self.tickers[0]).ask
|
|
479
|
+
p1_price = self.account.get_tick_info(self.tickers[1]).ask
|
|
480
|
+
prices = np.array([p0_price, p1_price])
|
|
492
481
|
et_std = self.kl_model.calculate_etqt(prices)
|
|
493
482
|
if et_std is not None:
|
|
494
483
|
et, std = et_std
|
|
@@ -514,19 +503,15 @@ class KalmanFilterStrategy(Strategy):
|
|
|
514
503
|
def calculate_backtest_signals(self):
|
|
515
504
|
p0, p1 = self.tickers[0], self.tickers[1]
|
|
516
505
|
dt = self.bars.get_latest_bar_datetime(p0)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
)
|
|
520
|
-
_y = self.bars.get_latest_bars_values(
|
|
521
|
-
p1, "Close", N=1
|
|
522
|
-
)
|
|
506
|
+
x = self.bars.get_latest_bar_value(p0, "close")
|
|
507
|
+
y = self.bars.get_latest_bar_value(p1, "close")
|
|
523
508
|
returns = self.bars.get_latest_bars_values(
|
|
524
|
-
self.hmm_tiker, "
|
|
509
|
+
self.hmm_tiker, "returns", N=self.hmm_window
|
|
525
510
|
)
|
|
526
511
|
latest_prices = np.array([-1.0, -1.0])
|
|
527
512
|
if len(returns) >= self.hmm_window:
|
|
528
|
-
latest_prices[0] =
|
|
529
|
-
latest_prices[1] =
|
|
513
|
+
latest_prices[0] = x
|
|
514
|
+
latest_prices[1] = y
|
|
530
515
|
et_qt = self.kl_model.calculate_etqt(latest_prices)
|
|
531
516
|
regime = self.risk_model[
|
|
532
517
|
self.hmm_tiker].which_trade_allowed(returns)
|
|
@@ -537,7 +522,7 @@ class KalmanFilterStrategy(Strategy):
|
|
|
537
522
|
signals = {symbol: None for symbol in self.symbol_list}
|
|
538
523
|
initial_signals = self.calculate_livexy()
|
|
539
524
|
hmm_data = Rates(self.hmm_ticker, self.tf, 0, self.hmm_window)
|
|
540
|
-
returns = hmm_data.
|
|
525
|
+
returns = hmm_data.returns.values
|
|
541
526
|
current_regime = self.risk_model[
|
|
542
527
|
self.hmm_tiker].which_trade_allowed(returns)
|
|
543
528
|
for symbol in self.symbol_list:
|
|
@@ -592,10 +577,7 @@ class StockIndexSTBOTrading(Strategy):
|
|
|
592
577
|
"""
|
|
593
578
|
self.bars = bars
|
|
594
579
|
self.events = events
|
|
595
|
-
|
|
596
|
-
self.symbol_list = symbol_list
|
|
597
|
-
else:
|
|
598
|
-
self.symbol_list = self.bars.symbol_list
|
|
580
|
+
self.symbol_list = symbol_list or self.bars.symbol_list
|
|
599
581
|
self.mode = mode
|
|
600
582
|
|
|
601
583
|
self.account = Account()
|
|
@@ -670,7 +652,7 @@ class StockIndexSTBOTrading(Strategy):
|
|
|
670
652
|
def calculate_backtest_signals(self):
|
|
671
653
|
for index in self.symbol_list.copy():
|
|
672
654
|
dt = self.bars.get_latest_bar_datetime(index)
|
|
673
|
-
last_price = self.bars.get_latest_bars_values(index, '
|
|
655
|
+
last_price = self.bars.get_latest_bars_values(index, 'close', N=1)
|
|
674
656
|
|
|
675
657
|
current_price = last_price[-1]
|
|
676
658
|
if self.last_price[index] is None:
|
|
@@ -693,7 +675,6 @@ class StockIndexSTBOTrading(Strategy):
|
|
|
693
675
|
and self.num_buys[index] <= self.max_trades[index]):
|
|
694
676
|
signal = SignalEvent(100, index, dt, 'LONG',
|
|
695
677
|
quantity=self.qty[index], price=current_price)
|
|
696
|
-
print(f'{dt}: LONG {self.qty[index]} units of {index} at {current_price}')
|
|
697
678
|
self.events.put(signal)
|
|
698
679
|
self.num_buys[index] += 1
|
|
699
680
|
self.buy_prices[index].append(current_price)
|
|
@@ -705,12 +686,10 @@ class StockIndexSTBOTrading(Strategy):
|
|
|
705
686
|
if self._calculate_pct_change(
|
|
706
687
|
current_price, av_price) >= (self.expeted_return[index]):
|
|
707
688
|
signal = SignalEvent(100, index, dt, 'EXIT', quantity=qty, price=current_price)
|
|
708
|
-
print(f'{dt}: EXIT {qty} units of {index} at {current_price}')
|
|
709
689
|
self.events.put(signal)
|
|
710
690
|
self.num_buys[index] = 0
|
|
711
691
|
self.buy_prices[index] = []
|
|
712
692
|
|
|
713
|
-
|
|
714
693
|
def calculate_signals(self, event=None) -> Dict[str, Union[str, None]]:
|
|
715
694
|
if self.mode == 'backtest' and event is not None:
|
|
716
695
|
if event.type == 'MARKET':
|
|
@@ -730,13 +709,12 @@ def _run_backtest(
|
|
|
730
709
|
engine = BacktestEngine(
|
|
731
710
|
symbol_list, capital, 0.0, datetime.strptime(
|
|
732
711
|
kwargs['yf_start'], "%Y-%m-%d"),
|
|
733
|
-
kwargs.get("data_handler",
|
|
734
|
-
kwargs.get("exc_handler",
|
|
712
|
+
kwargs.get("data_handler", YFDataHandler),
|
|
713
|
+
kwargs.get("exc_handler", SimExecutionHandler),
|
|
735
714
|
kwargs.pop('backtester_class'), **kwargs
|
|
736
715
|
)
|
|
737
716
|
engine.simulate_trading()
|
|
738
717
|
|
|
739
|
-
|
|
740
718
|
def _run_arch_backtest(
|
|
741
719
|
capital: float = 100000.0,
|
|
742
720
|
quantity: int = 1000
|
|
@@ -748,11 +726,10 @@ def _run_arch_backtest(
|
|
|
748
726
|
"yf_start": "2010-01-04",
|
|
749
727
|
"hmm_data": hmm_data,
|
|
750
728
|
'backtester_class': ArimaGarchStrategy,
|
|
751
|
-
"data_handler":
|
|
729
|
+
"data_handler": YFDataHandler,
|
|
752
730
|
}
|
|
753
731
|
_run_backtest("ARIMA+GARCH & HMM", capital, ["^GSPC"], kwargs)
|
|
754
732
|
|
|
755
|
-
|
|
756
733
|
def _run_kf_backtest(
|
|
757
734
|
capital: float = 100000.0,
|
|
758
735
|
quantity: int = 2000
|
|
@@ -767,11 +744,10 @@ def _run_kf_backtest(
|
|
|
767
744
|
"hmm_tiker": "TLT",
|
|
768
745
|
"session_duration": 6.5,
|
|
769
746
|
'backtester_class': KalmanFilterStrategy,
|
|
770
|
-
"data_handler":
|
|
747
|
+
"data_handler": YFDataHandler
|
|
771
748
|
}
|
|
772
749
|
_run_backtest("Kalman Filter & HMM", capital, symbol_list, kwargs)
|
|
773
750
|
|
|
774
|
-
|
|
775
751
|
def _run_sma_backtest(
|
|
776
752
|
capital: float = 100000.0,
|
|
777
753
|
quantity: int = 1
|
|
@@ -782,10 +758,10 @@ def _run_sma_backtest(
|
|
|
782
758
|
"hmm_end": "2009-12-31",
|
|
783
759
|
"yf_start": "2010-01-04",
|
|
784
760
|
"hmm_data": spx_data,
|
|
785
|
-
"mt5_start": datetime(2010,
|
|
786
|
-
"mt5_end": datetime(2023,
|
|
761
|
+
"mt5_start": datetime(2010,1,1),
|
|
762
|
+
"mt5_end": datetime(2023,1,1),
|
|
787
763
|
"backtester_class": SMAStrategy,
|
|
788
|
-
"data_handler":
|
|
764
|
+
"data_handler": MT5DataHandler,
|
|
789
765
|
"exc_handler": MT5ExecutionHandler
|
|
790
766
|
}
|
|
791
767
|
_run_backtest("SMA & HMM", capital, ["[SP500]"], kwargs)
|
|
@@ -810,7 +786,7 @@ def _run_sistbo_backtest(
|
|
|
810
786
|
'yf_start': start.strftime('%Y-%m-%d'),
|
|
811
787
|
'time_frame': '15m',
|
|
812
788
|
"backtester_class": StockIndexSTBOTrading,
|
|
813
|
-
"data_handler":
|
|
789
|
+
"data_handler": MT5DataHandler,
|
|
814
790
|
"exc_handler": MT5ExecutionHandler
|
|
815
791
|
}
|
|
816
792
|
_run_backtest("Stock Index Short Term Buy Only ", capital, symbol_list, kwargs)
|
|
@@ -839,4 +815,4 @@ def test_strategy(strategy: Literal['sma', 'klf', 'arch', 'sistbo'] = 'sma',
|
|
|
839
815
|
if strategy in _BACKTESTS:
|
|
840
816
|
_BACKTESTS[strategy](quantity=quantity)
|
|
841
817
|
else:
|
|
842
|
-
raise ValueError(f"Unknown strategy: {strategy}")
|
|
818
|
+
raise ValueError(f"Unknown strategy: {strategy}")
|