qka 2.2.6.dev2__tar.gz → 2.3.1__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.2.6.dev2 → qka-2.3.1}/PKG-INFO +1 -1
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/data.py +115 -20
- {qka-2.2.6.dev2 → qka-2.3.1}/.github/workflows/release.yml +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/.gitignore +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/.vscode/settings.json +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/CHANGELOG.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/LICENSE +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/README.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/pyproject.toml +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/__init__.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/cli.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/__init__.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/accessor.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/analysis.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/backtest.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/broker.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/indicator.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/report.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/simulate.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/sizing.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/core/strategy.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/utils/__init__.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/qka/utils/logger.py +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/skills/qka/SKILL.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/skills/qka/references/analysis.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/skills/qka/references/backtest.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/skills/qka/references/broker.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/skills/qka/references/data.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/skills/qka/references/sizing.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/skills/qka/references/strategy.md +0 -0
- {qka-2.2.6.dev2 → qka-2.3.1}/tools/generate_api_ref.py +0 -0
|
@@ -4,6 +4,7 @@ QKA数据模块
|
|
|
4
4
|
提供统一的数据获取、缓存和管理功能,支持多数据源、多周期、多因子的数据获取。
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import os
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from tqdm import tqdm
|
|
9
10
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -30,6 +31,23 @@ def _is_qka_indicator(ind_type):
|
|
|
30
31
|
return (isinstance(ind_type, str) and
|
|
31
32
|
(ind_type.startswith('qka.') or ind_type in _QKA_BUILTIN_NAMES))
|
|
32
33
|
|
|
34
|
+
|
|
35
|
+
# 当日日线数据「可能已经生成」的时刻(本地时间)。一天以它为界切成前后两个时段:
|
|
36
|
+
# 17:00 之前,数据源手里最新的一根还是上一交易日,当天这根根本还没生成;
|
|
37
|
+
# 17:00 之后,当天的已经出完了,要补也就这一次。
|
|
38
|
+
DATA_READY_HOUR = 17
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _data_period(ts) -> tuple:
|
|
42
|
+
"""把时刻归入「数据时段」——(日期,是否已过当日数据生成时刻)。
|
|
43
|
+
|
|
44
|
+
同一个时段内数据源不会多出任何东西,对同一只标的重复询问纯属白跑一趟。
|
|
45
|
+
休市日、节假日尤其明显:日历上没有交易日,缓存永远「缺今天」,若不记时段
|
|
46
|
+
就会每次请求都去问一遍,永远问不到结果。
|
|
47
|
+
"""
|
|
48
|
+
return (ts.date(), ts.hour >= DATA_READY_HOUR)
|
|
49
|
+
|
|
50
|
+
|
|
33
51
|
class Data():
|
|
34
52
|
"""
|
|
35
53
|
数据管理类
|
|
@@ -72,6 +90,10 @@ class Data():
|
|
|
72
90
|
# 无估值/换手率等扩展列(季报级 financials 不是每日序列,不接入)。
|
|
73
91
|
TICKFLOW_EXTRA_FIELDS = []
|
|
74
92
|
|
|
93
|
+
# parquet 元数据键:这只标的「已确认起点」——数据源在该日期之前没有更早的数据。
|
|
94
|
+
# 没有它,请求起点只要早于缓存首日(往往只是撞上长假)就得白跑一趟网络去确认。
|
|
95
|
+
VERIFIED_FROM_KEY = b'qka_verified_from'
|
|
96
|
+
|
|
75
97
|
# ── 模拟数据源默认值 ──
|
|
76
98
|
SIM_START = '2020-01-02' # 模拟行情默认起点(调用方未给 start_date 时使用)
|
|
77
99
|
SIM_START_PRICE = 20.0 # 起始价
|
|
@@ -376,6 +398,43 @@ class Data():
|
|
|
376
398
|
z[t] = phi * z[t - 1] + sigma * eps[t]
|
|
377
399
|
return z
|
|
378
400
|
|
|
401
|
+
def _checked_recently(self, path: Path) -> bool:
|
|
402
|
+
"""本时段是否已经问过数据源(以缓存文件的修改时间当记号)。
|
|
403
|
+
|
|
404
|
+
缓存文件每次被写都会刷新修改时间:首次下载、补到新数据是写出来的;
|
|
405
|
+
问了但数据源没给新数据时也会主动刷新一次(见 _download 末尾)。
|
|
406
|
+
两种情形都意味着「本时段已经核对过,数据源不会再给新东西」。
|
|
407
|
+
"""
|
|
408
|
+
if not path.exists():
|
|
409
|
+
return False
|
|
410
|
+
mtime = pd.Timestamp.fromtimestamp(path.stat().st_mtime)
|
|
411
|
+
return _data_period(mtime) == _data_period(pd.Timestamp.now())
|
|
412
|
+
|
|
413
|
+
def _mark_checked(self, path: Path) -> None:
|
|
414
|
+
"""留下「本时段已问过」的记号"""
|
|
415
|
+
if path.exists():
|
|
416
|
+
os.utime(path, None)
|
|
417
|
+
|
|
418
|
+
def _read_verified_from(self, path: Path):
|
|
419
|
+
"""读「已确认起点」;没有记录(老缓存)返回 None"""
|
|
420
|
+
try:
|
|
421
|
+
meta = pq.read_schema(path).metadata or {}
|
|
422
|
+
except Exception:
|
|
423
|
+
return None
|
|
424
|
+
raw = meta.get(self.VERIFIED_FROM_KEY)
|
|
425
|
+
if not raw:
|
|
426
|
+
return None
|
|
427
|
+
try:
|
|
428
|
+
return pd.Timestamp(raw.decode())
|
|
429
|
+
except Exception:
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
def _write_verified_from(self, path: Path, table: pa.Table, verified_from) -> None:
|
|
433
|
+
"""把「已确认起点」写进 parquet 元数据后落盘(数据本身原样保留)"""
|
|
434
|
+
meta = dict(table.schema.metadata or {})
|
|
435
|
+
meta[self.VERIFIED_FROM_KEY] = verified_from.strftime('%Y-%m-%d').encode()
|
|
436
|
+
pq.write_table(table.replace_schema_metadata(meta), path)
|
|
437
|
+
|
|
379
438
|
def _cache_missing_extra_fields(self, path: Path) -> bool:
|
|
380
439
|
"""检查已有 parquet 缓存是否缺少 extra_fields 指定的列。"""
|
|
381
440
|
if not self.extra_fields or not path.exists():
|
|
@@ -419,6 +478,10 @@ class Data():
|
|
|
419
478
|
只补下载缺失的部分(前面缺失补前面,后面缺失补后面),合并去重写回。
|
|
420
479
|
若缓存缺少 extra_fields 指定的列(如从无扩展字段升级到有),则全量重新下载。
|
|
421
480
|
|
|
481
|
+
每次问完数据源都会留下痕迹:补到新数据就写文件(修改时间自然刷新),
|
|
482
|
+
没补到就刷新修改时间 + 记下「已确认起点」。这两样让重复的请求不必再问网络,
|
|
483
|
+
详见 _needs_download。
|
|
484
|
+
|
|
422
485
|
Args:
|
|
423
486
|
symbol: 股票代码
|
|
424
487
|
download_start: 下载起始日期,格式 YYYY-MM-DD。None 表示拉全量(1990-01-01)
|
|
@@ -448,7 +511,10 @@ class Data():
|
|
|
448
511
|
if len(df) == 0:
|
|
449
512
|
raise RuntimeError(f"{symbol}: 数据源({self.source})返回空数据")
|
|
450
513
|
table = pa.Table.from_pandas(df)
|
|
451
|
-
|
|
514
|
+
# 这次就是从这个起点问的,一并记下「已确认起点」
|
|
515
|
+
self._write_verified_from(
|
|
516
|
+
path, table, pd.Timestamp(download_start or default_start)
|
|
517
|
+
)
|
|
452
518
|
return path
|
|
453
519
|
|
|
454
520
|
# ── 增量更新:检查缓存覆盖,补缺失范围 ──
|
|
@@ -461,17 +527,21 @@ class Data():
|
|
|
461
527
|
pieces = [existing]
|
|
462
528
|
changed = False
|
|
463
529
|
|
|
464
|
-
#
|
|
530
|
+
# 往前补(并记下「这个起点已确认没有更早的数据」,免得下次为同一个缺口白问)
|
|
531
|
+
old_verified = self._read_verified_from(path)
|
|
532
|
+
verified_from = old_verified
|
|
465
533
|
req_start = pd.Timestamp(download_start) if download_start else None
|
|
466
|
-
if req_start is not None and req_start <
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
534
|
+
if req_start is not None and (old_verified is None or req_start < old_verified):
|
|
535
|
+
if req_start < cache_min:
|
|
536
|
+
end_before = (cache_min - pd.Timedelta(days=1)).strftime("%Y-%m-%d")
|
|
537
|
+
df_before = self._fetch(
|
|
538
|
+
symbol, start_date=download_start, end_date=end_before,
|
|
539
|
+
extra_fields=merged_extra,
|
|
540
|
+
)
|
|
541
|
+
if len(df_before) > 0:
|
|
542
|
+
pieces.insert(0, df_before)
|
|
543
|
+
changed = True
|
|
544
|
+
verified_from = req_start
|
|
475
545
|
|
|
476
546
|
# 往后补
|
|
477
547
|
# 口径与 _needs_download 保持一致:按「日」比较。若用带时刻的 now(),
|
|
@@ -491,13 +561,24 @@ class Data():
|
|
|
491
561
|
pieces.append(df_after)
|
|
492
562
|
changed = True
|
|
493
563
|
|
|
564
|
+
if not changed and verified_from == old_verified:
|
|
565
|
+
# 问过了,数据源这段确实没有新数据 —— 留个记号,本时段内不再重复问。
|
|
566
|
+
# 失败(抛异常)时不走到这里,所以网络故障该重试还是会重试。
|
|
567
|
+
self._mark_checked(path)
|
|
568
|
+
return path
|
|
569
|
+
|
|
494
570
|
if changed:
|
|
495
571
|
combined = pd.concat(pieces)
|
|
496
572
|
combined = combined[~combined.index.duplicated(keep='last')]
|
|
497
573
|
combined = combined.sort_index()
|
|
498
574
|
table = pa.Table.from_pandas(combined)
|
|
499
|
-
|
|
575
|
+
else:
|
|
576
|
+
table = pa.Table.from_pandas(existing) # 数据没动,只是要更新「已确认起点」
|
|
500
577
|
|
|
578
|
+
if verified_from is not None:
|
|
579
|
+
self._write_verified_from(path, table, verified_from)
|
|
580
|
+
else:
|
|
581
|
+
pq.write_table(table, path)
|
|
501
582
|
return path
|
|
502
583
|
|
|
503
584
|
def _needs_download(
|
|
@@ -508,6 +589,11 @@ class Data():
|
|
|
508
589
|
"""
|
|
509
590
|
判断股票是否需要网络下载。
|
|
510
591
|
缓存不存在、不覆盖请求范围、或需要拉最新数据时返回 True。
|
|
592
|
+
|
|
593
|
+
「缺最新数据」这件事按本时段去重(见 _data_period):一天以数据生成时刻
|
|
594
|
+
切成前后两段,同一段里问过一次就够了 —— 数据源在这段时间里不会多出东西。
|
|
595
|
+
休市日、节假日的缓存末日永远早于「今天」,若不按时段去重就会每次请求都
|
|
596
|
+
白问一趟,还永远问不到结果。
|
|
511
597
|
"""
|
|
512
598
|
path = self.target_dir / f"{symbol}.parquet"
|
|
513
599
|
if not path.exists() or self._cache_missing_extra_fields(path):
|
|
@@ -517,15 +603,24 @@ class Data():
|
|
|
517
603
|
return False
|
|
518
604
|
cache_min = existing.index.min()
|
|
519
605
|
cache_max = existing.index.max()
|
|
520
|
-
today = pd.Timestamp.now().floor('D')
|
|
521
606
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
607
|
+
# 往前:起点早于缓存首日才有缺口。但这个缺口往往只是撞上长假(缓存首日
|
|
608
|
+
# 之后才是交易日),所以要看它是否已经被确认过 —— 没确认过才值得再问一趟。
|
|
609
|
+
if download_start is not None:
|
|
610
|
+
req_start = pd.Timestamp(download_start)
|
|
611
|
+
if req_start < cache_min:
|
|
612
|
+
verified_from = self._read_verified_from(path)
|
|
613
|
+
if verified_from is None or req_start < verified_from:
|
|
614
|
+
return True
|
|
615
|
+
|
|
616
|
+
# 往后:终点晚于缓存末日(给了 end_date 就是确定的缺口,直接补)。
|
|
617
|
+
if download_end is not None:
|
|
618
|
+
return pd.Timestamp(download_end) > cache_max
|
|
619
|
+
|
|
620
|
+
# 无 end_date = 要一直要到今天。缺的只是「今天」这一段时,本时段问过就不必再问:
|
|
621
|
+
# 数据源在这段时间里不会多出东西,休市日 / 盘前盘中尤其明显,否则每次请求都要白问一趟。
|
|
622
|
+
if cache_max < pd.Timestamp.now().floor('D'):
|
|
623
|
+
return not self._checked_recently(path)
|
|
529
624
|
return False
|
|
530
625
|
|
|
531
626
|
def get(self, lazy: bool = False, start_date: str = None, end_date: str = None):
|
|
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
|