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

@@ -85,6 +85,8 @@ class BacktestEngine(Backtest):
85
85
  strategy (Strategy): Generates signals based on market data.
86
86
  kwargs : Additional parameters based on the `ExecutionHandler`,
87
87
  the `DataHandler`, the `Strategy` used and the `Portfolio`.
88
+ - show_equity (bool): Show the equity curve of the portfolio.
89
+ - stats_file (str): File to save the summary stats.
88
90
  """
89
91
  self.symbol_list = symbol_list
90
92
  self.initial_capital = initial_capital
@@ -104,6 +106,7 @@ class BacktestEngine(Backtest):
104
106
 
105
107
  self._generate_trading_instances()
106
108
  self.show_equity = kwargs.get("show_equity", False)
109
+ self.stats_file = kwargs.get("stats_file", None)
107
110
 
108
111
  def _generate_trading_instances(self):
109
112
  """
@@ -137,14 +140,21 @@ class BacktestEngine(Backtest):
137
140
  i = 0
138
141
  while True:
139
142
  i += 1
140
- # Update the market bars
143
+ value = self.portfolio.all_holdings[-1]['Total']
141
144
  if self.data_handler.continue_backtest == True:
145
+ # Update the market bars
142
146
  self.data_handler.update_bars()
143
147
  self.strategy.check_pending_orders()
148
+ self.strategy.get_update_from_portfolio(
149
+ self.portfolio.current_positions,
150
+ self.portfolio.current_holdings
151
+ )
152
+ self.strategy.cash = value
144
153
  else:
145
154
  print("\n[======= BACKTEST COMPLETED =======]")
146
- print(f"END DATE: {self.data_handler.get_latest_bar_datetime()}")
155
+ print(f"END DATE: {self.data_handler.get_latest_bar_datetime(self.symbol_list[0])}")
147
156
  print(f"TOTAL BARS: {i} ")
157
+ print(f"PORFOLIO VALUE: {round(value, 2)}")
148
158
  break
149
159
 
150
160
  # Handle the events
@@ -171,10 +181,6 @@ class BacktestEngine(Backtest):
171
181
  self.fills += 1
172
182
  self.portfolio.update_fill(event)
173
183
  self.strategy.update_trades_from_fill(event)
174
- self.strategy.get_update_from_portfolio(
175
- self.portfolio.current_positions,
176
- self.portfolio.current_holdings
177
- )
178
184
 
179
185
  time.sleep(self.heartbeat)
180
186
 
@@ -192,13 +198,13 @@ class BacktestEngine(Backtest):
192
198
  stat2['Orders'] = self.orders
193
199
  stat2['Fills'] = self.fills
194
200
  stats.extend(stat2.items())
195
- print(
196
- tabulate(
197
- stats,
198
- headers=["Metric", "Value"],
199
- tablefmt="outline"),
200
- "\n"
201
- )
201
+ tab_stats = tabulate(stats, headers=["Metric", "Value"], tablefmt="outline")
202
+ print(tab_stats, "\n")
203
+ if self.stats_file:
204
+ with open(self.stats_file, 'a') as f:
205
+ f.write("\n[======= Summary Stats =======]\n")
206
+ f.write(tab_stats)
207
+ f.write("\n")
202
208
 
203
209
  if self.show_equity:
204
210
  print("\nCreating equity curve...")
@@ -336,7 +342,7 @@ def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwar
336
342
  data_handler=kwargs.get("data_handler"),
337
343
  strategy=kwargs.get("strategy"),
338
344
  exc_handler=kwargs.get("exc_handler"),
339
- initial_capital=kwargs.get("initial_capital"),
345
+ initial_capital=kwargs.get("initial_capital", 100000.0),
340
346
  heartbeat=kwargs.get("heartbeat", 0.0),
341
347
  **kwargs
342
348
  )
@@ -10,13 +10,18 @@ from bbstrader.metatrader.rates import download_historical_data
10
10
  from bbstrader.btengine.event import MarketEvent
11
11
  from bbstrader.config import BBSTRADER_DIR
12
12
  from datetime import datetime
13
+ from eodhd import APIClient
14
+ from financetoolkit import Toolkit
15
+ from pytz import timezone
13
16
 
14
17
 
15
18
  __all__ = [
16
19
  "DataHandler",
17
20
  "CSVDataHandler",
18
21
  "MT5DataHandler",
19
- "YFDataHandler"
22
+ "YFDataHandler",
23
+ "EODHDataHandler",
24
+ "FMPDataHandler",
20
25
  ]
21
26
 
22
27
 
@@ -167,7 +172,7 @@ class BaseCSVDataHandler(DataHandler):
167
172
  os.path.join(self.csv_dir, f'{self.symbol_list[0]}.csv')
168
173
  ).columns.to_list()
169
174
  new_names = self.columns or default_names
170
- new_names = [name.lower().replace(' ', '_') for name in new_names]
175
+ new_names = [name.strip().lower().replace(' ', '_') for name in new_names]
171
176
  self.columns = new_names
172
177
  assert 'adj_close' in new_names or 'close' in new_names, \
173
178
  "Column names must contain 'Adj Close' and 'Close' or adj_close and close"
@@ -340,7 +345,13 @@ class CSVDataHandler(BaseCSVDataHandler):
340
345
  """
341
346
  csv_dir = kwargs.get("csv_dir")
342
347
  csv_dir = csv_dir or BBSTRADER_DIR / 'csv_data'
343
- super().__init__(events, symbol_list, csv_dir)
348
+ super().__init__(
349
+ events,
350
+ symbol_list,
351
+ csv_dir,
352
+ columns =kwargs.get('columns'),
353
+ index_col=kwargs.get('index_col', 0)
354
+ )
344
355
 
345
356
 
346
357
  class MT5DataHandler(BaseCSVDataHandler):
@@ -372,32 +383,41 @@ class MT5DataHandler(BaseCSVDataHandler):
372
383
  See `bbstrader.metatrader.rates.Rates` for other arguments.
373
384
  See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
374
385
  """
375
- self.tf = kwargs.get('time_frame', 'D1')
376
- self.start = kwargs.get('mt5_start', datetime(2000, 1, 1))
377
- self.end = kwargs.get('mt5_end', datetime.now())
378
- self.use_utc = kwargs.get('use_utc', False)
379
- self.filer = kwargs.get('filter', False)
380
- self.fill_na = kwargs.get('fill_na', False)
386
+ self.tf = kwargs.get('time_frame', 'D1')
387
+ self.start = kwargs.get('mt5_start', datetime(2000, 1, 1))
388
+ self.end = kwargs.get('mt5_end', datetime.now())
389
+ self.use_utc = kwargs.get('use_utc', False)
390
+ self.filer = kwargs.get('filter', False)
391
+ self.fill_na = kwargs.get('fill_na', False)
381
392
  self.lower_cols = kwargs.get('lower_cols', True)
382
- self.data_dir = kwargs.get('data_dir')
393
+ self.data_dir = kwargs.get('data_dir')
383
394
  self.symbol_list = symbol_list
384
- csv_dir = self._download_data(self.data_dir)
385
- super().__init__(events, symbol_list, csv_dir)
395
+ self.kwargs = kwargs
396
+
397
+ csv_dir = self._download_and_cache_data(self.data_dir)
398
+ super().__init__(
399
+ events,
400
+ symbol_list,
401
+ csv_dir,
402
+ columns =kwargs.get('columns'),
403
+ index_col=kwargs.get('index_col', 0)
404
+ )
386
405
 
387
- def _download_data(self, cache_dir: str):
388
- data_dir = cache_dir or BBSTRADER_DIR / 'mt5_data' / self.tf
406
+ def _download_and_cache_data(self, cache_dir: str):
407
+ data_dir = cache_dir or BBSTRADER_DIR / 'mt5' / self.tf
389
408
  data_dir.mkdir(parents=True, exist_ok=True)
390
409
  for symbol in self.symbol_list:
391
410
  try:
392
411
  data = download_historical_data(
393
412
  symbol=symbol,
394
- time_frame=self.tf,
413
+ timeframe=self.tf,
395
414
  date_from=self.start,
396
- date_to=self.end,
415
+ date_to=self.end,
397
416
  utc=self.use_utc,
398
417
  filter=self.filer,
399
418
  fill_na=self.fill_na,
400
- lower_colnames=self.lower_cols
419
+ lower_colnames=self.lower_cols,
420
+ **self.kwargs
401
421
  )
402
422
  if data is None:
403
423
  raise ValueError(f"No data found for {symbol}")
@@ -432,15 +452,23 @@ class YFDataHandler(BaseCSVDataHandler):
432
452
  See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
433
453
  """
434
454
  self.symbol_list = symbol_list
435
- self.start_date = kwargs.get('yf_start', '2000-01-01')
436
- self.end_date = kwargs.get('yf_end', datetime.now().strftime('%Y-%m-%d'))
437
- self.cache_dir = kwargs.get('data_dir')
455
+ self.start_date = kwargs.get('yf_start')
456
+ self.end_date = kwargs.get('yf_end', datetime.now())
457
+ self.cache_dir = kwargs.get('data_dir')
458
+
438
459
  csv_dir = self._download_and_cache_data(self.cache_dir)
439
- super().__init__(events, symbol_list, csv_dir)
460
+
461
+ super().__init__(
462
+ events,
463
+ symbol_list,
464
+ csv_dir,
465
+ columns =kwargs.get('columns'),
466
+ index_col=kwargs.get('index_col', 0)
467
+ )
440
468
 
441
469
  def _download_and_cache_data(self, cache_dir: str):
442
470
  """Downloads and caches historical data as CSV files."""
443
- cache_dir = cache_dir or BBSTRADER_DIR / 'yf_data' / 'daily'
471
+ cache_dir = cache_dir or BBSTRADER_DIR / 'yfinance' / 'daily'
444
472
  os.makedirs(cache_dir, exist_ok=True)
445
473
  for symbol in self.symbol_list:
446
474
  filepath = os.path.join(cache_dir, f"{symbol}.csv")
@@ -449,39 +477,202 @@ class YFDataHandler(BaseCSVDataHandler):
449
477
  symbol, start=self.start_date, end=self.end_date, multi_level_index=False)
450
478
  if data.empty:
451
479
  raise ValueError(f"No data found for {symbol}")
452
- data.to_csv(filepath) # Cache the data
480
+ data.to_csv(filepath)
453
481
  except Exception as e:
454
482
  raise ValueError(f"Error downloading {symbol}: {e}")
455
483
  return cache_dir
456
484
 
457
485
 
458
- # TODO # Get data from EODHD
459
- # https://eodhd.com/
460
486
  class EODHDataHandler(BaseCSVDataHandler):
461
- ...
487
+ """
488
+ Downloads historical data from EOD Historical Data.
489
+ Data is fetched using the `eodhd` library.
462
490
 
463
- # TODO # Get data from FMP using Financialtoolkit API
464
- # https://github.com/bbalouki/FinanceToolkit
465
- class FMPDataHandler(BaseCSVDataHandler):
466
- ...
491
+ To use this class, you need to sign up for an API key at
492
+ https://eodhistoricaldata.com/ and provide the key as an argument.
493
+ """
494
+ def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
495
+ """
496
+ Args:
497
+ events (Queue): The Event Queue for passing market events.
498
+ symbol_list (list[str]): List of symbols to download data for.
499
+ eodhd_start (str): Start date for historical data (YYYY-MM-DD).
500
+ eodhd_end (str): End date for historical data (YYYY-MM-DD).
501
+ data_dir (str, optional): Directory for caching data .
502
+ eodhd_period (str, optional): Time period for historical data (e.g., 'd', 'w', 'm', '1m', '5m', '1h').
503
+ eodhd_api_key (str, optional): API key for EOD Historical Data.
467
504
 
505
+ Note:
506
+ See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
507
+ """
508
+ self.symbol_list = symbol_list
509
+ self.start_date = kwargs.get('eodhd_start')
510
+ self.end_date = kwargs.get('eodhd_end', datetime.now().strftime('%Y-%m-%d'))
511
+ self.period = kwargs.get('eodhd_period', 'd')
512
+ self.cache_dir = kwargs.get('data_dir')
513
+ self.__api_key = kwargs.get('eodhd_api_key', 'demo')
468
514
 
469
- class AlgoseekDataHandler(BaseCSVDataHandler):
470
- ...
515
+ csv_dir = self._download_and_cache_data(self.cache_dir)
516
+
517
+ super().__init__(
518
+ events,
519
+ symbol_list,
520
+ csv_dir,
521
+ columns =kwargs.get('columns'),
522
+ index_col=kwargs.get('index_col', 0)
523
+ )
471
524
 
525
+ def _get_data(self, symbol: str, period) -> pd.DataFrame | List[Dict]:
526
+ if not self.__api_key:
527
+ raise ValueError("API key is required for EODHD data.")
528
+ client = APIClient(api_key=self.__api_key)
529
+ if period in ['d', 'w', 'm']:
530
+ return client.get_historical_data(
531
+ symbol=symbol,
532
+ interval=period,
533
+ iso8601_start=self.start_date,
534
+ iso8601_end=self.end_date,
535
+ )
536
+ elif period in ['1m', '5m', '1h']:
537
+ hms = ' 00:00:00'
538
+ fmt = '%Y-%m-%d %H:%M:%S'
539
+ startdt = datetime.strptime(self.start_date + hms, fmt)
540
+ enddt = datetime.strptime(self.end_date + hms, fmt)
541
+ startdt = startdt.replace(tzinfo=timezone('UTC'))
542
+ enddt = enddt.replace(tzinfo=timezone('UTC'))
543
+ unix_start = int(startdt.timestamp())
544
+ unix_end = int(enddt.timestamp())
545
+ return client.get_intraday_historical_data(
546
+ symbol=symbol,
547
+ interval=period,
548
+ from_unix_time=unix_start,
549
+ to_unix_time=unix_end,
550
+ )
551
+
552
+ def _forma_data(self, data: List[Dict] | pd.DataFrame) -> pd.DataFrame:
553
+ if isinstance(data, pd.DataFrame):
554
+ if data.empty or len(data) == 0:
555
+ raise ValueError("No data found.")
556
+ df = data.drop(labels=['symbol', 'interval'], axis=1)
557
+ df = df.rename(columns={'adjusted_close': 'adj_close'})
558
+ return df
559
+
560
+ elif isinstance(data, list):
561
+ if not data or len(data) == 0:
562
+ raise ValueError("No data found.")
563
+ df = pd.DataFrame(data)
564
+ df = df.drop(columns=['timestamp', 'gmtoffset'], axis=1)
565
+ df = df.rename(columns={'datetime': 'date'})
566
+ df['adj_close'] = df['close']
567
+ df = df[['date', 'open', 'high', 'low', 'close', 'adj_close', 'volume']]
568
+ df.date = pd.to_datetime(df.date)
569
+ df = df.set_index('date')
570
+ return df
571
+
572
+ def _download_and_cache_data(self, cache_dir: str):
573
+ """Downloads and caches historical data as CSV files."""
574
+ cache_dir = cache_dir or BBSTRADER_DIR / 'eodhd' / self.period
575
+ os.makedirs(cache_dir, exist_ok=True)
576
+ for symbol in self.symbol_list:
577
+ filepath = os.path.join(cache_dir, f"{symbol}.csv")
578
+ try:
579
+ data = self._get_data(symbol, self.period)
580
+ data = self._forma_data(data)
581
+ data.to_csv(filepath)
582
+ except Exception as e:
583
+ raise ValueError(f"Error downloading {symbol}: {e}")
584
+ return cache_dir
472
585
 
473
- class BaseFMPDataHanler(object):
586
+
587
+ class FMPDataHandler(BaseCSVDataHandler):
474
588
  """
475
- This will serve as the base class for all other FMP data
476
- that is not historical data and does not have an OHLC structure.
589
+ Downloads historical data from Financial Modeling Prep (FMP).
590
+ Data is fetched using the `financetoolkit` library.
591
+
592
+ To use this class, you need to sign up for an API key at
593
+ https://financialmodelingprep.com/developer/docs/pricing and
594
+ provide the key as an argument.
595
+
477
596
  """
478
- ...
597
+ def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
598
+ """
599
+ Args:
600
+ events (Queue): The Event Queue for passing market events.
601
+ symbol_list (list[str]): List of symbols to download data for.
602
+ fmp_start (str): Start date for historical data (YYYY-MM-DD).
603
+ fmp_end (str): End date for historical data (YYYY-MM-DD).
604
+ data_dir (str, optional): Directory for caching data .
605
+ fmp_period (str, optional): Time period for historical data
606
+ (e.g. daily, weekly, monthly, quarterly, yearly, "1min", "5min", "15min", "30min", "1hour").
607
+ fmp_api_key (str): API key for Financial Modeling Prep.
479
608
 
609
+ Note:
610
+ See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
611
+ """
612
+ self.symbol_list = symbol_list
613
+ self.start_date = kwargs.get('fmp_start')
614
+ self.end_date = kwargs.get('fmp_end', datetime.now().strftime('%Y-%m-%d'))
615
+ self.period = kwargs.get('fmp_period', 'daily')
616
+ self.cache_dir = kwargs.get('data_dir')
617
+ self.__api_key = kwargs.get('fmp_api_key')
480
618
 
481
- class FMPFundamentalDataHandler(BaseFMPDataHanler):
482
- ...
619
+ csv_dir = self._download_and_cache_data(self.cache_dir)
620
+
621
+ super().__init__(
622
+ events,
623
+ symbol_list,
624
+ csv_dir,
625
+ columns =kwargs.get('columns'),
626
+ index_col=kwargs.get('index_col', 0)
627
+ )
483
628
 
484
- # TODO Add other Handlers for FMP
629
+ def _get_data(self, symbol: str, period: str) -> pd.DataFrame:
630
+ if not self.__api_key:
631
+ raise ValueError("API key is required for FMP data.")
632
+ toolkit = Toolkit(
633
+ symbol,
634
+ api_key=self.__api_key,
635
+ start_date=self.start_date,
636
+ end_date=self.end_date,
637
+ benchmark_ticker=None
638
+ )
639
+ if period in ['daily', 'weekly', 'monthly', 'quarterly', 'yearly']:
640
+ return toolkit.get_historical_data(period=period)
641
+ elif period in ['1min', '5min', '15min', '30min', '1hour']:
642
+ return toolkit.get_intraday_data(period=period)
643
+
644
+ def _format_data(self, data: pd.DataFrame, period: str) -> pd.DataFrame:
645
+ if data.empty or len(data) == 0:
646
+ raise ValueError("No data found.")
647
+ if period[0].isnumeric():
648
+ data = data.drop(columns=['Return', 'Volatility', 'Cumulative Return'], axis=1)
649
+ else:
650
+ data = data.drop(columns=['Dividends', 'Return', 'Volatility',
651
+ 'Excess Return', 'Excess Volatility',
652
+ 'Cumulative Return'], axis=1)
653
+ data = data.reset_index()
654
+ if 'Adj Close' not in data.columns:
655
+ data['Adj Close'] = data['Close']
656
+ data['date'] = data['date'].dt.to_timestamp()
657
+ data['date'] = pd.to_datetime(data['date'])
658
+ data.set_index('date', inplace=True)
659
+ return data
660
+
661
+ def _download_and_cache_data(self, cache_dir: str):
662
+ """Downloads and caches historical data as CSV files."""
663
+ cache_dir = cache_dir or BBSTRADER_DIR / 'fmp' / self.period
664
+ os.makedirs(cache_dir, exist_ok=True)
665
+ for symbol in self.symbol_list:
666
+ filepath = os.path.join(cache_dir, f"{symbol}.csv")
667
+ try:
668
+ data = self._get_data(symbol, self.period)
669
+ data = self._format_data(data, self.period)
670
+ data.to_csv(filepath)
671
+ except Exception as e:
672
+ raise ValueError(f"Error downloading {symbol}: {e}")
673
+ return cache_dir
485
674
 
486
675
 
487
676
  # TODO Add data Handlers for Interactive Brokers
677
+ class TWSDataHandler(BaseCSVDataHandler):
678
+ ...
@@ -77,8 +77,9 @@ class SimExecutionHandler(ExecutionHandler):
77
77
  if event.type == 'ORDER':
78
78
  dtime = self.bardata.get_latest_bar_datetime(event.symbol)
79
79
  fill_event = FillEvent(
80
- dtime, event.symbol,
81
- 'ARCA', event.quantity, event.direction, order=event.signal
80
+ timeindex=dtime, symbol=event.symbol,
81
+ exchange='ARCA', quantity=event.quantity, direction=event.direction,
82
+ fill_cost=None, commission=None, order=event.signal
82
83
  )
83
84
  self.events.put(fill_event)
84
85
  self.logger.info(
@@ -6,6 +6,8 @@ from scipy.stats import mstats
6
6
  import matplotlib.pyplot as plt
7
7
  from matplotlib.ticker import MaxNLocator
8
8
  import quantstats as qs
9
+ import warnings
10
+ warnings.filterwarnings("ignore")
9
11
 
10
12
  sns.set_theme()
11
13
 
@@ -93,16 +93,20 @@ class Portfolio(object):
93
93
  initial_capital (float): The starting capital in USD.
94
94
 
95
95
  kwargs (dict): Additional arguments
96
+ - `leverage`: The leverage to apply to the portfolio.
96
97
  - `time_frame`: The time frame of the bars.
97
- - `trading_hours`: The number of trading hours in a day.
98
+ - `session_duration`: The number of trading hours in a day.
98
99
  - `benchmark`: The benchmark symbol to compare the portfolio.
99
- - `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
100
+ - `output_dir`: The directory to save the backtest results.
101
+ - `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
102
+ - `print_stats`: Whether to print the backtest statistics.
100
103
  """
101
104
  self.bars = bars
102
105
  self.events = events
103
106
  self.symbol_list = self.bars.symbol_list
104
107
  self.start_date = start_date
105
108
  self.initial_capital = initial_capital
109
+ self._leverage = kwargs.get('leverage', 1)
106
110
 
107
111
  self.timeframe = kwargs.get("time_frame", "D1")
108
112
  self.trading_hours = kwargs.get("session_duration", 23)
@@ -277,8 +281,7 @@ class Portfolio(object):
277
281
 
278
282
  def generate_order(self, signal: SignalEvent):
279
283
  """
280
- Check if the portfolio has enough cash to place an order
281
- and generate an OrderEvent, else return None.
284
+ Turns a SignalEvent into an OrderEvent.
282
285
 
283
286
  Args:
284
287
  signal (SignalEvent): The tuple containing Signal information.
@@ -294,25 +297,17 @@ class Portfolio(object):
294
297
  strength = signal.strength
295
298
  price = signal.price or self._get_price(symbol)
296
299
  cur_quantity = self.current_positions[symbol]
297
- cash = self.current_holdings['Cash']
300
+ mkt_quantity = round(quantity * strength, 2)
301
+ new_quantity = mkt_quantity * self._leverage
298
302
 
299
303
  if direction in ['LONG', 'SHORT', 'EXIT']:
300
304
  order_type = 'MKT'
301
305
  else:
302
306
  order_type = direction
303
- mkt_quantity = round(quantity * strength)
304
- cost = mkt_quantity * price
305
307
 
306
- if cash >= cost:
307
- new_quantity = mkt_quantity
308
- elif cash < cost and cash > 0:
309
- new_quantity = round(cash // price)
310
- else:
311
- new_quantity = 0
312
-
313
- if new_quantity > 0 and direction == 'LONG':
308
+ if direction == 'LONG' and new_quantity > 0:
314
309
  order = OrderEvent(symbol, order_type, new_quantity, 'BUY', price, direction)
315
- if new_quantity > 0 and direction == 'SHORT':
310
+ if direction == 'SHORT' and new_quantity > 0:
316
311
  order = OrderEvent(symbol, order_type, new_quantity, 'SELL', price, direction)
317
312
 
318
313
  if direction == 'EXIT' and cur_quantity > 0:
@@ -322,6 +317,7 @@ class Portfolio(object):
322
317
 
323
318
  return order
324
319
 
320
+
325
321
  def update_signal(self, event: SignalEvent):
326
322
  """
327
323
  Acts on a SignalEvent to generate new orders