qka 2.5.5.dev2__tar.gz → 2.5.6__tar.gz
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.
- {qka-2.5.5.dev2 → qka-2.5.6}/PKG-INFO +1 -1
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/data.py +166 -18
- {qka-2.5.5.dev2 → qka-2.5.6}/.github/workflows/release.yml +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/.gitignore +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/.vscode/settings.json +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/CHANGELOG.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/LICENSE +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/README.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/pyproject.toml +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/__init__.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/cli.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/__init__.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/accessor.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/analysis.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/backtest.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/broker.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/indicator.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/report.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/simulate.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/sizing.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/strategy.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/core/tdx.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/utils/__init__.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/qka/utils/logger.py +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/SKILL.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/analysis.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/backtest.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/broker.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/data.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/sizing.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/skills/qka/references/strategy.md +0 -0
- {qka-2.5.5.dev2 → qka-2.5.6}/tools/generate_api_ref.py +0 -0
|
@@ -1017,7 +1017,9 @@ class Data():
|
|
|
1017
1017
|
parquet_path)
|
|
1018
1018
|
ddf['returns'] = ddf['close'].diff() / ddf['close'].shift(1)
|
|
1019
1019
|
ddf = self._drop_head_lazy(ddf)
|
|
1020
|
-
|
|
1020
|
+
# 🚨 因子不在这里算 —— 要等基准注入之后统一算(见下方 _apply_all_factors)。
|
|
1021
|
+
# 原因:内置因子和自定义因子没有本质区别,都可能是「相对基准」的,
|
|
1022
|
+
# 唯一合理的位置是「拿到基准之后」,而不是分两处、按两种时机各算一遍。
|
|
1021
1023
|
column_mapping = {col: f'{symbol}|{col}' for col in ddf.columns}
|
|
1022
1024
|
dfs.append(ddf.rename(columns=column_mapping))
|
|
1023
1025
|
|
|
@@ -1028,17 +1030,19 @@ class Data():
|
|
|
1028
1030
|
)
|
|
1029
1031
|
|
|
1030
1032
|
ddf = dd.concat(dfs, axis=1, join='outer')
|
|
1031
|
-
# 切片回用户请求的日期范围(去掉指标预热扩展部分)
|
|
1032
|
-
if start_date is not None and max_window > 0:
|
|
1033
|
-
cutoff = pd.Timestamp(start_date)
|
|
1034
|
-
ddf = ddf.loc[cutoff:]
|
|
1035
1033
|
|
|
1036
|
-
#
|
|
1034
|
+
# 基准数据(必须在算因子之前 —— 因子可能需要它)
|
|
1037
1035
|
if self.benchmark:
|
|
1038
1036
|
ddf = self._add_benchmark(ddf, self.target_dir, pq_filters)
|
|
1039
1037
|
|
|
1040
|
-
#
|
|
1041
|
-
ddf = self.
|
|
1038
|
+
# 所有因子,一处计算:内置 / ta / 自定义一视同仁
|
|
1039
|
+
ddf = self._apply_all_factors(ddf)
|
|
1040
|
+
|
|
1041
|
+
# 切片回用户请求的日期范围(去掉指标预热扩展部分)——
|
|
1042
|
+
# 必须放在算因子之后:因子要吃掉预热那段数据,切早了窗口就不够长。
|
|
1043
|
+
if start_date is not None and max_window > 0:
|
|
1044
|
+
cutoff = pd.Timestamp(start_date)
|
|
1045
|
+
ddf = ddf.loc[cutoff:]
|
|
1042
1046
|
|
|
1043
1047
|
return ddf
|
|
1044
1048
|
|
|
@@ -1079,21 +1083,18 @@ class Data():
|
|
|
1079
1083
|
# 补齐会重算 returns,先掐会被洗回来;因子在后面才吃 returns。
|
|
1080
1084
|
result = self._drop_head_returns(result)
|
|
1081
1085
|
|
|
1082
|
-
#
|
|
1083
|
-
|
|
1084
|
-
|
|
1086
|
+
# 基准数据(必须在算因子之前 —— 因子可能需要它)
|
|
1087
|
+
if self.benchmark:
|
|
1088
|
+
result = self._add_benchmark(result, self.target_dir, pq_filters)
|
|
1089
|
+
|
|
1090
|
+
# 所有因子,一处计算:内置 / ta / 自定义一视同仁(带缓存)
|
|
1091
|
+
# 注意仍在切片之前:此时还带着预热数据,滚动窗口才够长。
|
|
1092
|
+
result = self._all_factors_with_cache(result)
|
|
1085
1093
|
|
|
1086
1094
|
# 切片回用户请求的日期范围
|
|
1087
1095
|
if start_date is not None and max_window > 0:
|
|
1088
1096
|
result = result[result.index >= pd.Timestamp(start_date)]
|
|
1089
1097
|
|
|
1090
|
-
# 基准数据
|
|
1091
|
-
if self.benchmark:
|
|
1092
|
-
result = self._add_benchmark(result, self.target_dir, pq_filters)
|
|
1093
|
-
|
|
1094
|
-
# qka 内置因子(需要 benchmark 和 symbol 前缀)
|
|
1095
|
-
result = self._qka_factors_with_cache(result)
|
|
1096
|
-
|
|
1097
1098
|
# 列顺序复原,与重构前一致
|
|
1098
1099
|
return self._order_columns(result, self._qka_factor_names())
|
|
1099
1100
|
|
|
@@ -1164,6 +1165,149 @@ class Data():
|
|
|
1164
1165
|
lambda part: part.assign(returns=part['returns'].where(part.index > cutoff))
|
|
1165
1166
|
)
|
|
1166
1167
|
|
|
1168
|
+
def _all_factors_with_cache(self, result):
|
|
1169
|
+
"""算所有因子 —— 内置 / ta / 自定义,**同一处、同一时机**(基准注入之后)。
|
|
1170
|
+
|
|
1171
|
+
缓存仍按因子粒度分两笔账(内置走 `_qka_factors_with_cache`,其余走
|
|
1172
|
+
`_factors_with_cache`),但它们是同一个阶段的两笔账,不是两条路径:
|
|
1173
|
+
两者都在基准注入之后执行,都能看到基准。
|
|
1174
|
+
|
|
1175
|
+
🚨 为什么必须有基准之后才算:因子可能定义成「相对大盘」的量(β、残差波动、
|
|
1176
|
+
相关系数…)。内置的和自定义的在这个问题上没有任何区别 —— 所以不存在
|
|
1177
|
+
「自定义因子先算一遍、内置因子后算一遍」这种分法。
|
|
1178
|
+
"""
|
|
1179
|
+
if not self._factors:
|
|
1180
|
+
return result
|
|
1181
|
+
result = self._factors_with_cache(result)
|
|
1182
|
+
result = self._qka_factors_with_cache(result)
|
|
1183
|
+
return result
|
|
1184
|
+
|
|
1185
|
+
def _apply_all_factors(self, df):
|
|
1186
|
+
"""所有因子一处算(无缓存路径;懒加载 / dask 走这里)。df 已含基准。"""
|
|
1187
|
+
inds = self._factors
|
|
1188
|
+
if not inds:
|
|
1189
|
+
return df
|
|
1190
|
+
if callable(inds):
|
|
1191
|
+
# 旧版整表函数形式:直接把整表交给它
|
|
1192
|
+
if isinstance(df, dd.DataFrame):
|
|
1193
|
+
return df.map_partitions(lambda p: inds(p.copy()))
|
|
1194
|
+
return inds(df.copy())
|
|
1195
|
+
if isinstance(df, dd.DataFrame):
|
|
1196
|
+
sample = df.head(self._min_rows_for_factors())
|
|
1197
|
+
meta = self._compute_all_factor_cols(sample.copy())
|
|
1198
|
+
return df.map_partitions(
|
|
1199
|
+
lambda p: self._compute_all_factor_cols(p.copy()), meta=meta)
|
|
1200
|
+
return self._compute_all_factor_cols(df.copy())
|
|
1201
|
+
|
|
1202
|
+
def _compute_all_factor_cols(self, df, only=None):
|
|
1203
|
+
"""逐票把该票的全部因子算出来,写回 `{symbol}|{因子}` 列。
|
|
1204
|
+
|
|
1205
|
+
🚨 内置指标和自定义因子在这里没有区别:都是「拿这只票的数据,吐一列」。
|
|
1206
|
+
差别只有函数从哪来 —— 内置的从 qka.core.indicator 取,自定义的是传进来的
|
|
1207
|
+
函数对象,ta 的是从 `ta.xxx.yyy` 这个路径 import 出来的。
|
|
1208
|
+
"""
|
|
1209
|
+
if not isinstance(self._factors, dict):
|
|
1210
|
+
return df
|
|
1211
|
+
col_index = self._columns_by_symbol(df.columns)
|
|
1212
|
+
has_bench = 'benchmark|returns' in df.columns
|
|
1213
|
+
|
|
1214
|
+
for symbol in self.symbols:
|
|
1215
|
+
cols = col_index.get(symbol)
|
|
1216
|
+
if not cols:
|
|
1217
|
+
continue
|
|
1218
|
+
plain = self._symbol_slice(df, symbol, cols)
|
|
1219
|
+
if plain.empty:
|
|
1220
|
+
continue
|
|
1221
|
+
# 基准给到自定义因子时叫 bench —— 与内置看到的 benchmark|returns 同源同值
|
|
1222
|
+
if has_bench:
|
|
1223
|
+
plain['bench'] = df['benchmark|returns'].reindex(plain.index)
|
|
1224
|
+
|
|
1225
|
+
for name, spec in self._factors.items():
|
|
1226
|
+
if only is not None and name not in only:
|
|
1227
|
+
continue
|
|
1228
|
+
series = self._one_factor(plain, symbol, name, spec)
|
|
1229
|
+
if series is not None:
|
|
1230
|
+
df[f'{symbol}|{name}'] = series.reindex(df.index)
|
|
1231
|
+
return df
|
|
1232
|
+
|
|
1233
|
+
def _one_factor(self, plain, symbol, name, spec):
|
|
1234
|
+
"""算单个因子。返回一列 Series,认不出/算失败返回 None。
|
|
1235
|
+
|
|
1236
|
+
plain: 这只票的表,列名**已去前缀**(open/high/low/close/volume/amount/returns),
|
|
1237
|
+
外加一列 bench(基准收益,有基准时才有)。
|
|
1238
|
+
"""
|
|
1239
|
+
# ① 自定义函数:直接调,它自己决定用不用 bench
|
|
1240
|
+
if callable(spec):
|
|
1241
|
+
try:
|
|
1242
|
+
res = spec(plain)
|
|
1243
|
+
except Exception as e: # noqa: BLE001
|
|
1244
|
+
logger.warning(f'计算 {symbol}|{name} 失败: {type(e).__name__}: {e}')
|
|
1245
|
+
return None
|
|
1246
|
+
if isinstance(res, pd.DataFrame):
|
|
1247
|
+
return res[name] if name in res.columns else None
|
|
1248
|
+
return res
|
|
1249
|
+
|
|
1250
|
+
if not isinstance(spec, (list, tuple)) or not spec:
|
|
1251
|
+
logger.warning(f'因子 {name} 的定义必须是 tuple 或 callable,跳过')
|
|
1252
|
+
return None
|
|
1253
|
+
|
|
1254
|
+
ind_type, args = spec[0], list(spec[1:])
|
|
1255
|
+
|
|
1256
|
+
# ② qka 内置指标:从 indicator 模块取函数,喂「带前缀的小表」
|
|
1257
|
+
if _is_qka_factor(ind_type):
|
|
1258
|
+
opts = args.pop() if args and isinstance(args[-1], dict) else {}
|
|
1259
|
+
lim = opts.get('max_abs_return')
|
|
1260
|
+
fn_name = ind_type.split('.', 1)[1] if ind_type.startswith('qka.') else ind_type
|
|
1261
|
+
fn = getattr(indicator, fn_name, None)
|
|
1262
|
+
if fn is None:
|
|
1263
|
+
logger.warning(f'qka 内置指标不存在: {fn_name},跳过')
|
|
1264
|
+
return None
|
|
1265
|
+
work = pd.DataFrame({f'{symbol}|returns': plain['returns']})
|
|
1266
|
+
if 'bench' in plain.columns:
|
|
1267
|
+
work['benchmark|returns'] = plain['bench']
|
|
1268
|
+
if lim:
|
|
1269
|
+
for c in work.columns:
|
|
1270
|
+
work[c] = work[c].where(work[c].abs() <= lim)
|
|
1271
|
+
rf_kwargs = {}
|
|
1272
|
+
if (self.risk_free is not None and _accepts_rf(fn)
|
|
1273
|
+
and not _rf_given_positionally(fn, args)):
|
|
1274
|
+
rf_kwargs['rf'] = self.risk_free
|
|
1275
|
+
try:
|
|
1276
|
+
return fn(work, symbol, *args, **rf_kwargs)
|
|
1277
|
+
except ValueError as e:
|
|
1278
|
+
logger.warning(f'计算 {symbol}|{name} 失败: {e}')
|
|
1279
|
+
return None
|
|
1280
|
+
|
|
1281
|
+
# ③ ta 指标:'ta.trend.sma_indicator' + 列名 + 参数
|
|
1282
|
+
cols, rest_start = [], 0
|
|
1283
|
+
for i, arg in enumerate(args):
|
|
1284
|
+
if isinstance(arg, str):
|
|
1285
|
+
cols.append(arg)
|
|
1286
|
+
rest_start = i + 1
|
|
1287
|
+
else:
|
|
1288
|
+
break
|
|
1289
|
+
if not cols:
|
|
1290
|
+
logger.warning(f'因子 {name} 缺少计算列名,跳过')
|
|
1291
|
+
return None
|
|
1292
|
+
rest = args[rest_start:]
|
|
1293
|
+
if not isinstance(ind_type, str) or '.' not in ind_type:
|
|
1294
|
+
logger.warning(f'因子 {name} 的类型必须为 "ta.xxx.yyy" 格式,跳过: {ind_type}')
|
|
1295
|
+
return None
|
|
1296
|
+
import importlib
|
|
1297
|
+
module_name, func_name = ind_type.rsplit('.', 1)
|
|
1298
|
+
try:
|
|
1299
|
+
fn = getattr(importlib.import_module(module_name), func_name)
|
|
1300
|
+
except (ImportError, AttributeError) as e:
|
|
1301
|
+
logger.warning(f'无法加载指标函数 {ind_type}: {e}')
|
|
1302
|
+
return None
|
|
1303
|
+
try:
|
|
1304
|
+
if len(cols) == 1:
|
|
1305
|
+
return fn(plain[cols[0]], *rest)
|
|
1306
|
+
return fn(*[plain[f] for f in cols], *rest)
|
|
1307
|
+
except Exception as e: # noqa: BLE001
|
|
1308
|
+
logger.warning(f'计算 {symbol}|{name} 失败: {type(e).__name__}: {e}')
|
|
1309
|
+
return None
|
|
1310
|
+
|
|
1167
1311
|
def _apply_factors(self, df):
|
|
1168
1312
|
"""
|
|
1169
1313
|
对单只股票的数据应用预定义的指标/因子。
|
|
@@ -1903,6 +2047,10 @@ class Data():
|
|
|
1903
2047
|
sub = self._symbol_slice(result, symbol, cols)
|
|
1904
2048
|
if sub.empty:
|
|
1905
2049
|
continue
|
|
2050
|
+
# 基准也交给自定义因子(叫 bench)—— 此时 benchmark 已注入,
|
|
2051
|
+
# 自定义因子和内置因子看到的是同一份基准收益
|
|
2052
|
+
if 'benchmark|returns' in result.columns:
|
|
2053
|
+
sub['bench'] = result['benchmark|returns'].reindex(sub.index)
|
|
1906
2054
|
|
|
1907
2055
|
# 模拟标的没有缓存文件;没开缓存 / 配方认不出也一样 —— 一律现算不落盘
|
|
1908
2056
|
if plan is None or symbol in self._sim_specs:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|