Skip to content

Commit ae56652

Browse files
committed
ta_regime bot, buxfices
1 parent 953bcde commit ae56652

9 files changed

Lines changed: 534 additions & 15 deletions

File tree

docs/examples/example-bots.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ AI-driven portfolio research and rebalancing with tools. The main LLM uses tools
7979

8080
**Pattern**: Override `get_ai_tools()` for custom tools; use main LLM for tool flow, cheap LLM for output validation and fallback
8181

82+
## ta_regime_bot.py (TARegimeAdaptiveBot)
83+
84+
Single-asset bot that uses **only historic OHLCV and TA** (no Fear & Greed). It classifies regime as **trend** vs **mean reversion** via a Hurst-style proxy (lag-1 autocorrelation of returns), then applies ADX/MACD/EMA in trend regime and RSI/Bollinger BBP (and optional z-score) in mean-reversion regime. All decision logic lives in `utils.ta_regime`; the bot only fetches data and delegates.
85+
86+
**Pattern**: Minimal bot; reusable logic in `utils.ta_regime`; `decisionFunction(row)` calls `ta_regime_decision(row, self.data, **self._ta_params)`.
87+
88+
For the mathematical and quant concepts (Hurst, R/S, variance ratio, z-score, Hilbert/Ehlers, entropy) and source links, see **[TA Regime Bot: Mathematical and Quant Concepts](ta-regime-bot.md)**.
89+
8290
## Learning from Examples
8391

8492
Each example demonstrates:

docs/examples/ta-regime-bot.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# TA Regime-Adaptive Bot: Mathematical and Quant Concepts
2+
3+
The **TARegimeAdaptiveBot** is a single-asset bot that uses **only historic OHLCV and technical indicators** (no Fear & Greed or other external APIs). It classifies the market into two regimes—**trend** vs **mean reversion**—using a Hurst-style proxy, then applies regime-specific rules (ADX/MACD/EMA in trend; RSI/Bollinger BBP and optional z-score in mean reversion). All logic lives in `tradingbot.utils.ta_regime`; the bot itself only fetches data and delegates.
4+
5+
This page explains the mathematical and quantitative concepts behind the strategy and links to primary sources.
6+
7+
---
8+
9+
## 1. Regime: Trend vs Mean Reversion
10+
11+
Markets alternate between periods where:
12+
13+
- **Trend (persistence)**: Returns tend to follow the same direction; momentum strategies work better.
14+
- **Mean reversion**: Prices tend to revert to a local mean; oversold/overbought signals work better.
15+
16+
Choosing the wrong strategy for the current regime can hurt performance. The bot therefore **classifies the regime from historic returns** and then applies the matching rule set.
17+
18+
---
19+
20+
## 2. Hurst Exponent and Long-Memory
21+
22+
The **Hurst exponent** \(H\) is a measure of long-range dependence in a time series. It was developed by Harold Edwin Hurst for hydrology (e.g. Nile River flows) and is widely used in finance to distinguish trending from mean-reverting behavior.
23+
24+
### Interpretation
25+
26+
- **\(H = 0.5\)**: Random walk (no long-term memory); past returns do not predict future direction.
27+
- **\(H > 0.5\)** (up to 1): **Persistence** — positive autocorrelation; highs tend to follow highs, lows tend to follow lows → suitable for **trend-following**.
28+
- **\(H < 0.5\)** (down to 0): **Mean reversion** — negative autocorrelation; highs tend to be followed by lows → suitable for **mean-reversion** strategies.
29+
30+
Research shows that **dynamically selecting** strategy (trend vs mean reversion) based on a Hurst-type classification can improve returns, though with higher variability. See for example:
31+
32+
- **Macrosynergy**: [Detecting trends and mean reversion with the Hurst exponent](https://macrosynergy.com/research/detecting-trends-and-mean-reversion-with-the-hurst-exponent/) — application to strategy selection.
33+
- **CFA Institute**: [Rescaled Range Analysis: Detecting Persistence, Randomness, or Mean Reversion](https://blogs.cfainstitute.org/investor/2013/01/30/rescaled-range-analysis-a-method-for-detecting-persistence-randomness-or-mean-reversion-in-financial-markets/) — R/S method and interpretation.
34+
- **Wikipedia**: [Hurst exponent](https://en.wikipedia.org/wiki/Hurst_exponent) — definition and estimation methods.
35+
36+
### How the Hurst exponent is usually estimated: R/S analysis
37+
38+
The classical approach is **rescaled range (R/S) analysis**:
39+
40+
1. For a window of returns, compute the **range** \(R\) of cumulative deviations from the mean over sub-intervals of length \(\tau\).
41+
2. Compute the **standard deviation** \(S\) of returns over the same sub-intervals.
42+
3. The ratio \(R/S\) scales with \(\tau^H\). Regressing \(\log(R/S)\) on \(\log(\tau)\) yields an estimate of \(H\).
43+
44+
This is described in the CFA link above and in many quant finance texts.
45+
46+
### What this codebase uses: a simple proxy (lag-1 autocorrelation)
47+
48+
Computing the full R/S Hurst is more involved and sensitive to sample size. The bot uses a **lightweight proxy** that captures the same idea:
49+
50+
- **Lag-1 autocorrelation** \(\rho_1\) of returns over a rolling window:
51+
- \(\rho_1 > 0\) → persistence (trend-like).
52+
- \(\rho_1 < 0\) → mean reversion.
53+
- The proxy maps \(\rho_1 \in [-1, 1]\) to a value in \([0, 1]\) via \(\frac{1 + \rho_1}{2}\), so that **0.5** corresponds to no autocorrelation (random walk), **> 0.5** to trend, **< 0.5** to mean reversion. A threshold (e.g. 0.5) then classifies the regime.
54+
55+
So the *concept* is Hurst (trend vs mean reversion); the *implementation* is autocorrelation-based for simplicity and stability on typical bar counts.
56+
57+
---
58+
59+
## 3. Variance Ratio (alternative Hurst-style proxy)
60+
61+
The **variance ratio** is another way to detect persistence vs mean reversion:
62+
63+
\[
64+
VR(n) = \frac{\operatorname{Var}(r_t + r_{t-1} + \cdots + r_{t-n+1})}{n \cdot \operatorname{Var}(r_t)}
65+
\]
66+
67+
- **\(VR > 1\)**: Persistence (trend-like).
68+
- **\(VR < 1\)**: Mean reversion.
69+
70+
It is related to the Hurst exponent and is often used in empirical finance. The bot does not implement it; the lag-1 autocorrelation proxy is used instead. For variance-ratio tests and applications, see the CFA and Macrosynergy references above.
71+
72+
---
73+
74+
## 4. Z-Score for Mean Reversion
75+
76+
In **mean-reversion** regimes, a common idea is to treat price (or an indicator) as reverting to a local mean. The **z-score** measures how many standard deviations the current value is from its recent mean:
77+
78+
\[
79+
z = \frac{x_t - \mu_w}{\sigma_w}
80+
\]
81+
82+
where \(\mu_w\) and \(\sigma_w\) are the mean and standard deviation over a rolling window of length \(w\). Large negative \(z\) (e.g. \(z < -2\)) suggests oversold; large positive \(z\) suggests overbought. The bot optionally uses this to **strengthen** mean-reversion entries (e.g. only buy when RSI/BBP are oversold *and* z-score is sufficiently negative).
83+
84+
---
85+
86+
## 5. Technical Indicators Used (trend vs mean reversion)
87+
88+
Once the regime is classified, the bot uses only **historic TA** from the `ta` library (no external APIs):
89+
90+
- **Trend regime**: ADX (strength of trend), MACD vs signal (direction), and optionally EMA fast vs slow (alignment). Buy when ADX > threshold, MACD > signal (and EMAs aligned if required); sell when MACD < signal (and EMAs aligned for sell).
91+
- **Mean-reversion regime**: RSI (oversold/overbought), Bollinger Band position BBP (low/high), and optionally the z-score of price. Buy when RSI and BBP are in oversold territory (and z below a negative threshold if enabled); sell when overbought (and z above a positive threshold if enabled).
92+
93+
Evidence that **combining** indicators (e.g. RSI, Bollinger Bands, ADX, MACD) improves over single-indicator rules is discussed in multi-indicator and algorithmic trading studies (e.g. [Enhancing Trading Strategies: Multi-indicator Analysis](https://link.springer.com/article/10.1007/s10614-024-10669-3); [Empirical Study of Technical Indicators](https://escholarship.org/uc/item/5tq0q6cq)).
94+
95+
---
96+
97+
## 6. Further Reading: Hilbert Transform and Ehlers MESA
98+
99+
**John Ehlers** introduced cycle-oriented tools that use the **Hilbert transform** to estimate dominant cycle period and to build adaptive moving averages (e.g. MESA Adaptive Moving Average, MAMA). The idea is to reduce lag and adapt to market rhythm. These are more advanced than the current bot’s logic; they are mentioned here for context and possible future extension.
100+
101+
- **Traders.com**: [On Lag, Signal Processing, and the Hilbert Transform](https://traders.com/documentation/feedbk_docs/2000/03/Abstracts_new/Ehlers/Ehlers.html).
102+
- **MESA Adaptive Moving Average**: [September 2001](https://traders.com/documentation/feedbk_docs/2001/09/Abstracts_new/Ehlers/ehlers.html) — adaptive alpha from detected cycle.
103+
104+
---
105+
106+
## 7. Further Reading: Entropy and Regularity
107+
108+
**Approximate entropy** and **sample entropy** measure regularity/unpredictability in a time series. Lower entropy can indicate more structure (e.g. during stress); they have been used in finance as irregularity measures. The bot does not implement them; they are listed as optional “advanced” concepts.
109+
110+
- **Approximate entropy in finance**: [Approximate Entropy as an Irregularity Measure for Financial Data](https://ideas.repec.org/a/taf/emetrv/v27y2008i4-6p329-362.html).
111+
- **Wikipedia**: [Approximate entropy](https://en.wikipedia.org/wiki/Approximate_entropy).
112+
113+
---
114+
115+
## 8. Summary and Code Entry Points
116+
117+
| Concept | Role in bot | Implemented in |
118+
|--------------------|-------------------------------------|----------------------------------------|
119+
| Hurst-style regime | Trend vs mean reversion | `hurst_proxy_from_returns` (ACF proxy) |
120+
| Regime classification | Threshold on proxy | `classify_ta_regime` |
121+
| Z-score | Optional mean-reversion filter | `rolling_zscore` |
122+
| TA rules | ADX/MACD/EMA (trend); RSI/BBP (MR) | `ta_regime_decision` |
123+
124+
- **Utils**: `tradingbot.utils.ta_regime` — pure functions, no Bot/db.
125+
- **Bot**: `tradingbot.ta_regime_bot.TARegimeAdaptiveBot` — fetches data, calls `ta_regime_decision(row, self.data, **self._ta_params)`.
126+
127+
For a minimal code example, see [Example Bots](example-bots.md). For backtesting and hyperparameter tuning, see [Hyperparameter Tuning](../api/hyperparameter-tuning.md) and [Backtest](../api/backtest.md).

helm/tradingbots/values.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,7 @@ bots:
172172
schedule: "20 20 * * 3" # Weekly on Wednesday at 8:20 PM
173173

174174
- name: earningsinsidertiltbot
175-
schedule: "30 20 * * 3" # Weekly on Wednesday at 8:30 PM
175+
schedule: "30 20 * * 3" # Weekly on Wednesday at 8:30 PM
176+
177+
- name: ta_regime_bot
178+
schedule: "20 17 * * 4" # weekly 5:20 PM, Thursday

tradingbot/ta_regime_bot.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
TA-only regime-adaptive bot: historic data only (no Fear & Greed).
3+
4+
Regime and signal logic live in utils.ta_regime; this bot only fetches data
5+
and calls ta_regime_decision.
6+
"""
7+
8+
from utils.core import Bot
9+
from utils.ta_regime import ta_regime_decision
10+
11+
12+
class TARegimeAdaptiveBot(Bot):
13+
"""
14+
Single-asset bot that uses a Hurst-style regime (trend vs mean-reversion)
15+
and TA indicators from historic OHLCV only. All decision logic is in
16+
utils.ta_regime; the bot delegates decisionFunction to ta_regime_decision.
17+
"""
18+
19+
# Grid centered around best params (from prior tuning: ~12.58% return, 2.65 Sharpe)
20+
param_grid = {
21+
"hurst_window": [40, 50, 60],
22+
"hurst_trend_threshold": [0.44, 0.46, 0.48],
23+
"adx_threshold": [14, 16, 18],
24+
"rsi_oversold": [34, 36, 38],
25+
"rsi_overbought": [64, 66, 68],
26+
"bbp_low": [0.0, 0.05, 0.1],
27+
"bbp_high": [0.8, 0.85, 0.9],
28+
"zscore_window": [0, 15, 20],
29+
"zscore_entry": [1.5, 1.75, 2.0],
30+
}
31+
32+
def __init__(
33+
self,
34+
symbol: str = "SPY",
35+
interval: str = "1d",
36+
period: str = "3mo",
37+
hurst_window: int = 50,
38+
hurst_trend_threshold: float = 0.46,
39+
adx_threshold: float = 16,
40+
rsi_oversold: float = 36,
41+
rsi_overbought: float = 66,
42+
bbp_low: float = 0.0,
43+
bbp_high: float = 0.8,
44+
zscore_window: int = 15,
45+
zscore_entry: float = 1.5,
46+
macd_confirm_trend: bool = True,
47+
**kwargs,
48+
):
49+
super().__init__(
50+
"TARegimeAdaptiveBot",
51+
symbol=symbol,
52+
interval=interval,
53+
period=period,
54+
hurst_window=hurst_window,
55+
hurst_trend_threshold=hurst_trend_threshold,
56+
adx_threshold=adx_threshold,
57+
rsi_oversold=rsi_oversold,
58+
rsi_overbought=rsi_overbought,
59+
bbp_low=bbp_low,
60+
bbp_high=bbp_high,
61+
zscore_window=zscore_window,
62+
zscore_entry=zscore_entry,
63+
macd_confirm_trend=macd_confirm_trend,
64+
**kwargs,
65+
)
66+
self._ta_params = {
67+
"hurst_window": hurst_window,
68+
"hurst_trend_threshold": hurst_trend_threshold,
69+
"adx_threshold": adx_threshold,
70+
"rsi_oversold": rsi_oversold,
71+
"rsi_overbought": rsi_overbought,
72+
"bbp_low": bbp_low,
73+
"bbp_high": bbp_high,
74+
"zscore_window": zscore_window,
75+
"zscore_entry": zscore_entry,
76+
"macd_confirm_trend": macd_confirm_trend,
77+
}
78+
79+
def decisionFunction(self, row):
80+
return ta_regime_decision(row, self.data, **self._ta_params)
81+
82+
def makeOneIteration(self) -> int:
83+
"""Fetch data, set self.data for decisionFunction, then run default buy/sell logic."""
84+
self.dbBot = self._bot_repository.create_or_get_bot(self.bot_name)
85+
data = self.getYFDataWithTA(
86+
saveToDB=True, interval=self.interval, period=self.period
87+
)
88+
self.data = data
89+
self.datasettings = (self.interval, self.period)
90+
decision = self.getLatestDecision(data)
91+
cash = self.dbBot.portfolio.get("USD", 0)
92+
holding = self.dbBot.portfolio.get(self.symbol, 0)
93+
if decision == 1 and cash > 0:
94+
self.buy(self.symbol)
95+
return 1
96+
if decision == -1 and holding > 0:
97+
self.sell(self.symbol)
98+
return -1
99+
return 0
100+
101+
102+
if __name__ == "__main__":
103+
bot = TARegimeAdaptiveBot()
104+
105+
# ============================================================
106+
# Backtesting with best parameters (max-sharpe)
107+
# ============================================================
108+
# hurst_window: 50
109+
# hurst_trend_threshold: 0.46
110+
# adx_threshold: 16
111+
# rsi_oversold: 36
112+
# rsi_overbought: 66
113+
# bbp_low: 0.0
114+
# bbp_high: 0.8
115+
# zscore_window: 15
116+
# zscore_entry: 1.5
117+
118+
# --- Backtest Results: TARegimeAdaptiveBot ---
119+
# Yearly Return: 12.58%
120+
# Buy & Hold Return: 16.32%
121+
# Outperformance vs B&H: -3.74%
122+
# Sharpe Ratio: 2.65
123+
# Number of Trades: 7
124+
# Max Drawdown: 2.62%
125+
# bot.local_development(objective="yearly_return", param_sample_ratio=.1) #
126+
bot.run()

tradingbot/utils/backtest.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ def backtest_bot(
9393
9494
Returns:
9595
Dictionary with keys:
96-
- yearly_return: Annualized return as decimal (e.g., 0.15 for 15%)
96+
- yearly_return: Strategy return over backtest period as decimal (e.g., 0.15 for 15%)
97+
- buy_hold_return: Buy-and-hold return over same period as decimal
9798
- sharpe_ratio: Sharpe ratio (annualized, assuming 252 trading days)
9899
- nrtrades: Total number of trades executed (buy + sell)
99100
- maxdrawdown: Maximum drawdown as decimal (e.g., 0.25 for 25%)
@@ -274,10 +275,24 @@ def backtest_bot(
274275
# Handle edge cases
275276
if not np.isfinite(maxdrawdown):
276277
maxdrawdown = 0.0
277-
278+
279+
# Buy-and-hold return (same period as backtest)
280+
close = data["close"].dropna()
281+
if len(close) < 2:
282+
buy_hold_return = 0.0
283+
else:
284+
first_close = float(close.iloc[0])
285+
last_close = float(close.iloc[-1])
286+
if first_close > 0 and np.isfinite(first_close) and np.isfinite(last_close):
287+
buy_hold_return = (last_close - first_close) / first_close
288+
else:
289+
buy_hold_return = 0.0
290+
buy_hold_return = float(buy_hold_return)
291+
278292
return {
279293
"yearly_return": float(yearly_return),
280294
"sharpe_ratio": float(sharpe_ratio),
281295
"nrtrades": int(nrtrades),
282296
"maxdrawdown": float(maxdrawdown),
297+
"buy_hold_return": buy_hold_return,
283298
}

tradingbot/utils/botclass.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ def local_optimize(
581581
objective: str = "sharpe_ratio",
582582
initial_capital: float = 10000.0,
583583
n_jobs: Optional[int] = None,
584+
param_sample_ratio: float = 1.0,
584585
) -> Dict[str, Any]:
585586
"""
586587
Local-only helper: run hyperparameter optimization for this bot's class.
@@ -593,6 +594,7 @@ def local_optimize(
593594
objective: Metric to maximize ("sharpe_ratio" or "yearly_return")
594595
initial_capital: Starting capital for backtests
595596
n_jobs: Number of parallel jobs (None = auto-detect)
597+
param_sample_ratio: Fraction of param combinations to test (0.0–1.0). 1.0 = all (default).
596598
597599
Returns:
598600
Full optimization results dictionary
@@ -621,6 +623,7 @@ def local_optimize(
621623
initial_capital=initial_capital,
622624
verbose=True,
623625
n_jobs=n_jobs,
626+
param_sample_ratio=param_sample_ratio,
624627
)
625628

626629
print("\n" + "=" * 60)
@@ -647,6 +650,8 @@ def local_backtest(self, initial_capital: float = 10000.0) -> Dict[str, Any]:
647650
results = backtest_bot(self, initial_capital=initial_capital)
648651
print(f"\n--- Backtest Results: {self.bot_name} ---")
649652
print(f"Yearly Return: {results['yearly_return']:.2%}")
653+
print(f"Buy & Hold Return: {results['buy_hold_return']:.2%}")
654+
print(f"Outperformance vs B&H: {(results['yearly_return'] - results['buy_hold_return']):+.2%}")
650655
print(f"Sharpe Ratio: {results['sharpe_ratio']:.2f}")
651656
print(f"Number of Trades: {results['nrtrades']}")
652657
print(f"Max Drawdown: {results['maxdrawdown']:.2%}")
@@ -658,6 +663,7 @@ def local_development(
658663
objective: str = "sharpe_ratio",
659664
initial_capital: float = 10000.0,
660665
n_jobs: Optional[int] = None,
666+
param_sample_ratio: float = 1.0,
661667
) -> Dict[str, Any]:
662668
"""
663669
Convenience wrapper for the typical local development workflow:
@@ -672,6 +678,8 @@ def local_development(
672678
objective: Metric to maximize ("sharpe_ratio" or "yearly_return")
673679
initial_capital: Starting capital for backtests
674680
n_jobs: Number of parallel jobs (None = auto-detect)
681+
param_sample_ratio: Fraction of param combinations to test (0.0–1.0). 1.0 = all (default).
682+
e.g. 0.2 = randomly test 20% of the grid.
675683
676684
Returns:
677685
Optimization results dictionary with 'best_params' and performance metrics
@@ -689,13 +697,18 @@ def local_development(
689697
objective=objective,
690698
initial_capital=initial_capital,
691699
n_jobs=n_jobs,
700+
param_sample_ratio=param_sample_ratio,
692701
)
693702

694703
# Step 2: Backtest with best parameters
695704
print("\n" + "=" * 60)
696705
print("Backtesting with best parameters...")
697706
print("=" * 60)
698-
best_bot = self.__class__(**opt_results["best_params"])
707+
best_params = opt_results["best_params"]
708+
for key, value in best_params.items():
709+
print(f" {key}: {value}")
710+
print()
711+
best_bot = self.__class__(**best_params)
699712
best_bot.local_backtest(initial_capital=initial_capital)
700713

701714
return opt_results

0 commit comments

Comments
 (0)