Skip to content

Commit 3b294bf

Browse files
committed
feat: Multi-timeframe resonance factor from a-share-signal
Adds MultiTimeframeResonanceFactor — checks trend alignment across daily/weekly/monthly timeframes. Score [0, 1]: 1.0 = all 3 timeframes align (strong trend) 0.7 = 2 agree, 1 flat 0.3 = weak agreement 0.0 = timeframes conflict (choppy) Inspired by a-share-signal's 三周期共振 methodology. 26 total registered factors (22 tech + 4 fundamental). 1205 passed (all existing)
1 parent 23147ee commit 3b294bf

3 files changed

Lines changed: 69 additions & 1 deletion

File tree

config/default.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ factors:
3737
ma_convergence: {enabled: true}
3838
breakout_proximity: {enabled: true, params: {period: 20}}
3939
pure_volatility: {enabled: true, params: {window: 20, ar_lags: 30}}
40+
mtf_resonance: {enabled: true}
4041

4142
# Fundamental factors
4243
fundamental:

config/schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class FactorsConfig:
169169
"momentum_1m", "momentum_3m", "momentum_6m", "momentum_12m",
170170
"volatility_20d", "volatility_60d",
171171
"turnover_20d", "rsi_14d", "amplitude_20d", "macd",
172-
"efficiency_ratio", "breakout_ignition", "trend_stage", "ma_convergence", "breakout_proximity", "pure_volatility",
172+
"efficiency_ratio", "breakout_ignition", "trend_stage", "ma_convergence", "breakout_proximity", "pure_volatility", "mtf_resonance",
173173
"kmid", "klen", "kup", "klow", "ksft",
174174
)
175175
enabled_fundamentals: tuple[str, ...] = (

factors/technical.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,72 @@ def _rolling_ivol(x):
629629

630630
return result
631631

632+
633+
634+
# ---------------------------------------------------------------------------
635+
# Multi-timeframe resonance factor (inspired by a-share-signal)
636+
# ---------------------------------------------------------------------------
637+
638+
639+
class MultiTimeframeResonanceFactor(BaseFactor):
640+
"""Multi-timeframe resonance: trend alignment across daily/weekly/monthly.
641+
642+
Checks whether price trends across 3 timeframes are aligned.
643+
High resonance = daily, weekly, and monthly trends all point same direction.
644+
Low resonance = timeframes conflict, choppy price action expected.
645+
646+
Score [0, 1]:
647+
1.0 = perfect alignment (all 3 timeframes bullish or bearish)
648+
0.0 = complete conflict (daily and monthly opposite)
649+
"""
650+
category = FactorCategory.TECHNICAL
651+
652+
def __init__(self, name: str = "mtf_resonance"):
653+
super().__init__()
654+
self._name = name
655+
656+
@property
657+
def name(self) -> str:
658+
return self._name
659+
660+
def compute(self, prices, **kwargs):
661+
# Daily trend: 20-day MA slope
662+
daily_ma = prices.rolling(20).mean()
663+
daily_slope = (daily_ma - daily_ma.shift(5)) / daily_ma.shift(5).replace(0, float("nan"))
664+
665+
# Weekly trend: approximate via 60-day MA slope
666+
weekly_ma = prices.rolling(60).mean()
667+
weekly_slope = (weekly_ma - weekly_ma.shift(15)) / weekly_ma.shift(15).replace(0, float("nan"))
668+
669+
# Monthly trend: approximate via 120-day MA slope
670+
monthly_ma = prices.rolling(120).mean()
671+
monthly_slope = (monthly_ma - monthly_ma.shift(30)) / monthly_ma.shift(30).replace(0, float("nan"))
672+
673+
# Direction: 1 = bullish, -1 = bearish, 0 = flat
674+
def _direction(slope: pd.DataFrame) -> pd.DataFrame:
675+
d = pd.DataFrame(0.0, index=slope.index, columns=slope.columns)
676+
d[slope > 0.001] = 1.0
677+
d[slope < -0.001] = -1.0
678+
return d
679+
680+
d_dir = _direction(daily_slope)
681+
w_dir = _direction(weekly_slope)
682+
m_dir = _direction(monthly_slope)
683+
684+
# Resonance: how many timeframes agree
685+
agreement = d_dir + w_dir + m_dir
686+
687+
# Map to [0, 1] score
688+
# 3 or -3 = all agree = 1.0
689+
# 1 or -1 = 2 agree, 1 flat = 0.7
690+
# 0 = conflict = 0.0
691+
score = pd.DataFrame(0.0, index=prices.index, columns=prices.columns)
692+
score[agreement.abs() >= 3] = 1.0
693+
score[agreement.abs() == 2] = 0.7
694+
score[agreement.abs() == 1] = 0.3
695+
696+
return score
697+
632698
def register_all():
633699
registry = get_registry()
634700
for cls in [
@@ -643,5 +709,6 @@ def register_all():
643709
MAConvergenceFactor,
644710
BreakoutProximityFactor,
645711
PureVolatilityFactor,
712+
MultiTimeframeResonanceFactor,
646713
]:
647714
registry.register(cls)

0 commit comments

Comments
 (0)