From 7f8b27df3b93234ec3ccd5030510597c33fff1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=A5=E5=BE=AE?= Date: Thu, 2 Jul 2026 00:36:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AD=B9=E7=A0=81=E5=9B=A0=E5=AD=90?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20+=20=E5=88=86=E9=92=9FK=E7=BA=BF=20+=20?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E6=95=B0=E6=8D=AE=E5=9B=9E=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/chip_factors.py: - _build_chip_distribution(): 从60日日线OHLCV估算筹码分布 - calc_all(): 计算三大筹码因子(穿透率/当日穿透率/乖离率) - batch_calc(): 批量计算全部持仓+自选 - chip_cache/: 缓存每日筹码状态(支持因子滚动计算) mo_provider.py: - get_minute_kline(): 1分钟K线获取(1s限流, 单次最多240条) 首次运行结果: 33只股票完成筹码分布计算 最高亏损筹码占比: 万科99.5% 神华98.6% 比亚迪股份98.7% 最低亏损筹码占比: 建滔4.0% 药明康德2.1% --- data/chip_cache/000657.json | 1 + data/chip_cache/000700.json | 1 + data/chip_cache/000711.json | 1 + data/chip_cache/001309.json | 1 + data/chip_cache/002594.json | 1 + data/chip_cache/01070.json | 1 + data/chip_cache/01088.json | 1 + data/chip_cache/01211.json | 1 + data/chip_cache/01478.json | 1 + data/chip_cache/01888.json | 1 + data/chip_cache/02202.json | 1 + data/chip_cache/02318.json | 1 + data/chip_cache/02359.json | 1 + data/chip_cache/02388.json | 1 + data/chip_cache/02628.json | 1 + data/chip_cache/06160.json | 1 + data/chip_cache/06869.json | 1 + data/chip_cache/09868.json | 1 + data/chip_cache/09988.json | 1 + data/chip_cache/300035.json | 1 + data/chip_cache/300124.json | 1 + data/chip_cache/300308.json | 1 + data/chip_cache/300548.json | 1 + data/chip_cache/300750.json | 1 + data/chip_cache/301308.json | 1 + data/chip_cache/518880.json | 1 + data/chip_cache/600519.json | 1 + data/chip_cache/600563.json | 1 + data/chip_cache/601318.json | 1 + data/chip_cache/601899.json | 1 + data/chip_cache/688411.json | 1 + data/chip_cache/688630.json | 1 + data/chip_cache/688639.json | 1 + scripts/chip_factors.py | 262 ++++++++++++++++++++++++++++++++++++ 34 files changed, 295 insertions(+) create mode 100644 data/chip_cache/000657.json create mode 100644 data/chip_cache/000700.json create mode 100644 data/chip_cache/000711.json create mode 100644 data/chip_cache/001309.json create mode 100644 data/chip_cache/002594.json create mode 100644 data/chip_cache/01070.json create mode 100644 data/chip_cache/01088.json create mode 100644 data/chip_cache/01211.json create mode 100644 data/chip_cache/01478.json create mode 100644 data/chip_cache/01888.json create mode 100644 data/chip_cache/02202.json create mode 100644 data/chip_cache/02318.json create mode 100644 data/chip_cache/02359.json create mode 100644 data/chip_cache/02388.json create mode 100644 data/chip_cache/02628.json create mode 100644 data/chip_cache/06160.json create mode 100644 data/chip_cache/06869.json create mode 100644 data/chip_cache/09868.json create mode 100644 data/chip_cache/09988.json create mode 100644 data/chip_cache/300035.json create mode 100644 data/chip_cache/300124.json create mode 100644 data/chip_cache/300308.json create mode 100644 data/chip_cache/300548.json create mode 100644 data/chip_cache/300750.json create mode 100644 data/chip_cache/301308.json create mode 100644 data/chip_cache/518880.json create mode 100644 data/chip_cache/600519.json create mode 100644 data/chip_cache/600563.json create mode 100644 data/chip_cache/601318.json create mode 100644 data/chip_cache/601899.json create mode 100644 data/chip_cache/688411.json create mode 100644 data/chip_cache/688630.json create mode 100644 data/chip_cache/688639.json create mode 100644 scripts/chip_factors.py diff --git a/data/chip_cache/000657.json b/data/chip_cache/000657.json new file mode 100644 index 0000000..7afe047 --- /dev/null +++ b/data/chip_cache/000657.json @@ -0,0 +1 @@ +{"winner_pct": 0.8905253861016628, "loser_pct": 0.10947461389833724, "bias": 0.0021894922779667446, "updated_at": "2026-07-02T00:35:07.027353"} \ No newline at end of file diff --git a/data/chip_cache/000700.json b/data/chip_cache/000700.json new file mode 100644 index 0000000..86809ab --- /dev/null +++ b/data/chip_cache/000700.json @@ -0,0 +1 @@ +{"winner_pct": 0.7051650439382611, "loser_pct": 0.29483495606173893, "bias": 0.005896699121234778, "updated_at": "2026-07-02T00:35:08.872295"} \ No newline at end of file diff --git a/data/chip_cache/000711.json b/data/chip_cache/000711.json new file mode 100644 index 0000000..3bf52b4 --- /dev/null +++ b/data/chip_cache/000711.json @@ -0,0 +1 @@ +{"winner_pct": 0.4599205028102528, "loser_pct": 0.5400794971897471, "bias": 0.010801589943794944, "updated_at": "2026-07-02T00:35:10.904477"} \ No newline at end of file diff --git a/data/chip_cache/001309.json b/data/chip_cache/001309.json new file mode 100644 index 0000000..f5235aa --- /dev/null +++ b/data/chip_cache/001309.json @@ -0,0 +1 @@ +{"winner_pct": 0.9384281148873177, "loser_pct": 0.061571885112682245, "bias": 0.001231437702253645, "updated_at": "2026-07-02T00:35:13.007896"} \ No newline at end of file diff --git a/data/chip_cache/002594.json b/data/chip_cache/002594.json new file mode 100644 index 0000000..965d101 --- /dev/null +++ b/data/chip_cache/002594.json @@ -0,0 +1 @@ +{"winner_pct": 0.06109805218679897, "loser_pct": 0.938901947813201, "bias": 0.01877803895626402, "updated_at": "2026-07-02T00:35:15.121583"} \ No newline at end of file diff --git a/data/chip_cache/01070.json b/data/chip_cache/01070.json new file mode 100644 index 0000000..896cfd5 --- /dev/null +++ b/data/chip_cache/01070.json @@ -0,0 +1 @@ +{"winner_pct": 0.11515632261891377, "loser_pct": 0.8848436773810862, "bias": 0.017696873547621724, "updated_at": "2026-07-02T00:35:21.588604"} \ No newline at end of file diff --git a/data/chip_cache/01088.json b/data/chip_cache/01088.json new file mode 100644 index 0000000..89c6f9e --- /dev/null +++ b/data/chip_cache/01088.json @@ -0,0 +1 @@ +{"winner_pct": 0.01442763129876452, "loser_pct": 0.9855723687012354, "bias": 0.01971144737402471, "updated_at": "2026-07-02T00:35:23.448927"} \ No newline at end of file diff --git a/data/chip_cache/01211.json b/data/chip_cache/01211.json new file mode 100644 index 0000000..e8d406b --- /dev/null +++ b/data/chip_cache/01211.json @@ -0,0 +1 @@ +{"winner_pct": 0.012654394366315136, "loser_pct": 0.9873456056336849, "bias": 0.0197469121126737, "updated_at": "2026-07-02T00:35:25.515177"} \ No newline at end of file diff --git a/data/chip_cache/01478.json b/data/chip_cache/01478.json new file mode 100644 index 0000000..dc03f53 --- /dev/null +++ b/data/chip_cache/01478.json @@ -0,0 +1 @@ +{"winner_pct": 0.02378383161135486, "loser_pct": 0.9762161683886451, "bias": 0.019524323367772902, "updated_at": "2026-07-02T00:35:27.617318"} \ No newline at end of file diff --git a/data/chip_cache/01888.json b/data/chip_cache/01888.json new file mode 100644 index 0000000..01e03e7 --- /dev/null +++ b/data/chip_cache/01888.json @@ -0,0 +1 @@ +{"winner_pct": 0.9605101031912507, "loser_pct": 0.039489896808749314, "bias": 0.0007897979361749863, "updated_at": "2026-07-02T00:35:29.720189"} \ No newline at end of file diff --git a/data/chip_cache/02202.json b/data/chip_cache/02202.json new file mode 100644 index 0000000..e5816e1 --- /dev/null +++ b/data/chip_cache/02202.json @@ -0,0 +1 @@ +{"winner_pct": 0.0054820390352224024, "loser_pct": 0.9945179609647776, "bias": 0.019890359219295554, "updated_at": "2026-07-02T00:35:31.579501"} \ No newline at end of file diff --git a/data/chip_cache/02318.json b/data/chip_cache/02318.json new file mode 100644 index 0000000..c7b97bb --- /dev/null +++ b/data/chip_cache/02318.json @@ -0,0 +1 @@ +{"winner_pct": 0.02266099339387972, "loser_pct": 0.9773390066061202, "bias": 0.019546780132122403, "updated_at": "2026-07-02T00:35:33.432173"} \ No newline at end of file diff --git a/data/chip_cache/02359.json b/data/chip_cache/02359.json new file mode 100644 index 0000000..12a7108 --- /dev/null +++ b/data/chip_cache/02359.json @@ -0,0 +1 @@ +{"winner_pct": 0.9788099282869067, "loser_pct": 0.02119007171309323, "bias": 0.00042380143426186466, "updated_at": "2026-07-02T00:35:35.527231"} \ No newline at end of file diff --git a/data/chip_cache/02388.json b/data/chip_cache/02388.json new file mode 100644 index 0000000..70952e5 --- /dev/null +++ b/data/chip_cache/02388.json @@ -0,0 +1 @@ +{"winner_pct": 0.03250983201120672, "loser_pct": 0.9674901679887933, "bias": 0.019349803359775868, "updated_at": "2026-07-02T00:35:37.394766"} \ No newline at end of file diff --git a/data/chip_cache/02628.json b/data/chip_cache/02628.json new file mode 100644 index 0000000..07ae320 --- /dev/null +++ b/data/chip_cache/02628.json @@ -0,0 +1 @@ +{"winner_pct": 0.16852330162051746, "loser_pct": 0.8314766983794825, "bias": 0.01662953396758965, "updated_at": "2026-07-02T00:35:39.254760"} \ No newline at end of file diff --git a/data/chip_cache/06160.json b/data/chip_cache/06160.json new file mode 100644 index 0000000..de1da33 --- /dev/null +++ b/data/chip_cache/06160.json @@ -0,0 +1 @@ +{"winner_pct": 0.36653309165872255, "loser_pct": 0.6334669083412774, "bias": 0.012669338166825549, "updated_at": "2026-07-02T00:35:41.322663"} \ No newline at end of file diff --git a/data/chip_cache/06869.json b/data/chip_cache/06869.json new file mode 100644 index 0000000..7d5c43c --- /dev/null +++ b/data/chip_cache/06869.json @@ -0,0 +1 @@ +{"winner_pct": 0.8235950164623566, "loser_pct": 0.17640498353764347, "bias": 0.0035280996707528697, "updated_at": "2026-07-02T00:35:43.401261"} \ No newline at end of file diff --git a/data/chip_cache/09868.json b/data/chip_cache/09868.json new file mode 100644 index 0000000..8434511 --- /dev/null +++ b/data/chip_cache/09868.json @@ -0,0 +1 @@ +{"winner_pct": 0.10325476172315214, "loser_pct": 0.8967452382768478, "bias": 0.017934904765536958, "updated_at": "2026-07-02T00:35:45.448103"} \ No newline at end of file diff --git a/data/chip_cache/09988.json b/data/chip_cache/09988.json new file mode 100644 index 0000000..fb91a09 --- /dev/null +++ b/data/chip_cache/09988.json @@ -0,0 +1 @@ +{"winner_pct": 0.053119794705996114, "loser_pct": 0.9468802052940039, "bias": 0.018937604105880076, "updated_at": "2026-07-02T00:35:47.336553"} \ No newline at end of file diff --git a/data/chip_cache/300035.json b/data/chip_cache/300035.json new file mode 100644 index 0000000..597103a --- /dev/null +++ b/data/chip_cache/300035.json @@ -0,0 +1 @@ +{"winner_pct": 0.028368192935391008, "loser_pct": 0.971631807064609, "bias": 0.019432636141292182, "updated_at": "2026-07-02T00:35:49.222809"} \ No newline at end of file diff --git a/data/chip_cache/300124.json b/data/chip_cache/300124.json new file mode 100644 index 0000000..24877cf --- /dev/null +++ b/data/chip_cache/300124.json @@ -0,0 +1 @@ +{"winner_pct": 0.36417545165724186, "loser_pct": 0.6358245483427581, "bias": 0.012716490966855162, "updated_at": "2026-07-02T00:35:51.342524"} \ No newline at end of file diff --git a/data/chip_cache/300308.json b/data/chip_cache/300308.json new file mode 100644 index 0000000..2c86c49 --- /dev/null +++ b/data/chip_cache/300308.json @@ -0,0 +1 @@ +{"winner_pct": 0.792023910513367, "loser_pct": 0.20797608948663301, "bias": 0.004159521789732661, "updated_at": "2026-07-02T00:35:53.208843"} \ No newline at end of file diff --git a/data/chip_cache/300548.json b/data/chip_cache/300548.json new file mode 100644 index 0000000..c20f674 --- /dev/null +++ b/data/chip_cache/300548.json @@ -0,0 +1 @@ +{"winner_pct": 0.5896316941534578, "loser_pct": 0.41036830584654216, "bias": 0.008207366116930844, "updated_at": "2026-07-02T00:35:55.349248"} \ No newline at end of file diff --git a/data/chip_cache/300750.json b/data/chip_cache/300750.json new file mode 100644 index 0000000..1489225 --- /dev/null +++ b/data/chip_cache/300750.json @@ -0,0 +1 @@ +{"winner_pct": 0.051381452378474426, "loser_pct": 0.9486185476215256, "bias": 0.018972370952430512, "updated_at": "2026-07-02T00:35:57.452499"} \ No newline at end of file diff --git a/data/chip_cache/301308.json b/data/chip_cache/301308.json new file mode 100644 index 0000000..b961054 --- /dev/null +++ b/data/chip_cache/301308.json @@ -0,0 +1 @@ +{"winner_pct": 0.9278175418685898, "loser_pct": 0.07218245813141022, "bias": 0.0014436491626282045, "updated_at": "2026-07-02T00:35:59.312573"} \ No newline at end of file diff --git a/data/chip_cache/518880.json b/data/chip_cache/518880.json new file mode 100644 index 0000000..91bbd6a --- /dev/null +++ b/data/chip_cache/518880.json @@ -0,0 +1 @@ +{"winner_pct": 0.015377013137657148, "loser_pct": 0.9846229868623428, "bias": 0.019692459737246856, "updated_at": "2026-07-02T00:36:01.402360"} \ No newline at end of file diff --git a/data/chip_cache/600519.json b/data/chip_cache/600519.json new file mode 100644 index 0000000..9a8774e --- /dev/null +++ b/data/chip_cache/600519.json @@ -0,0 +1 @@ +{"winner_pct": 0.10535744228960574, "loser_pct": 0.8946425577103942, "bias": 0.035427845285331605, "updated_at": "2026-07-02T00:36:03.265204"} \ No newline at end of file diff --git a/data/chip_cache/600563.json b/data/chip_cache/600563.json new file mode 100644 index 0000000..453171b --- /dev/null +++ b/data/chip_cache/600563.json @@ -0,0 +1 @@ +{"winner_pct": 0.901115667682602, "loser_pct": 0.09888433231739803, "bias": 0.0019776866463479606, "updated_at": "2026-07-02T00:36:05.897221"} \ No newline at end of file diff --git a/data/chip_cache/601318.json b/data/chip_cache/601318.json new file mode 100644 index 0000000..edb5240 --- /dev/null +++ b/data/chip_cache/601318.json @@ -0,0 +1 @@ +{"winner_pct": 0.12629635051048638, "loser_pct": 0.8737036494895137, "bias": 0.017474072989790274, "updated_at": "2026-07-02T00:36:07.760371"} \ No newline at end of file diff --git a/data/chip_cache/601899.json b/data/chip_cache/601899.json new file mode 100644 index 0000000..eab1aa9 --- /dev/null +++ b/data/chip_cache/601899.json @@ -0,0 +1 @@ +{"winner_pct": 0.03603420474156716, "loser_pct": 0.9639657952584328, "bias": 0.019279315905168657, "updated_at": "2026-07-02T00:36:09.643701"} \ No newline at end of file diff --git a/data/chip_cache/688411.json b/data/chip_cache/688411.json new file mode 100644 index 0000000..627f947 --- /dev/null +++ b/data/chip_cache/688411.json @@ -0,0 +1 @@ +{"winner_pct": 0.5745885997936249, "loser_pct": 0.42541140020637513, "bias": 0.008508228004127504, "updated_at": "2026-07-02T00:36:11.528137"} \ No newline at end of file diff --git a/data/chip_cache/688630.json b/data/chip_cache/688630.json new file mode 100644 index 0000000..777cd60 --- /dev/null +++ b/data/chip_cache/688630.json @@ -0,0 +1 @@ +{"winner_pct": 0.9180078278115366, "loser_pct": 0.08199217218846348, "bias": 0.0016398434437692698, "updated_at": "2026-07-02T00:36:13.369248"} \ No newline at end of file diff --git a/data/chip_cache/688639.json b/data/chip_cache/688639.json new file mode 100644 index 0000000..42ce1af --- /dev/null +++ b/data/chip_cache/688639.json @@ -0,0 +1 @@ +{"winner_pct": 0.19133053163048952, "loser_pct": 0.8086694683695105, "bias": 0.01617338936739021, "updated_at": "2026-07-02T00:36:15.460650"} \ No newline at end of file diff --git a/scripts/chip_factors.py b/scripts/chip_factors.py new file mode 100644 index 0000000..035522b --- /dev/null +++ b/scripts/chip_factors.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +"""chip_factors.py — 筹码因子计算模块 + +基于中信建投《筹码分布因子系统构建》研报,实现四大类因子。 +用日线数据就够了,分钟数据用于当日穿透率增强。 + +用法: + from chip_factors import ChipFactors + cf = ChipFactors() + + # 计算单只股票的筹码乖离率 + result = cf.calc_all("600519") + print(result["bias"], result["ptr"], result["ptr_today"]) + + # 批量计算所有持仓/自选 + results = cf.batch_calc(["600519", "00700", "000700"]) +""" + +import json, os, sqlite3, time, urllib.request +from datetime import datetime, timedelta +from pathlib import Path + +DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") +MOFIN_ROOT = Path("/home/hmo/MoFin") +CACHE_DIR = MOFIN_ROOT / "data" / "chip_cache" + +# ── 分钟数据限流 ── +_last_minute_call = 0 + +def _fetch_quote(code): + """拉腾讯实时价(清代理)""" + for k in list(os.environ.keys()): + if 'proxy' in k.lower(): + os.environ.pop(k) + prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk" + try: + url = f"http://qt.gtimg.cn/q={prefix}{code}" + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + resp = urllib.request.urlopen(req, timeout=5).read().decode('gbk') + fields = resp.split('=')[1].strip().strip('"').strip(';').split('~') + return float(fields[3]) if len(fields) > 3 else 0 + except: + return 0 + + +def _fetch_minute_kline(code, count=60): + """拉1分钟K线(带限流)""" + global _last_minute_call + now = time.time() + if now - _last_minute_call < 1.0: + time.sleep(1.0 - (now - _last_minute_call)) + secid = f"1.{code}" if code.startswith(('6','5')) else f"0.{code}" + url = (f"https://push2.eastmoney.com/api/qt/stock/kline/get" + f"?secid={secid}&fields1=f1,f2,f3&fields2=f51,f52,f53,f54,f55,f56,f57" + f"&klt=1&fqt=1&end=20500101&lmt={min(count, 240)}") + try: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + resp = urllib.request.urlopen(req, timeout=8) + data = json.loads(resp.read())["data"]["klines"] + _last_minute_call = time.time() + return [line.split(",") for line in data] + except: + return None + + +class ChipFactors: + """筹码因子计算器""" + + def __init__(self): + CACHE_DIR.mkdir(parents=True, exist_ok=True) + self._cache = {} # code → {last_chip, winner, bias} + self._load_cache() + + def _load_cache(self): + """加载缓存的筹码状态""" + for f in CACHE_DIR.glob("*.json"): + code = f.stem + try: + with open(f) as fp: + self._cache[code] = json.load(fp) + except: + pass + + def _save_cache(self, code): + """保存筹码状态""" + if code in self._cache: + path = CACHE_DIR / f"{code}.json" + with open(path, "w") as fp: + json.dump(self._cache[code], fp, ensure_ascii=False) + + # ── 筹码分布估算(用日线OHLCV) ── + def _build_chip_distribution(self, code): + """从日线K线估算筹码分布。 + + 原理:假设每日成交量在OHLC区间内均匀分布, + 每根K线的成交量按价格区间分配,累积成筹码分布。 + """ + for k in list(os.environ.keys()): + if 'proxy' in k.lower(): + os.environ.pop(k) + # 从腾讯API取60日K线 + prefix = "sh" if code.startswith(('60','68','51')) else "sz" if code.startswith(('00','30','15')) else "hk" + url = f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?param={prefix}{code},day,,,60,qfq" + try: + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + resp = opener.open(req, timeout=8).read().decode('utf-8') + data = json.loads(resp) + day_key = 'qfqday' if prefix != 'hk' else 'day' + bars = data.get('data', {}).get(f'{prefix}{code}', {}).get(day_key, []) + except: + return {} + + # 估算筹码分布:价格区间 → 累积量 + chip_dist = {} # price_level → accumulated_volume + decay = 0.97 # 每日衰减因子(老筹码逐步换手) + + for bar in bars: + try: + if len(bar) < 6: + continue + high = float(bar[3]) # index 3 = high + low = float(bar[4]) # index 4 = low + volume = float(bar[5]) if len(bar) > 5 else 0 # index 5 = volume + if high <= low or volume <= 0: + continue + # 在OHLC区间均匀分配成交量 + step = max(round((high - low) / 5, 2), 0.01) + level = round(low, 2) + vol_per_level = volume / max(int((high - low) / step) + 1, 1) + while level <= high: + chip_dist[level] = chip_dist.get(level, 0) + vol_per_level + level = round(level + step, 2) + except: + continue + + # 衰减老筹码 + total = sum(chip_dist.values()) + if total > 0: + for k in chip_dist: + chip_dist[k] *= decay + + return chip_dist + + # ── 三大因子计算 ── + def calc_all(self, code, name="", price=None): + """计算全部筹码因子,返回dict""" + result = {"code": code, "name": name, "price": price} + + # 获取当前价(如果没传) + if not price: + price = _fetch_quote(code) + if not price: + return result + result["price"] = price + + # 构建筹码分布 + chip_dist = self._build_chip_distribution(code) + if not chip_dist or price <= 0: + return result + + # 计算盈利/亏损筹码占比 + total_vol = sum(chip_dist.values()) + if total_vol <= 0: + return result + + winner_vol = sum(v for k, v in chip_dist.items() if k <= price) # 盈利筹码(cost≤现价) + loser_vol = total_vol - winner_vol # 亏损筹码(cost>现价) + + winner_pct = winner_vol / total_vol + loser_pct = loser_vol / total_vol + + # 获取前日状态 + prev = self._cache.get(code, {}) + prev_winner = prev.get("winner_pct", winner_pct) + prev_bias = prev.get("bias", 0) + + # 估算换手率(近10日均量/总流通股) + turnover = 0.02 # 默认2% + try: + prefix2 = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk" + url2 = f"http://ifzq.gtimg.cn/appstock/app/fkline/get?param={prefix2}{code},day,,,10,qfq" + req2 = urllib.request.Request(url2, headers={"User-Agent": "Mozilla/5.0"}) + resp2 = urllib.request.urlopen(req2, timeout=5).read().decode('utf-8') + data2 = json.loads(resp2) + dk = 'qfqday' if prefix2 != 'hk' else 'day' + bars2 = data2.get('data', {}).get(f'{prefix2}{code}', {}).get(dk, []) + if len(bars2) > 5: + avg_vol = sum(float(b[5]) for b in bars2[-10:] if len(b)>5) / min(len(bars2), 10) + # 用近60日最高量估算总流通股 + max_vol = avg_vol * 50 # 估算值 + turnover = min(avg_vol / max(max_vol, 1), 0.3) + except: + pass + + # 1. 筹码穿透率 PTR = (winner_pct - prev_winner) / turnover + ptr = (winner_pct - prev_winner) / max(turnover, 0.001) + + # 2. 当日筹码穿透率(简化版) = 今日量 / 总筹码 / turnover + ptr_today = 0 + minute_data = _fetch_minute_kline(code, count=30) + if minute_data: + today_vol = sum(float(m[5]) for m in minute_data if len(m) > 5) + ptr_today = today_vol / max(total_vol, 1) / max(turnover, 0.001) + + # 3. 筹码乖离率(亏损版本—按文章发现,亏损筹码版反而最强) + # bias = loser_pct * turnover + prev_bias * (1 - turnover) + bias = loser_pct * turnover + prev_bias * (1 - turnover) + + # 更新缓存 + self._cache[code] = { + "winner_pct": winner_pct, + "loser_pct": loser_pct, + "bias": bias, + "updated_at": datetime.now().isoformat() + } + self._save_cache(code) + + return { + "code": code, + "name": name, + "price": price, + "winner_pct": round(winner_pct, 4), + "loser_pct": round(loser_pct, 4), + "ptr": round(ptr, 4), + "ptr_today": round(ptr_today, 4), + "bias": round(bias, 4), + "turnover": round(turnover, 4), + } + + def batch_calc(self, stocks): + """批量计算多只股票""" + results = [] + for i, (code, name) in enumerate(stocks): + if i > 0: + time.sleep(1.5) # 限流 + result = self.calc_all(code, name) + results.append(result) + return results + + +# ── 主入口 ── +if __name__ == "__main__": + import sys + cf = ChipFactors() + + # 从decisions.json获取持仓+自选 + dec = json.loads(open(str(MOFIN_ROOT / "data" / "decisions.json")).read()) + stocks = [(s["code"], s.get("name","")) for s in dec.get("decisions", []) if s.get("status") != "closed"] + + results = cf.batch_calc(stocks) + + # 按bias排序显示(亏损筹码占比最高的排前面) + results.sort(key=lambda r: r.get("bias", 0), reverse=True) + print(f"{'股票':16} {'亏损筹码%':>10} {'PTR':>8} {'乖离率':>8} {'换手率':>8}") + print("-" * 60) + for r in results: + if r.get("price"): + print(f"{r['name']:8}({r['code']:6}) {r['loser_pct']*100:>8.1f}% {r['ptr']:>8.4f} {r['bias']:>8.4f} {r['turnover']*100:>6.1f}%") + + print(f"\n共计算{len(results)}只股票") + print(f"筹码缓存目录: {CACHE_DIR}")