Skip to content

Commit d524e5f

Browse files
Copilotmichaelchu
andcommitted
Upgrade pandas-ta to pandas-ta-classic
- Update pyproject.toml dependency from pandas-ta to pandas-ta-classic>=0.3.59 - Update imports in signals.py and ui/tools/_indicators.py - Update conftest.py mock module name and BB column format - Fix BB column naming: BBU_{len}_{std} (pandas-ta-classic) vs BBU_{len}_{std}_{std} (old) - Update test fixture bar index (53→55) to match pandas-ta-classic RSI values Co-authored-by: michaelchu <540510+michaelchu@users.noreply.github.com>
1 parent 5323c40 commit d524e5f

File tree

6 files changed

+32
-32
lines changed

6 files changed

+32
-32
lines changed

conftest.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
"""Root conftest: mock pandas_ta when it's not installable (Python < 3.12)."""
1+
"""Root conftest: mock pandas_ta_classic when it's not installable (Python < 3.12)."""
22

33
import sys
44
import types
55

66
try:
7-
import pandas_ta # noqa: F401
7+
import pandas_ta_classic # noqa: F401
88
except (ImportError, ModuleNotFoundError, Exception):
99
import pandas as pd
1010

11-
pta = types.ModuleType("pandas_ta")
11+
pta = types.ModuleType("pandas_ta_classic")
1212
pta.version = "0.0.0-mock"
1313

1414
def _rsi(prices, length=14):
@@ -54,9 +54,9 @@ def _bbands(prices, length=20, std=2.0):
5454
lower = mid - std * std_dev
5555
return pd.DataFrame(
5656
{
57-
f"BBU_{length}_{std}_{std}": upper,
58-
f"BBM_{length}_{std}_{std}": mid,
59-
f"BBL_{length}_{std}_{std}": lower,
57+
f"BBU_{length}_{std}": upper,
58+
f"BBM_{length}_{std}": mid,
59+
f"BBL_{length}_{std}": lower,
6060
},
6161
index=prices.index,
6262
)
@@ -75,4 +75,4 @@ def _atr(high, low, close, length=14):
7575
pta.bbands = _bbands
7676
pta.atr = _atr
7777

78-
sys.modules["pandas_ta"] = pta
78+
sys.modules["pandas_ta_classic"] = pta

optopsy/signals.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from typing import Callable
3434

3535
import pandas as pd
36-
import pandas_ta as ta
36+
import pandas_ta_classic as ta
3737

3838
from .timestamps import normalize_dates
3939

@@ -69,10 +69,10 @@ def _groupby_symbol(
6969

7070
def _compute_rsi(prices: pd.Series, period: int) -> pd.Series:
7171
"""
72-
Compute RSI for a price series using pandas_ta.
72+
Compute RSI for a price series using pandas_ta_classic.
7373
7474
Kept as a named function for test compatibility. Uses Wilder smoothing
75-
(RMA) internally via pandas_ta, which is equivalent to the previous
75+
(RMA) internally via pandas_ta_classic, which is equivalent to the previous
7676
hand-rolled EWM implementation.
7777
7878
Args:
@@ -81,7 +81,7 @@ def _compute_rsi(prices: pd.Series, period: int) -> pd.Series:
8181
8282
Returns:
8383
Series of RSI values (0-100), with NaN for the first `period` entries.
84-
Returns a NaN series if pandas_ta cannot compute (insufficient data).
84+
Returns a NaN series if pandas_ta_classic cannot compute (insufficient data).
8585
"""
8686
result = ta.rsi(prices, length=period)
8787
if result is None:
@@ -310,14 +310,14 @@ def _bb_signal(length: int, std: float, above: bool) -> SignalFunc:
310310
above: True → price > upper band; False → price < lower band
311311
"""
312312
length, std = int(length), float(std)
313-
# pandas_ta names BB columns as BBU_{length}_{std}_{std} where std is a float.
314-
# The float() cast above ensures we match this format (e.g. 2 -> 2.0 -> "BBU_20_2.0_2.0").
313+
# pandas_ta_classic names BB columns as BBU_{length}_{std} where std is a float.
314+
# The float() cast above ensures we match this format (e.g. 2 -> 2.0 -> "BBU_20_2.0").
315315
if above:
316-
band_col = f"BBU_{length}_{std}_{std}"
316+
band_col = f"BBU_{length}_{std}"
317317
fill_val = float("inf")
318318
cmp = operator.gt
319319
else:
320-
band_col = f"BBL_{length}_{std}_{std}"
320+
band_col = f"BBL_{length}_{std}"
321321
fill_val = float("-inf")
322322
cmp = operator.lt
323323

optopsy/ui/tools/_indicators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import Any
1111

1212
import pandas as pd
13-
import pandas_ta as ta
13+
import pandas_ta_classic as ta
1414

1515
# ---------------------------------------------------------------------------
1616
# Indicator classification
@@ -120,7 +120,7 @@ def add_bbands_traces(
120120
bb = ta.bbands(close, length=period, std=std) # type: ignore[arg-type]
121121
if bb is None or bb.empty:
122122
return
123-
# pandas_ta column names vary (e.g. BBL_20_2.0 vs BBL_20_2.0_2.0)
123+
# pandas_ta_classic column names use format: BBL_{length}_{std} (e.g. BBL_20_2.0)
124124
upper_col = next(c for c in bb.columns if c.startswith("BBU_"))
125125
lower_col = next(c for c in bb.columns if c.startswith("BBL_"))
126126
mid_col = next(c for c in bb.columns if c.startswith("BBM_"))

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies = [
1919
"pandas",
2020
"numpy",
2121
"tabulate>=0.9.0,<1.0.0",
22-
"pandas-ta>=0.4.67b0",
22+
"pandas-ta-classic>=0.3.59",
2323
"empyrical-reloaded>=0.5.7",
2424
"pytz",
2525
"pydantic>=2.0,<3.0",
@@ -74,7 +74,7 @@ exclude = ["optopsy/ui/"]
7474

7575
[tool.pytest.ini_options]
7676
addopts = "--cov=optopsy --cov-report=term-missing"
77-
filterwarnings = ["ignore::DeprecationWarning:pandas_ta"]
77+
filterwarnings = ["ignore::DeprecationWarning:pandas_ta_classic"]
7878

7979
[dependency-groups]
8080
dev = [

tests/conftest.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -457,12 +457,12 @@ def stock_data_long_history():
457457
- Bars 140–199: recovery from ~174 → ~227 (SMA crossover, RSI > 70)
458458
459459
The bounce at bars 41–42 creates a gap where rsi_below(14,30) resets,
460-
so sustained(rsi_below(14,30), days=3) rejects bar 53 (RSI just crossed
460+
so sustained(rsi_below(14,30), days=3) rejects bar 55 (RSI just crossed
461461
back below 30 for <3 bars) but accepts bar 30 (deep in the streak).
462462
463463
Key bar values:
464464
bar 30: RSI=0, sma_above(20)=False, rsi_below(14,30)=True, sustained(3)=True
465-
bar 53: RSI≈29.8, sma_above(20)=False, rsi_below(14,30)=True, sustained(3)=False
465+
bar 55: RSI≈27.1, sma_above(20)=False, rsi_below(14,30)=True, sustained(3)=False
466466
bar 100: RSI≈14, rsi_above(14,70)=False (exit signal rejects exp-A)
467467
bar 160: RSI≈99.8, sma_above(20)=True, rsi_below(14,30)=False
468468
bar 170: RSI≈99.9, sma_above(20)=True, rsi_below(14,30)=False
@@ -505,7 +505,7 @@ def option_data_with_stock(stock_data_long_history):
505505
506506
Entry dates (4):
507507
- Bar 30 (decline, RSI=0, sma=False)
508-
- Bar 53 (post-bounce, RSI≈29.8, sma=False — sustained(3) rejects this)
508+
- Bar 55 (post-bounce, RSI≈27.1, sma=False — sustained(3) rejects this)
509509
- Bar 160 (recovery, RSI≈99.8, sma=True)
510510
- Bar 170 (recovery, RSI≈99.9, sma=True)
511511
@@ -514,11 +514,11 @@ def option_data_with_stock(stock_data_long_history):
514514
- Exp B = bar 195 (recovery, RSI≈100 → rsi_above(14,70) keeps)
515515
516516
Recovery entries (bars 160, 170) are AFTER exp-A so they only match
517-
exp-B. Decline entries (bars 30, 53) can match both.
517+
exp-B. Decline entries (bars 30, 55) can match both.
518518
519519
Baseline row count for long_calls: 18
520520
- Bar 30: 3 calls × 2 exps = 6
521-
- Bar 53: 3 calls × 2 exps = 6
521+
- Bar 55: 3 calls × 2 exps = 6
522522
- Bar 160: 3 calls × 1 exp (B only) = 3
523523
- Bar 170: 3 calls × 1 exp (B only) = 3
524524
"""
@@ -528,7 +528,7 @@ def option_data_with_stock(stock_data_long_history):
528528

529529
# Entry dates
530530
entry_decline_1 = pd.Timestamp(dates[30]) # deep in decline
531-
entry_decline_2 = pd.Timestamp(dates[53]) # post-bounce, RSI just < 30
531+
entry_decline_2 = pd.Timestamp(dates[55]) # post-bounce, RSI just < 30
532532
entry_recovery_1 = pd.Timestamp(dates[160]) # recovery phase
533533
entry_recovery_2 = pd.Timestamp(dates[170]) # recovery phase
534534
exit_date_a = pd.Timestamp(dates[100]) # flat phase, RSI < 70

tests/test_signals.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,11 +1560,11 @@ class TestTASignalE2E:
15601560
15611561
Key entry-bar properties:
15621562
bar 30: RSI=0, sma_above=False, rsi_below=True, sustained(3)=True
1563-
bar 53: RSI≈29.8, sma_above=False, rsi_below=True, sustained(3)=False
1563+
bar 55: RSI≈27.1, sma_above=False, rsi_below=True, sustained(3)=False
15641564
bar 160: RSI≈99.8, sma_above=True, rsi_below=False, atr_ohlcv=True, atr_close=False
15651565
bar 170: RSI≈99.9, sma_above=True, rsi_below=False, atr_ohlcv=True, atr_close=True
15661566
1567-
Decline entries (30, 53) match both exps → 3 calls × 2 exps = 6 each.
1567+
Decline entries (30, 55) match both exps → 3 calls × 2 exps = 6 each.
15681568
Recovery entries (160, 170) are after exp-A → 3 calls × 1 exp = 3 each.
15691569
Baseline: 6 + 6 + 3 + 3 = 18 call rows.
15701570
@@ -1578,7 +1578,7 @@ def _get_entry_dates(self, stock_data_long_history):
15781578
return {
15791579
"decline": {
15801580
pd.Timestamp(dates[30]),
1581-
pd.Timestamp(dates[53]),
1581+
pd.Timestamp(dates[55]),
15821582
},
15831583
"recovery": {
15841584
pd.Timestamp(dates[160]),
@@ -1754,10 +1754,10 @@ def test_entry_and_exit_dates_both_filter(
17541754
def test_sustained_ta_signal_with_apply_signal(
17551755
self, option_data_with_stock, stock_data_long_history
17561756
):
1757-
"""sustained(rsi_below(14, 30), days=3) must reject bar 53.
1757+
"""sustained(rsi_below(14, 30), days=3) must reject bar 55.
17581758
1759-
The bounce at bars 41-42 resets the RSI streak. At bar 53,
1760-
RSI just crossed back below 30 for <3 consecutive bars, so
1759+
The bounce at bars 41-42 resets the RSI streak. At bar 55,
1760+
RSI has crossed back below 30 for only 2 consecutive bars, so
17611761
sustained(days=3) rejects it. Bar 30 is deep in the streak
17621762
(30+ consecutive bars with RSI=0) and survives.
17631763
@@ -1777,7 +1777,7 @@ def test_sustained_ta_signal_with_apply_signal(
17771777
)
17781778
assert len(results_plain) > 0
17791779

1780-
# sustained(days=3) rejects bar 53 (streak < 3 bars)
1780+
# sustained(days=3) rejects bar 55 (streak < 3 bars)
17811781
entry_dates_sust = apply_signal(
17821782
stock_data_long_history, sustained(rsi_below(14, 30), days=3)
17831783
)

0 commit comments

Comments
 (0)