qka 2.5.6.dev4__tar.gz → 2.5.7.dev2__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.
Files changed (32) hide show
  1. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/PKG-INFO +1 -1
  2. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/data.py +199 -0
  3. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/.github/workflows/release.yml +0 -0
  4. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/.gitignore +0 -0
  5. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/.vscode/settings.json +0 -0
  6. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/CHANGELOG.md +0 -0
  7. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/LICENSE +0 -0
  8. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/README.md +0 -0
  9. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/pyproject.toml +0 -0
  10. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/__init__.py +0 -0
  11. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/cli.py +0 -0
  12. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/__init__.py +0 -0
  13. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/accessor.py +0 -0
  14. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/analysis.py +0 -0
  15. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/backtest.py +0 -0
  16. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/broker.py +0 -0
  17. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/indicator.py +0 -0
  18. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/report.py +0 -0
  19. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/simulate.py +0 -0
  20. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/sizing.py +0 -0
  21. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/strategy.py +0 -0
  22. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/core/tdx.py +0 -0
  23. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/utils/__init__.py +0 -0
  24. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/qka/utils/logger.py +0 -0
  25. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/skills/qka/SKILL.md +0 -0
  26. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/skills/qka/references/analysis.md +0 -0
  27. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/skills/qka/references/backtest.md +0 -0
  28. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/skills/qka/references/broker.md +0 -0
  29. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/skills/qka/references/data.md +0 -0
  30. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/skills/qka/references/sizing.md +0 -0
  31. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/skills/qka/references/strategy.md +0 -0
  32. {qka-2.5.6.dev4 → qka-2.5.7.dev2}/tools/generate_api_ref.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qka
3
- Version: 2.5.6.dev4
3
+ Version: 2.5.7.dev2
4
4
  Summary: QKA(快量化 / Quant Kit for A-shares)- 简洁易用的 A 股量化回测框架
5
5
  Project-URL: Home, https://github.com/zsrl/qka
6
6
  Project-URL: Repository, https://github.com/zsrl/qka
@@ -767,6 +767,116 @@ class Data():
767
767
  pq.write_table(table, path)
768
768
  return path
769
769
 
770
+ def _prefetch_latest(
771
+ self, symbols: List[str], download_start: str = None,
772
+ download_end: str = None,
773
+ ) -> List[str]:
774
+ """先用批量接口把缓存补上,返回**仍需逐只下载**的票。
775
+
776
+ 这是 `get()` 下载前的预热:绝大多数日常调用只是「缓存落后一两天」,
777
+ 一次批量请求就能补齐,不必每只各发一个请求。
778
+
779
+ **取数区间由缓存自己决定,不拍条数:**
780
+ 起点 = 这批票里最早的缓存末日 − 5 天(与 `_download` 的 start_after 同口径)
781
+ 终点 = 请求的 end_date(None 则到今天)
782
+
783
+ 前复权不做增量(历史会被分红送股改写),整批原样返回,交给 `_download`
784
+ 整段重取。
785
+
786
+ Args:
787
+ symbols: 待下载的标的
788
+ download_start / download_end: 本次请求范围,与 `_download` 同义
789
+
790
+ Returns:
791
+ List[str]: 仍需逐只下载的标的
792
+ """
793
+ if not self.incremental:
794
+ return list(symbols)
795
+
796
+ # 起点取决于「本地最旧的那只补到哪天」。读各票末日只读一列,几百只
797
+ # 一两秒,比多发几百个请求便宜得多。
798
+ lasts = []
799
+ for s in symbols:
800
+ p = self.target_dir / f"{s}.parquet"
801
+ if not p.exists():
802
+ continue # 首次建缓存,批量帮不上,留在后面
803
+ try:
804
+ idx = pd.read_parquet(p, columns=['close']).index
805
+ lasts.append(pd.to_datetime(idx).max())
806
+ except Exception:
807
+ continue
808
+ if not lasts:
809
+ return list(symbols)
810
+
811
+ start = (min(lasts) - pd.Timedelta(days=5)).strftime('%Y-%m-%d')
812
+ end = download_end or pd.Timestamp.now().floor('D').strftime('%Y-%m-%d')
813
+ if pd.Timestamp(start) > pd.Timestamp(end):
814
+ return list(symbols) # 请求区间全在缓存之前,批量帮不上
815
+
816
+ try:
817
+ bars_map = self._get_many_from_tickflow(symbols, start, end)
818
+ except Exception as e:
819
+ # 批量这条路不通(网络 / 限流 / 接口变更)就整体回落逐只,行为与从前一致
820
+ logger.warning(f"批量补数失败,回落逐只下载:{e}")
821
+ return list(symbols)
822
+
823
+ for s, bars in bars_map.items():
824
+ try:
825
+ self._append_bars(s, bars)
826
+ except Exception as e:
827
+ # 单只写盘失败不带崩整批;它会在下面的重算里留下来走逐只
828
+ logger.warning(f"{s}: 批量补数写入失败,回落逐只下载:{e}")
829
+
830
+ # 补过之后重新问一遍:**真补上了的才算解决**。首次建缓存的、缓存缺扩展
831
+ # 列的、请求区间落在缓存之前的,都会在这里留下来,交给 _download。
832
+ return [s for s in symbols
833
+ if self._needs_download(s, download_start, download_end)]
834
+
835
+ def _append_bars(self, symbol: str, bars: pd.DataFrame):
836
+ """把新日期的行情追加进这只票的缓存,返回新增天数(None = 缓存不存在)。
837
+
838
+ 🚨 只追加缓存里**没有的日期**,历史行一个不动。
839
+ 🚨 新行的因子列留空(NaN),由之后 cache_only 的因子补算填上;
840
+ 列结构与 dtype 必须与缓存完全一致,否则 qka 读回来会错列。
841
+ 🚨 必须保留原有 parquet 元数据(因子列登记 / 已确认起点),否则下次
842
+ `_restore_factor_columns` 认不出哪些列是因子,增量会把因子抹掉。
843
+ """
844
+ path = self.target_dir / f"{symbol}.parquet"
845
+ if not path.exists():
846
+ return None
847
+
848
+ existing = pd.read_parquet(path)
849
+ if not isinstance(existing.index, pd.DatetimeIndex):
850
+ existing.index = pd.to_datetime(existing.index)
851
+ new_rows = bars[bars.index > existing.index.max()]
852
+ if new_rows.empty:
853
+ return 0
854
+
855
+ # 按缓存的列顺序造新行:行情列有值,其余(因子等派生列)留空
856
+ add = pd.DataFrame(
857
+ {c: (new_rows[c].to_numpy() if c in new_rows.columns else np.nan)
858
+ for c in existing.columns},
859
+ index=new_rows.index,
860
+ )
861
+ for col, dt in existing.dtypes.items():
862
+ try:
863
+ add[col] = add[col].astype(dt)
864
+ except (ValueError, TypeError):
865
+ pass
866
+
867
+ combined = pd.concat([existing, add]).sort_index()
868
+ combined.index.name = existing.index.name or 'date'
869
+ # 因子列是派生数据,合并后补回旧值(口径同 _download 的增量段)
870
+ combined = self._restore_factor_columns(combined, existing, path)
871
+
872
+ table = pa.Table.from_pandas(combined)
873
+ old_meta = dict(pq.read_schema(path).metadata or {})
874
+ if old_meta:
875
+ table = table.replace_schema_metadata(
876
+ {**old_meta, **(table.schema.metadata or {})})
877
+ pq.write_table(table, path)
878
+ return len(add)
879
+
770
880
  def _rewrite_full(
771
881
  self, path: Path, symbol: str,
772
882
  download_start: str = None, download_end: str = None,
@@ -959,6 +1069,13 @@ class Data():
959
1069
  try:
960
1070
  if need_calendar:
961
1071
  sim_index = self._trading_index(sim_start, sim_end)
1072
+ # tickflow 有批量接口:先用一次请求把落后的票批量补上,剩下补不齐的
1073
+ # (首次建缓存 / 前复权要整段重取 / 缓存缺扩展列)才逐只下载。
1074
+ # 日常调用通常只落后一两天,这一段能把 N 次请求压到几次。
1075
+ if need_download and self.source == 'tickflow':
1076
+ need_download = self._prefetch_latest(
1077
+ need_download, download_start, download_end)
1078
+
962
1079
  if need_download:
963
1080
  # baostock 与 tdx 都是「进程级单例连接」(tdx 的 tq 类方法
964
1081
  # 共用同一 run_id),并发调用会共用同一连接状态,故逐个下载;
@@ -2387,6 +2504,88 @@ class Data():
2387
2504
  df[col] = pd.to_numeric(df[col], errors='coerce')
2388
2505
  return df
2389
2506
 
2507
+ def _get_many_from_tickflow(
2508
+ self, symbols: List[str], start_date: str, end_date: str,
2509
+ batch_size: int = 100, max_workers: int = 4,
2510
+ ) -> dict:
2511
+ """从 TickFlow **一次取回多只**标的的日线(按日期区间,不是按条数)。
2512
+
2513
+ 单只走 `_get_from_tickflow`,一只一个请求;全市场 7500 只就是 7500 次,
2514
+ 受限流约束要几小时。TickFlow 的 klines.batch 会把它们按 batch_size 切块、
2515
+ 用 max_workers 个并发发出去,几十次请求几分钟就够。
2516
+
2517
+ 返回结构与 `_get_from_tickflow` 完全一致(date 索引 + OHLCV/amount),
2518
+ 口径转换也一致:
2519
+ - **volume**:TickFlow 是「手」,×100 换成「股」
2520
+ - **timestamp**:北京时间零点的 UTC 表示,要换到上海时区再取日期
2521
+
2522
+ Args:
2523
+ symbols: 标的列表(「代码.市场」或 sh.600519 均可)
2524
+ start_date / end_date: 要取的日期区间(含两端)。这个区间由调用方
2525
+ 按「缓存缺到哪」算出来,不在这里拍一个条数。
2526
+ batch_size: 一次请求塞多少只(接口按 URL 长度自己切块)
2527
+ max_workers: SDK 内部一次并发几个请求(这是 SDK 参数,
2528
+ 不是业务层开线程池)
2529
+
2530
+ Returns:
2531
+ dict: {原始代码: DataFrame}。**取不到的标的不在字典里**(停牌 /
2532
+ 退市 / 新代码),由调用方决定怎么处理。
2533
+ """
2534
+ period_map = {'1d': '1d', '1w': '1w', '1M': '1M'}
2535
+ if self.period not in period_map:
2536
+ raise RuntimeError(
2537
+ f"批量取数暂不支持周期 {self.period!r}(可选: {list(period_map)})"
2538
+ )
2539
+ adjust_map = {'bfq': 'none', 'qfq': 'forward', 'hfq': 'backward'}
2540
+ adjust = adjust_map.get(self.adjust, 'forward')
2541
+
2542
+ # 日期 → 毫秒时间戳(右端 +1 天,保证 end_date 当天落在闭区间内)
2543
+ start_ms = int(pd.Timestamp(start_date).timestamp() * 1000)
2544
+ end_ms = int((pd.Timestamp(end_date) + pd.Timedelta(days=1)).timestamp() * 1000)
2545
+
2546
+ # 原代码 ↔ tickflow 代码互转:batch 用它自己那套做 key,要能映射回来
2547
+ tf_of = {self.to_tickflow_symbol(s): s for s in symbols}
2548
+ client = self._tickflow_client()
2549
+ try:
2550
+ res = client.klines.batch(
2551
+ list(tf_of),
2552
+ period=period_map[self.period],
2553
+ adjust=adjust,
2554
+ # count 与区间一起给:只给区间的话服务端默认只回最近 100 条,
2555
+ # 会把长区间静默截断(单只 _get_from_tickflow 踩过同一个坑)
2556
+ count=10000,
2557
+ start_time=start_ms,
2558
+ end_time=end_ms,
2559
+ batch_size=batch_size,
2560
+ max_workers=max_workers,
2561
+ )
2562
+ except Exception as e:
2563
+ raise RuntimeError(f"tickflow 批量取数失败: {e}")
2564
+
2565
+ out = {}
2566
+ for tf_sym, d in (res or {}).items():
2567
+ sym = tf_of.get(tf_sym)
2568
+ if sym is None or not d or not d.get('timestamp'):
2569
+ continue
2570
+ idx = (pd.to_datetime(pd.Series(d['timestamp']), unit='ms', utc=True)
2571
+ .dt.tz_convert('Asia/Shanghai')
2572
+ .dt.normalize()
2573
+ .dt.tz_localize(None))
2574
+ idx.name = 'date'
2575
+ df = pd.DataFrame({
2576
+ 'open': d['open'],
2577
+ 'high': d['high'],
2578
+ 'low': d['low'],
2579
+ 'close': d['close'],
2580
+ 'volume': [v * 100 for v in d['volume']], # 手 → 股
2581
+ 'amount': d['amount'],
2582
+ }, index=idx)
2583
+ for col in self.BAOSTOCK_BASE_FIELDS:
2584
+ df[col] = pd.to_numeric(df[col], errors='coerce')
2585
+ df = df[~df.index.duplicated(keep='last')].sort_index()
2586
+ out[sym] = df
2587
+ return out
2588
+
2390
2589
  # ── 通达信数据源(本机 TQ 版客户端,仅 Windows)────────────────
2391
2590
 
2392
2591
  # 通达信 get_market_data 字段名 → qka 标准列名
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