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