Initial commit: cc-cursor 全链路量化研究平台
7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
振幅因子。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class AmplitudeFactor(BaseFactor):
|
||||
"""N 日均振幅 = mean((high - low) / close, N) * 100"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 5):
|
||||
self.period = period
|
||||
self.name = f"amplitude_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
daily_amp = (df["high"] - df["low"]) / df["close"].replace(0, float("nan")) * 100
|
||||
return daily_amp.rolling(window=self.period, min_periods=self.period).mean()
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["high", "low", "close"]
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
ATR 平均真实波幅因子。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class ATRFactor(BaseFactor):
|
||||
"""Average True Range,衡量波动性。"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 14):
|
||||
self.period = period
|
||||
self.name = f"atr_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
high, low, close = df["high"], df["low"], df["close"]
|
||||
prev_close = close.shift(1)
|
||||
tr = pd.concat([
|
||||
(high - low).abs(),
|
||||
(high - prev_close).abs(),
|
||||
(low - prev_close).abs(),
|
||||
], axis=1).max(axis=1)
|
||||
return tr.ewm(span=self.period, min_periods=self.period).mean()
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["high", "low", "close"]
|
||||
|
||||
|
||||
class ATRRatioFactor(BaseFactor):
|
||||
"""ATR / close 归一化,便于跨股票比较。"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 14):
|
||||
self.period = period
|
||||
self.name = f"atr_ratio_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
atr = ATRFactor(period=self.period).calculate(df)
|
||||
return atr / df["close"].replace(0, float("nan")) * 100
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["high", "low", "close"]
|
||||
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
布林带因子:价格在布林带中的位置。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class BollingerFactor(BaseFactor):
|
||||
"""
|
||||
布林带位置 = (close - middle) / (upper - lower)
|
||||
|
||||
值在 0~1 之间:接近 0 表示在下轨,接近 1 表示在上轨。
|
||||
"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 20, std: float = 2.0):
|
||||
self.period = period
|
||||
self.std = std
|
||||
self.name = f"boll_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
middle = df["close"].rolling(window=self.period, min_periods=self.period).mean()
|
||||
std = df["close"].rolling(window=self.period, min_periods=self.period).std()
|
||||
upper = middle + self.std * std
|
||||
lower = middle - self.std * std
|
||||
band_width = upper - lower
|
||||
return ((df["close"] - lower) / band_width.replace(0, float("nan"))).clip(0, 1)
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
|
||||
|
||||
class BollingerWidthFactor(BaseFactor):
|
||||
"""布林带宽度 = (upper - lower) / middle * 100"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 20, std: float = 2.0):
|
||||
self.period = period
|
||||
self.std = std
|
||||
self.name = f"boll_width_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
middle = df["close"].rolling(window=self.period, min_periods=self.period).mean()
|
||||
std = df["close"].rolling(window=self.period, min_periods=self.period).std()
|
||||
band_width = 2 * self.std * std
|
||||
return band_width / middle.replace(0, float("nan")) * 100
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
均线交叉因子。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class MACrossFactor(BaseFactor):
|
||||
"""
|
||||
均线交叉信号。
|
||||
|
||||
返回:fast_ma / slow_ma - 1,正值表示短期均线在上方。
|
||||
"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, fast: int = 5, slow: int = 20):
|
||||
self.fast = fast
|
||||
self.slow = slow
|
||||
self.name = f"ma_cross_{fast}_{slow}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
ma_fast = df["close"].rolling(window=self.fast, min_periods=self.fast).mean()
|
||||
ma_slow = df["close"].rolling(window=self.slow, min_periods=self.slow).mean()
|
||||
return (ma_fast / ma_slow.replace(0, float("nan"))) - 1
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
|
||||
|
||||
class MADeviationFactor(BaseFactor):
|
||||
"""
|
||||
价格偏离均线程度 = (close - ma) / ma * 100
|
||||
"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 20):
|
||||
self.period = period
|
||||
self.name = f"ma_dev_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
ma = df["close"].rolling(window=self.period, min_periods=self.period).mean()
|
||||
return (df["close"] - ma) / ma.replace(0, float("nan")) * 100
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
MACD 因子。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class MACDFactor(BaseFactor):
|
||||
"""
|
||||
MACD 系列因子。
|
||||
|
||||
返回 DIF/DEA/HIST 三个值。使用 calculate() 返回 HIST(柱),
|
||||
单独方法获取 DIF/DEA。
|
||||
"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, fast: int = 12, slow: int = 26, signal: int = 9):
|
||||
self.fast = fast
|
||||
self.slow = slow
|
||||
self.signal = signal
|
||||
self.name = f"macd_{fast}_{slow}_{signal}"
|
||||
|
||||
def _ema(self, series: pd.Series, span: int) -> pd.Series:
|
||||
return series.ewm(span=span, min_periods=span).mean()
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
"""返回 MACD 柱(DIF - DEA)。"""
|
||||
ema_fast = self._ema(df["close"], self.fast)
|
||||
ema_slow = self._ema(df["close"], self.slow)
|
||||
dif = ema_fast - ema_slow
|
||||
dea = self._ema(dif, self.signal)
|
||||
return dif - dea
|
||||
|
||||
def dif(self, df: pd.DataFrame) -> pd.Series:
|
||||
ema_fast = self._ema(df["close"], self.fast)
|
||||
ema_slow = self._ema(df["close"], self.slow)
|
||||
return ema_fast - ema_slow
|
||||
|
||||
def dea(self, df: pd.DataFrame) -> pd.Series:
|
||||
dif = self.dif(df)
|
||||
return self._ema(dif, self.signal)
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
动量因子:N 日收益率。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class MomentumFactor(BaseFactor):
|
||||
"""N 日价格动量 = (close_t - close_{t-N}) / close_{t-N} * 100"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 20):
|
||||
self.period = period
|
||||
self.name = f"momentum_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
return df["close"].pct_change(periods=self.period) * 100
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
RSI 相对强弱因子。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class RSIFactor(BaseFactor):
|
||||
"""Wilder's RSI = 100 - 100 / (1 + RS), RS = avg_gain / avg_loss"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 14):
|
||||
self.period = period
|
||||
self.name = f"rsi_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
delta = df["close"].diff()
|
||||
gain = delta.clip(lower=0)
|
||||
loss = (-delta).clip(lower=0)
|
||||
avg_gain = gain.ewm(span=self.period, min_periods=self.period).mean()
|
||||
avg_loss = loss.ewm(span=self.period, min_periods=self.period).mean()
|
||||
rs = avg_gain / avg_loss.replace(0, float("nan"))
|
||||
return 100 - 100 / (1 + rs)
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
换手率因子。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class TurnoverFactor(BaseFactor):
|
||||
"""N 日均换手率。"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 5):
|
||||
self.period = period
|
||||
self.name = f"turnover_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
return df["turnover_rate"].rolling(window=self.period, min_periods=self.period).mean()
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["turnover_rate"]
|
||||
|
||||
|
||||
class TurnoverChangeFactor(BaseFactor):
|
||||
"""换手率变化 = 当日换手率 / N 日均换手率。"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 5):
|
||||
self.period = period
|
||||
self.name = f"turnover_chg_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
avg = df["turnover_rate"].rolling(window=self.period, min_periods=self.period).mean()
|
||||
return df["turnover_rate"] / avg.replace(0, float("nan"))
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["turnover_rate"]
|
||||
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
波动率因子。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class VolatilityFactor(BaseFactor):
|
||||
"""N 日年化波动率 = std(daily_return, N) * sqrt(252) * 100"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 20):
|
||||
self.period = period
|
||||
self.name = f"volatility_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
daily_ret = df["close"].pct_change()
|
||||
return daily_ret.rolling(window=self.period, min_periods=self.period).std() * (252 ** 0.5) * 100
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
|
||||
|
||||
class DownsideVolatilityFactor(BaseFactor):
|
||||
"""下行波动率:只计算负收益的标准差。"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 20):
|
||||
self.period = period
|
||||
self.name = f"down_vol_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
daily_ret = df["close"].pct_change()
|
||||
downside = daily_ret.clip(upper=0)
|
||||
return downside.rolling(window=self.period, min_periods=self.period).std() * (252 ** 0.5) * 100
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["close"]
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
量价因子:量比 / 成交量变化率。
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from factors.base import BaseFactor
|
||||
|
||||
|
||||
class VolumeFactor(BaseFactor):
|
||||
"""N 日均量比 = vol / mean(vol, N)"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 5):
|
||||
self.period = period
|
||||
self.name = f"vol_ratio_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
avg_vol = df["vol"].rolling(window=self.period, min_periods=self.period).mean()
|
||||
return df["vol"] / avg_vol.replace(0, float("nan"))
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["vol"]
|
||||
|
||||
|
||||
class VolumeChangeFactor(BaseFactor):
|
||||
"""成交量 N 日变化率"""
|
||||
|
||||
category = "technical"
|
||||
|
||||
def __init__(self, period: int = 5):
|
||||
self.period = period
|
||||
self.name = f"vol_chg_{period}"
|
||||
|
||||
def calculate(self, df: pd.DataFrame) -> pd.Series:
|
||||
return df["vol"].pct_change(periods=self.period) * 100
|
||||
|
||||
def get_required_columns(self) -> list[str]:
|
||||
return ["vol"]
|
||||
Reference in New Issue
Block a user