bullishpy 0.14.0__py3-none-any.whl → 0.16.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.

Potentially problematic release.


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

@@ -1,14 +1,12 @@
1
1
  import logging
2
2
  from datetime import date
3
- from typing import Optional, List, Callable, Any, Literal, Dict, Union
3
+ from typing import Optional, List, Callable, Any, Literal, Dict
4
4
 
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
  from pydantic import BaseModel, Field, PrivateAttr, create_model
8
8
 
9
9
  from bullish.analysis.functions import (
10
- cross,
11
- cross_value,
12
10
  ADX,
13
11
  MACD,
14
12
  RSI,
@@ -19,24 +17,48 @@ from bullish.analysis.functions import (
19
17
  SMA,
20
18
  ADOSC,
21
19
  PRICE,
22
- momentum,
23
- sma_50_above_sma_200,
24
- price_above_sma50,
20
+ cross_simple,
21
+ cross_value_series,
22
+ find_last_true_run_start,
25
23
  )
26
24
 
27
25
  logger = logging.getLogger(__name__)
28
26
  SignalType = Literal["Short", "Long", "Oversold", "Overbought", "Value"]
29
27
 
30
28
 
29
+ def _last_date(d: pd.Series) -> Optional[date]:
30
+ d_valid = d[d == 1]
31
+ if d_valid.empty:
32
+ return None
33
+ last_index = d_valid.last_valid_index()
34
+ return last_index.date() if last_index is not None else None # type: ignore
35
+
36
+
37
+ class ProcessingFunction(BaseModel):
38
+ date: Callable[[pd.Series], Optional[date]] = Field(default=_last_date)
39
+ number: Callable[[pd.Series], Optional[float]] = Field(
40
+ default=lambda d: d.iloc[-1] if not d.dropna().empty else None
41
+ )
42
+
43
+
44
+ class SignalSeries(BaseModel):
45
+ name: str
46
+ date: date
47
+ value: float
48
+ symbol: str
49
+
50
+
31
51
  class Signal(BaseModel):
32
52
  name: str
33
53
  type_info: SignalType
34
54
  type: Any
35
55
  range: Optional[List[float]] = None
36
- function: Callable[[pd.DataFrame], Optional[Union[date, float]]]
56
+ function: Callable[[pd.DataFrame], pd.Series]
57
+ processing: ProcessingFunction = Field(default_factory=ProcessingFunction)
37
58
  description: str
38
59
  date: Optional[date] = None
39
60
  value: Optional[float] = None
61
+ in_use_backtest: bool = False
40
62
 
41
63
  def is_date(self) -> bool:
42
64
  if self.type == Optional[date]:
@@ -46,11 +68,22 @@ class Signal(BaseModel):
46
68
  else:
47
69
  raise NotImplementedError
48
70
 
71
+ def apply_function(self, data: pd.DataFrame) -> pd.Series:
72
+ result = self.function(data)
73
+ if not isinstance(result, pd.Series):
74
+ raise ValueError(
75
+ f"Function for signal {self.name} must return a pandas Series"
76
+ )
77
+ return result
78
+
49
79
  def compute(self, data: pd.DataFrame) -> None:
50
80
  if self.is_date():
51
- self.date = self.function(data) # type: ignore
81
+ self.date = self.processing.date(self.apply_function(data))
52
82
  else:
53
- self.value = self.function(data) # type: ignore
83
+ self.value = self.processing.number(self.apply_function(data))
84
+
85
+ def compute_series(self, data: pd.DataFrame) -> pd.Series:
86
+ return self.apply_function(data)
54
87
 
55
88
 
56
89
  class Indicator(BaseModel):
@@ -68,9 +101,9 @@ class Indicator(BaseModel):
68
101
  f"Expected columns {self.expected_columns}, but got {results.columns.tolist()}"
69
102
  )
70
103
  self._data = results
71
- self._signals()
104
+ self.compute_signals()
72
105
 
73
- def _signals(self) -> None:
106
+ def compute_signals(self) -> None:
74
107
  for signal in self.signals:
75
108
  try:
76
109
  signal.compute(self._data)
@@ -79,6 +112,45 @@ class Indicator(BaseModel):
79
112
  f"Fail to compute signal {signal.name} for indicator {self.name}: {e}"
80
113
  )
81
114
 
115
+ def compute_series(self, data: pd.DataFrame, symbol: str) -> pd.DataFrame:
116
+ series = []
117
+ try:
118
+ results = self.function(data)
119
+ except Exception as e:
120
+ logger.error(
121
+ f"Failed to compute indicator {self.name} for symbol {symbol}: {e}"
122
+ )
123
+ return pd.DataFrame()
124
+ if not set(self.expected_columns).issubset(results.columns):
125
+ raise ValueError(
126
+ f"Expected columns {self.expected_columns}, but got {results.columns.tolist()}"
127
+ )
128
+ for signal in self.signals:
129
+ if not signal.in_use_backtest:
130
+ continue
131
+ try:
132
+ series_ = signal.compute_series(results)
133
+ if signal.type == Optional[date]:
134
+ series__ = pd.DataFrame(series_[series_ == 1].rename("value"))
135
+ else:
136
+ series__ = pd.DataFrame(
137
+ series_[series_ != None].rename("value") # noqa: E711
138
+ )
139
+
140
+ series__["name"] = signal.name
141
+ series__["date"] = series__.index.date # type: ignore
142
+ series__["symbol"] = symbol
143
+ series__ = series__.reset_index(drop=True)
144
+ series.append(series__)
145
+ except Exception as e:
146
+ logger.error(
147
+ f"Fail to compute signal {signal.name} for indicator {self.name}: {e}"
148
+ )
149
+ if not series:
150
+ return pd.DataFrame()
151
+ data = pd.concat(series).reset_index(drop=True)
152
+ return data
153
+
82
154
 
83
155
  def indicators_factory() -> List[Indicator]:
84
156
  return [
@@ -93,18 +165,14 @@ def indicators_factory() -> List[Indicator]:
93
165
  description="ADX 14 Long Signal",
94
166
  type_info="Long",
95
167
  type=Optional[date],
96
- function=lambda d: d[
97
- (d.ADX_14 > 20) & (d.PLUS_DI > d.MINUS_DI)
98
- ].last_valid_index(),
168
+ function=lambda d: (d.ADX_14 > 20) & (d.PLUS_DI > d.MINUS_DI),
99
169
  ),
100
170
  Signal(
101
171
  name="ADX_14_SHORT",
102
172
  description="ADX 14 Short Signal",
103
173
  type_info="Short",
104
174
  type=Optional[date],
105
- function=lambda d: d[
106
- (d.ADX_14 > 20) & (d.MINUS_DI > d.PLUS_DI)
107
- ].last_valid_index(),
175
+ function=lambda d: (d.ADX_14 > 20) & (d.MINUS_DI > d.PLUS_DI),
108
176
  ),
109
177
  ],
110
178
  ),
@@ -123,28 +191,35 @@ def indicators_factory() -> List[Indicator]:
123
191
  description="MACD 12-26-9 Bullish Crossover",
124
192
  type_info="Long",
125
193
  type=Optional[date],
126
- function=lambda d: cross(d.MACD_12_26_9, d.MACD_12_26_9_SIGNAL),
194
+ function=lambda d: cross_simple(
195
+ d.MACD_12_26_9, d.MACD_12_26_9_SIGNAL
196
+ ),
197
+ in_use_backtest=True,
127
198
  ),
128
199
  Signal(
129
200
  name="MACD_12_26_9_BEARISH_CROSSOVER",
130
201
  description="MACD 12-26-9 Bearish Crossover",
131
202
  type_info="Short",
132
203
  type=Optional[date],
133
- function=lambda d: cross(d.MACD_12_26_9_SIGNAL, d.MACD_12_26_9),
204
+ function=lambda d: cross_simple(
205
+ d.MACD_12_26_9_SIGNAL, d.MACD_12_26_9
206
+ ),
134
207
  ),
135
208
  Signal(
136
209
  name="MACD_12_26_9_ZERO_LINE_CROSS_UP",
137
210
  description="MACD 12-26-9 Zero Line Cross Up",
138
211
  type_info="Long",
139
212
  type=Optional[date],
140
- function=lambda d: cross_value(d.MACD_12_26_9, 0),
213
+ function=lambda d: cross_value_series(d.MACD_12_26_9, 0),
141
214
  ),
142
215
  Signal(
143
216
  name="MACD_12_26_9_ZERO_LINE_CROSS_DOWN",
144
217
  description="MACD 12-26-9 Zero Line Cross Down",
145
218
  type_info="Long",
146
219
  type=Optional[date],
147
- function=lambda d: cross_value(d.MACD_12_26_9, 0, above=False),
220
+ function=lambda d: cross_value_series(
221
+ d.MACD_12_26_9, 0, above=False
222
+ ),
148
223
  ),
149
224
  ],
150
225
  ),
@@ -159,53 +234,53 @@ def indicators_factory() -> List[Indicator]:
159
234
  description="RSI Bullish Crossover",
160
235
  type_info="Long",
161
236
  type=Optional[date],
162
- function=lambda d: cross_value(d.RSI, 30),
237
+ function=lambda d: cross_value_series(d.RSI, 30),
238
+ in_use_backtest=True,
163
239
  ),
164
240
  Signal(
165
241
  name="RSI_BULLISH_CROSSOVER_40",
166
242
  description="RSI Bullish Crossover 40",
167
243
  type_info="Long",
168
244
  type=Optional[date],
169
- function=lambda d: cross_value(d.RSI, 40),
245
+ function=lambda d: cross_value_series(d.RSI, 40),
246
+ in_use_backtest=True,
170
247
  ),
171
248
  Signal(
172
249
  name="RSI_BULLISH_CROSSOVER_45",
173
250
  description="RSI Bullish Crossover 45",
174
251
  type_info="Long",
175
252
  type=Optional[date],
176
- function=lambda d: cross_value(d.RSI, 45),
253
+ function=lambda d: cross_value_series(d.RSI, 45),
254
+ in_use_backtest=True,
177
255
  ),
178
256
  Signal(
179
257
  name="RSI_BEARISH_CROSSOVER",
180
258
  description="RSI Bearish Crossover",
181
259
  type_info="Short",
182
260
  type=Optional[date],
183
- function=lambda d: cross_value(d.RSI, 70, above=False),
261
+ function=lambda d: cross_value_series(d.RSI, 70, above=False),
184
262
  ),
185
263
  Signal(
186
264
  name="RSI_OVERSOLD",
187
265
  description="RSI Oversold Signal",
188
266
  type_info="Oversold",
189
267
  type=Optional[date],
190
- function=lambda d: d[(d.RSI < 30) & (d.RSI > 0)].last_valid_index(),
268
+ function=lambda d: (d.RSI < 30) & (d.RSI > 0),
269
+ in_use_backtest=True,
191
270
  ),
192
271
  Signal(
193
272
  name="RSI_OVERBOUGHT",
194
273
  description="RSI Overbought Signal",
195
274
  type_info="Overbought",
196
275
  type=Optional[date],
197
- function=lambda d: d[
198
- (d.RSI < 100) & (d.RSI > 70)
199
- ].last_valid_index(),
276
+ function=lambda d: (d.RSI < 100) & (d.RSI > 70),
200
277
  ),
201
278
  Signal(
202
279
  name="RSI_NEUTRAL",
203
280
  description="RSI Neutral Signal",
204
281
  type_info="Overbought",
205
282
  type=Optional[date],
206
- function=lambda d: d[
207
- (d.RSI < 60) & (d.RSI > 40)
208
- ].last_valid_index(),
283
+ function=lambda d: (d.RSI < 60) & (d.RSI > 40),
209
284
  ),
210
285
  ],
211
286
  ),
@@ -220,18 +295,14 @@ def indicators_factory() -> List[Indicator]:
220
295
  description="Stoch Oversold Signal",
221
296
  type_info="Oversold",
222
297
  type=Optional[date],
223
- function=lambda d: d[
224
- (d.SLOW_K < 20) & (d.SLOW_K > 0)
225
- ].last_valid_index(),
298
+ function=lambda d: (d.SLOW_K < 20) & (d.SLOW_K > 0),
226
299
  ),
227
300
  Signal(
228
301
  name="STOCH_OVERBOUGHT",
229
302
  description="Stoch Overbought Signal",
230
303
  type_info="Overbought",
231
304
  type=Optional[date],
232
- function=lambda d: d[
233
- (d.SLOW_K < 100) & (d.SLOW_K > 80)
234
- ].last_valid_index(),
305
+ function=lambda d: (d.SLOW_K < 100) & (d.SLOW_K > 80),
235
306
  ),
236
307
  ],
237
308
  ),
@@ -246,14 +317,14 @@ def indicators_factory() -> List[Indicator]:
246
317
  description="MFI Oversold Signal",
247
318
  type_info="Oversold",
248
319
  type=Optional[date],
249
- function=lambda d: d[(d.MFI < 20)].last_valid_index(),
320
+ function=lambda d: (d.MFI < 20),
250
321
  ),
251
322
  Signal(
252
323
  name="MFI_OVERBOUGHT",
253
324
  description="MFI Overbought Signal",
254
325
  type_info="Overbought",
255
326
  type=Optional[date],
256
- function=lambda d: d[(d.MFI > 80)].last_valid_index(),
327
+ function=lambda d: (d.MFI > 80),
257
328
  ),
258
329
  ],
259
330
  ),
@@ -268,35 +339,33 @@ def indicators_factory() -> List[Indicator]:
268
339
  description="Golden cross: SMA 50 crosses above SMA 200",
269
340
  type_info="Oversold",
270
341
  type=Optional[date],
271
- function=lambda d: cross(d.SMA_50, d.SMA_200),
342
+ function=lambda d: cross_simple(d.SMA_50, d.SMA_200),
343
+ in_use_backtest=True,
272
344
  ),
273
345
  Signal(
274
346
  name="DEATH_CROSS",
275
347
  description="Death cross: SMA 50 crosses below SMA 200",
276
348
  type_info="Overbought",
277
349
  type=Optional[date],
278
- function=lambda d: cross(d.SMA_50, d.SMA_200, above=False),
279
- ),
280
- Signal(
281
- name="MOMENTUM_TIME_SPAN",
282
- description="Momentum time span",
283
- type_info="Overbought",
284
- type=Optional[date],
285
- function=lambda d: momentum(d),
350
+ function=lambda d: cross_simple(d.SMA_50, d.SMA_200, above=False),
286
351
  ),
287
352
  Signal(
288
353
  name="SMA_50_ABOVE_SMA_200",
289
354
  description="SMA 50 is above SMA 200",
290
355
  type_info="Overbought",
291
356
  type=Optional[date],
292
- function=lambda d: sma_50_above_sma_200(d),
357
+ function=lambda d: d.SMA_50 > d.SMA_200,
358
+ in_use_backtest=True,
359
+ processing=ProcessingFunction(date=find_last_true_run_start),
293
360
  ),
294
361
  Signal(
295
362
  name="PRICE_ABOVE_SMA_50",
296
363
  description="Price is above SMA 50",
297
364
  type_info="Overbought",
298
365
  type=Optional[date],
299
- function=lambda d: price_above_sma50(d),
366
+ function=lambda d: d.SMA_50 < d.CLOSE,
367
+ in_use_backtest=True,
368
+ processing=ProcessingFunction(date=find_last_true_run_start),
300
369
  ),
301
370
  ],
302
371
  ),
@@ -311,39 +380,44 @@ def indicators_factory() -> List[Indicator]:
311
380
  description="Current price is lower than the 200-day high",
312
381
  type_info="Oversold",
313
382
  type=Optional[date],
314
- function=lambda d: d[
315
- 0.6 * d["200_DAY_HIGH"] > d.LAST_PRICE
316
- ].last_valid_index(),
383
+ function=lambda d: 0.6 * d["200_DAY_HIGH"] > d.LAST_PRICE,
317
384
  ),
318
385
  Signal(
319
386
  name="LOWER_THAN_20_DAY_HIGH",
320
387
  description="Current price is lower than the 20-day high",
321
388
  type_info="Oversold",
322
389
  type=Optional[date],
323
- function=lambda d: d[
324
- 0.6 * d["20_DAY_HIGH"] > d.LAST_PRICE
325
- ].last_valid_index(),
390
+ function=lambda d: 0.6 * d["20_DAY_HIGH"] > d.LAST_PRICE,
326
391
  ),
327
392
  Signal(
328
393
  name="MEDIAN_WEEKLY_GROWTH",
329
394
  description="Median weekly growth",
330
395
  type_info="Oversold",
331
396
  type=Optional[float],
332
- function=lambda d: np.median(d.WEEKLY_GROWTH.unique()),
397
+ function=lambda d: d.WEEKLY_GROWTH,
398
+ processing=ProcessingFunction(
399
+ number=lambda v: np.median(v.unique())
400
+ ),
333
401
  ),
334
402
  Signal(
335
403
  name="MEDIAN_MONTHLY_GROWTH",
336
404
  description="Median monthly growth",
337
405
  type_info="Oversold",
338
406
  type=Optional[float],
339
- function=lambda d: np.median(d.MONTHLY_GROWTH.unique()),
407
+ function=lambda d: d.MONTHLY_GROWTH,
408
+ processing=ProcessingFunction(
409
+ number=lambda v: np.median(v.unique())
410
+ ),
340
411
  ),
341
412
  Signal(
342
413
  name="MEDIAN_YEARLY_GROWTH",
343
414
  description="Median yearly growth",
344
415
  type_info="Oversold",
345
416
  type=Optional[float],
346
- function=lambda d: np.median(d.YEARLY_GROWTH.unique()),
417
+ function=lambda d: d.YEARLY_GROWTH,
418
+ processing=ProcessingFunction(
419
+ number=lambda v: np.median(v.unique())
420
+ ),
347
421
  ),
348
422
  ],
349
423
  ),
@@ -358,49 +432,61 @@ def indicators_factory() -> List[Indicator]:
358
432
  type_info="Value",
359
433
  description="Median daily Rate of Change of the last 30 days",
360
434
  type=Optional[float],
361
- function=lambda d: np.median(d.ROC_1.tolist()[-30:]),
435
+ function=lambda d: d.ROC_1,
436
+ processing=ProcessingFunction(
437
+ number=lambda v: np.median(v.tolist()[-30:])
438
+ ),
362
439
  ),
363
440
  Signal(
364
441
  name="MEDIAN_RATE_OF_CHANGE_7_4",
365
442
  type_info="Value",
366
443
  description="Median weekly Rate of Change of the last 4 weeks",
367
444
  type=Optional[float],
368
- function=lambda d: np.median(d.ROC_7.tolist()[-4:]),
445
+ function=lambda d: d.ROC_7,
446
+ processing=ProcessingFunction(
447
+ number=lambda v: np.median(v.tolist()[-4:])
448
+ ),
369
449
  ),
370
450
  Signal(
371
451
  name="MEDIAN_RATE_OF_CHANGE_7_12",
372
452
  type_info="Value",
373
453
  description="Median weekly Rate of Change of the last 12 weeks",
374
454
  type=Optional[float],
375
- function=lambda d: np.median(d.ROC_7.tolist()[-12:]),
455
+ function=lambda d: d.ROC_7,
456
+ processing=ProcessingFunction(
457
+ number=lambda v: np.median(v.tolist()[-12:])
458
+ ),
376
459
  ),
377
460
  Signal(
378
461
  name="MEDIAN_RATE_OF_CHANGE_30",
379
462
  type_info="Value",
380
463
  description="Median monthly Rate of Change of the last 12 Months",
381
464
  type=Optional[float],
382
- function=lambda d: np.median(d.ROC_30.tolist()[-12:]),
465
+ function=lambda d: d.ROC_30,
466
+ processing=ProcessingFunction(
467
+ number=lambda v: np.median(v.tolist()[-12:])
468
+ ),
383
469
  ),
384
470
  Signal(
385
471
  name="RATE_OF_CHANGE_30",
386
472
  type_info="Value",
387
473
  description="30-day Rate of Change",
388
474
  type=Optional[float],
389
- function=lambda d: d.ROC_30.tolist()[-1],
475
+ function=lambda d: d.ROC_30,
390
476
  ),
391
477
  Signal(
392
478
  name="RATE_OF_CHANGE_7",
393
479
  type_info="Value",
394
480
  description="7-day Rate of Change",
395
481
  type=Optional[float],
396
- function=lambda d: d.ROC_7.tolist()[-1],
482
+ function=lambda d: d.ROC_7,
397
483
  ),
398
484
  Signal(
399
485
  name="MOMENTUM",
400
486
  type_info="Value",
401
487
  description="7-day Rate of Change",
402
488
  type=Optional[float],
403
- function=lambda d: d.MOM.iloc[-1],
489
+ function=lambda d: d.MOM,
404
490
  ),
405
491
  ],
406
492
  ),
@@ -415,16 +501,14 @@ def indicators_factory() -> List[Indicator]:
415
501
  type_info="Oversold",
416
502
  description="Bullish momentum in money flow",
417
503
  type=Optional[date],
418
- function=lambda d: cross_value(d.ADOSC, 0, above=True),
504
+ function=lambda d: cross_value_series(d.ADOSC, 0, above=True),
419
505
  ),
420
506
  Signal(
421
507
  name="POSITIVE_ADOSC_20_DAY_BREAKOUT",
422
508
  type_info="Oversold",
423
509
  description="20-day breakout confirmed by positive ADOSC",
424
510
  type=Optional[date],
425
- function=lambda d: d[
426
- (d.ADOSC_SIGNAL == True) # noqa: E712
427
- ].last_valid_index(),
511
+ function=lambda d: (d.ADOSC_SIGNAL == True), # noqa: E712
428
512
  ),
429
513
  ],
430
514
  ),
@@ -447,53 +531,49 @@ def indicators_factory() -> List[Indicator]:
447
531
  type_info="Long",
448
532
  description="Morning Star Candlestick Pattern",
449
533
  type=Optional[date],
450
- function=lambda d: d[(d.CDLMORNINGSTAR == 100)].last_valid_index(),
534
+ function=lambda d: d.CDLMORNINGSTAR == 100,
451
535
  ),
452
536
  Signal(
453
537
  name="CDL3LINESTRIKE",
454
538
  description="3 Line Strike Candlestick Pattern",
455
539
  type_info="Long",
456
540
  type=Optional[date],
457
- function=lambda d: d[(d.CDL3LINESTRIKE == 100)].last_valid_index(),
541
+ function=lambda d: d.CDL3LINESTRIKE == 100,
458
542
  ),
459
543
  Signal(
460
544
  name="CDL3WHITESOLDIERS",
461
545
  description="3 White Soldiers Candlestick Pattern",
462
546
  type_info="Long",
463
547
  type=Optional[date],
464
- function=lambda d: d[
465
- (d.CDL3WHITESOLDIERS == 100)
466
- ].last_valid_index(),
548
+ function=lambda d: d.CDL3WHITESOLDIERS == 100,
467
549
  ),
468
550
  Signal(
469
551
  name="CDLABANDONEDBABY",
470
552
  description="Abandoned Baby Candlestick Pattern",
471
553
  type_info="Long",
472
554
  type=Optional[date],
473
- function=lambda d: d[
474
- (d.CDLABANDONEDBABY == 100)
475
- ].last_valid_index(),
555
+ function=lambda d: d.CDLABANDONEDBABY == 100,
476
556
  ),
477
557
  Signal(
478
558
  name="CDLTASUKIGAP",
479
559
  description="Tasukigap Candlestick Pattern",
480
560
  type_info="Long",
481
561
  type=Optional[date],
482
- function=lambda d: d[(d.CDLTASUKIGAP == 100)].last_valid_index(),
562
+ function=lambda d: d.CDLTASUKIGAP == 100,
483
563
  ),
484
564
  Signal(
485
565
  name="CDLPIERCING",
486
566
  description="Piercing Candlestick Pattern",
487
567
  type_info="Long",
488
568
  type=Optional[date],
489
- function=lambda d: d[(d.CDLPIERCING == 100)].last_valid_index(),
569
+ function=lambda d: d.CDLPIERCING == 100,
490
570
  ),
491
571
  Signal(
492
572
  name="CDLENGULFING",
493
573
  description="Engulfing Candlestick Pattern",
494
574
  type_info="Long",
495
575
  type=Optional[date],
496
- function=lambda d: d[(d.CDLENGULFING == 100)].last_valid_index(),
576
+ function=lambda d: d.CDLENGULFING == 100,
497
577
  ),
498
578
  ],
499
579
  ),
@@ -503,7 +583,15 @@ def indicators_factory() -> List[Indicator]:
503
583
  class Indicators(BaseModel):
504
584
  indicators: List[Indicator] = Field(default_factory=indicators_factory)
505
585
 
506
- def compute(self, data: pd.DataFrame) -> None:
586
+ def in_use_backtest(self) -> List[str]:
587
+ return [
588
+ signal.name.lower()
589
+ for indicator in self.indicators
590
+ for signal in indicator.signals
591
+ if signal.in_use_backtest
592
+ ]
593
+
594
+ def _compute(self, data: pd.DataFrame) -> None:
507
595
  for indicator in self.indicators:
508
596
  try:
509
597
  indicator.compute(data)
@@ -514,8 +602,16 @@ class Indicators(BaseModel):
514
602
  f"Computed {indicator.name} with {len(indicator.signals)} signals"
515
603
  )
516
604
 
517
- def to_dict(self, data: pd.DataFrame) -> Dict[str, Any]:
518
- self.compute(data)
605
+ def compute_series(self, data: pd.DataFrame, symbol: str) -> List[SignalSeries]:
606
+ data__ = pd.concat(
607
+ [indicator.compute_series(data, symbol) for indicator in self.indicators]
608
+ )
609
+ return [
610
+ SignalSeries.model_validate(s) for s in data__.to_dict(orient="records")
611
+ ]
612
+
613
+ def compute(self, data: pd.DataFrame) -> Dict[str, Any]:
614
+ self._compute(data)
519
615
  res = {}
520
616
  for indicator in self.indicators:
521
617
  for signal in indicator.signals: