找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 53|回复: 0

全市场5000只票看不过来?这个工具只盯“板块+个股”共振创新高,一眼锁定真强势 (含源码)

[复制链接]

1965

主题

107

回帖

5万

积分

管理员

积分
58756
发表于 昨天 20:01 | 显示全部楼层 |阅读模式
重要提示:本指标公式源码属于技术指标,仅作为知识分享和学习参考,不做为投资依据。

炒股最累的是什么?不是买卖,是从5000多只股票里选出那几只真强势的。
你看,单按“创新高”筛一遍,呼啦啦出来几百只,根本看不过来。为啥?因为很多创新高只是个股自己的事,跟大盘、跟板块没半毛钱关系,持续性极差。
但如果一只股票创新高,它所在的板块也正在创新高,那性质就完全变了——这说明是整个板块在走强,资金在集体做多,个股不是孤立无援。
先找强势板块,再找强势股。 板块和个股一起创新高,才叫真强势
别人给你一堆名单,它给你一条逻辑。这个工具干的就是这件事:
扫全市场400多个板块,先揪出正在创新高的板块,再看这些板块里哪些个股也在同步创新高。 板块和个股共振,比你在5000只里瞎找靠谱一百倍。
你不用全市场大海捞针,只在最强板块里挑。选股范围一下从5000缩到几十只,效率直接拉满。


37b34436c6969022649da6d094785c91.png



# -*- coding: utf-8 -*-"""板块创新高扫描器 —— 桌面 GUI 版(不依赖网页)==============================================基于 tkinter(Python 自带,无需联网打开浏览器),界面包含:  - 2 个判定选项:用【收盘价】创高 / 用【最高价】创高  - 手动输入:板块创 N 日新高  - 手动输入:个股创 M 日新高   ← 板块与个股周期独立  - 每板块取前 N 只、限制创新高板块数  - 开始扫描按钮 + 进度条,结果直接在窗口表格展示(板块可展开看个股)复用 sector_new_high.py 的扫描内核(行情/板块解析/新高判定)。运行:    python sector_new_high_gui.py            # 双击即可    python sector_new_high_gui.py --tdx-path "D:/tdx""""from __future__ import annotationsimport argparseimport osimport queueimport sysimport threadingimport timefrom datetime import datetime# ---- 让双击(系统 python)也能找到 venv 里的 eltdx -------------------------_HERE = os.path.dirname(os.path.abspath(__file__))_VENV_SP = os.path.join(_HERE, ".venv_eltdx", "Lib", "site-packages")if os.path.isdir(_VENV_SP) and _VENV_SP not in sys.path:    sys.path.insert(0, _VENV_SP)import tkinter as tkimport tkinter.font as tkfontfrom tkinter import ttk, messagebox, filedialogimport sector_new_high as core  # 复用扫描内核# ---------------------------------------------------------------------------# 全局字体(统一放大)# ---------------------------------------------------------------------------FONT_TITLE = ("Microsoft YaHei", 18, "bold")FONT_SUBTITLE = ("Microsoft YaHei", 14, "bold")FONT_LABEL = ("Microsoft YaHei", 12)FONT_RADIO = ("Microsoft YaHei", 12)FONT_BTN = ("Microsoft YaHei", 12)FONT_STATUS = ("Microsoft YaHei", 11)FONT_TREE = ("Microsoft YaHei", 12)FONT_TREE_HEAD = ("Microsoft YaHei", 12, "bold")# ---------------------------------------------------------------------------# 扫描工作线程(通过 queue 与主线程通信,避免卡 UI)# ---------------------------------------------------------------------------class ScanWorker(threading.Thread):    def __init__(self, q: queue.Queue, params: dict):        super().__init__(daemon=True)        self.q = q        self.p = params    def run(self):        try:            self._run()        except Exception as e:  # 任何异常都回传,避免无声失败            self.q.put(("error", f"{type(e).__name__}: {e}"))    def _run(self):        q = self.q        p = self.p        # 先做一次协议级主站探测:剔除「TCP 能连但协议不通」的失效节点,        # 否则会卡在失效主机上反复失败。结果进程内缓存,只探测一次。        q.put(("status", "正在探测可用行情主站..."))        hosts = core.resolve_hosts()        q.put(("status", f"可用行情主站 {len(hosts)} 个,正在查找板块文件..."))        block_path = core.find_block_file(p.get("tdx_path"))        if not block_path:            self.q.put(("error", "未找到板块文件 infoharbor_block.dat,请用「通达信目录」按钮指定。"))            return        q.put(("status", f"板块文件: {block_path}"))        blocks = core.parse_blocks(block_path)        q.put(("status", f"解析到 {len(blocks)} 个板块,加载代码表..."))        core.build_name_map()        q.put(("status", f"名称映射 {len(core._NAME_MAP)} 条,开始扫描板块..."))        sectors = core.load_sector_indices()        sectors = [(c6, fc, nm) for (c6, fc, nm) in sectors if c6 in blocks]        total = len(sectors)        q.put(("phase", "sector", total, 0))  # 阶段:板块,总数        lookback_sec = p["lookback_sector"]        lookback_stk = p["lookback_stock"]        use_close = p["use_close"]        count_sec = lookback_sec + 6        count_stk = lookback_stk + 6        # ---- 第一层:板块创新高(板块用 lookback_sector) ----        new_secs = []        done = 0        lock = threading.Lock()        def eval_sector(item):            c6, fc, nm = item            s = core.fetch_kline(fc, count_sec, "index")            if s is None:                return None            is_new, prev_max, bp, last = core.new_high_info(s, lookback_sec, use_close)            if not is_new:                return None            return {"code": c6, "full_code": fc, "name": nm,                    "breakout_pct": bp, "last": last, "prev_max": prev_max}        with core.cf.ThreadPoolExecutor(max_workers=core.THREADS) as ex:            import concurrent.futures as cf_local            futs = {ex.submit(eval_sector, it): it for it in sectors}            for fut in cf_local.as_completed(futs):                r = fut.result()                with lock:                    done += 1                if r:                    new_secs.append(r)                q.put(("prog", done, total, len(new_secs)))        new_secs.sort(key=lambda x: x["breakout_pct"], reverse=True)        max_sec = p["max_sectors"]        if max_sec and len(new_secs) > max_sec:            new_secs = new_secs[:max_sec]        q.put(("status", f"创新高板块 {len(new_secs)} 个,开始扫描成分股..."))        # ---- 第二层:成分股创新高(个股用 lookback_stock) ----        sum_members = sum(min(len(blocks[s["code"]]["members"]), p["member_cap"] or 10**9)                          for s in new_secs)        q.put(("phase", "stock", sum_members, 0))        report = []        m_done = 0        m_lock = threading.Lock()        def eval_stock(full):            s = core.fetch_kline(full, count_stk, "stock")            if s is None:                return None            is_new, prev_max, bp, last = core.new_high_info(s, lookback_stk, use_close)            if not is_new:                return None            return {"full_code": full, "name": core._NAME_MAP.get(full, ""),                    "last": last, "breakout_pct": bp, "prev_max": prev_max}        for sec in new_secs:            members = blocks[sec["code"]]["members"]            if p["member_cap"] and len(members) > p["member_cap"]:                members = members[:p["member_cap"]]            stocks = []            sec_done = 0            with core.cf.ThreadPoolExecutor(max_workers=core.THREADS) as ex:                futs = {ex.submit(eval_stock, m): m for m in members}                for fut in cf_local.as_completed(futs):                    r = fut.result()                    with m_lock:                        m_done += 1                        sec_done += 1                    if r:                        stocks.append(r)                    q.put(("prog2", m_done, sum_members, sec["name"], sec_done, len(members)))            stocks.sort(key=lambda x: x["breakout_pct"], reverse=True)            sec["stocks"] = stocks[:p["top_n"]]            sec["stock_count"] = len(stocks)            sec["member_total"] = len(members)            report.append(sec)        q.put(("done", report, time.time()))# ---------------------------------------------------------------------------# GUI# ---------------------------------------------------------------------------class App:    def __init__(self, root: tk.Tk, tdx_path: str | None):        self.root = root        self.tdx_path = tdx_path        self.scanning = False        self.q = queue.Queue()        self.worker = None        root.title("板块创新高扫描器")        root.geometry("1020x720")        try:            from ctypes import windll            windll.shcore.SetProcessDpiAwareness(1)        except Exception:            pass        self._setup_styles()        self._build_widgets()        self._poll()    # ---- ttk 样式统一放大 ----    def _setup_styles(self):        style = ttk.Style()        style.configure("TLabel", font=FONT_LABEL)        style.configure("TButton", font=FONT_BTN)        style.configure("TRadiobutton", font=FONT_RADIO)        style.configure("TSpinbox", font=FONT_LABEL)        style.configure("TEntry", font=FONT_LABEL)        style.configure("TLabelframe.Label", font=FONT_LABEL)        style.configure("Treeview", font=FONT_TREE, rowheight=28)        style.configure("Treeview.Heading", font=FONT_TREE_HEAD)    # ---- 界面布局 ----    def _build_widgets(self):        pad = {"padx": 12, "pady": 6}        # 顶部说明        ttk.Label(self.root, text="板块创新高扫描  ·  桌面版(无需浏览器)",                  font=FONT_TITLE).pack(anchor="w", **pad)        # 参数区        frm = ttk.LabelFrame(self.root, text="扫描参数")        frm.pack(fill="x", **pad)        # 2 个判定选项(收盘价 / 最高价)        opt = ttk.LabelFrame(frm, text="新高判定口径(2 选 1)")        opt.grid(row=0, column=0, columnspan=3, sticky="ew", padx=8, pady=8)        self.use_close = tk.BooleanVar(value=True)        ttk.Radiobutton(opt, text="收盘价创高", variable=self.use_close,                        value=True).pack(side="left", padx=14, pady=6)        ttk.Radiobutton(opt, text="最高价创高", variable=self.use_close,                        value=False).pack(side="left", padx=14, pady=6)        # 板块创 N 日新高(手动输入)        ttk.Label(frm, text="板块创多少日新高:").grid(row=1, column=0, sticky="w", padx=8, pady=8)        self.sec_days = tk.IntVar(value=250)        self.sec_spin = ttk.Spinbox(frm, from_=5, to=999, width=8,                                    textvariable=self.sec_days, font=FONT_LABEL)        self.sec_spin.grid(row=1, column=1, sticky="w", padx=8, pady=8)        ttk.Label(frm, text="日").grid(row=1, column=2, sticky="w", padx=2)        # 个股创 M 日新高(手动输入)        ttk.Label(frm, text="个股创多少日新高:").grid(row=2, column=0, sticky="w", padx=8, pady=8)        self.stk_days = tk.IntVar(value=250)        self.stk_spin = ttk.Spinbox(frm, from_=5, to=999, width=8,                                    textvariable=self.stk_days, font=FONT_LABEL)        self.stk_spin.grid(row=2, column=1, sticky="w", padx=8, pady=8)        ttk.Label(frm, text="日").grid(row=2, column=2, sticky="w", padx=2)        # 每板块取前 N 只 + 限制板块数 + 通达信目录        ttk.Label(frm, text="每板块取前 N 只:").grid(row=3, column=0, sticky="w", padx=8, pady=8)        self.top_n = tk.IntVar(value=5)        ttk.Spinbox(frm, from_=1, to=30, width=8, textvariable=self.top_n,                    font=FONT_LABEL).grid(row=3, column=1, sticky="w", padx=8, pady=8)        ttk.Label(frm, text="限制创新高板块数:").grid(row=4, column=0, sticky="w", padx=8, pady=8)        self.max_sec = tk.IntVar(value=0)        ttk.Spinbox(frm, from_=0, to=200, width=8, textvariable=self.max_sec,                    font=FONT_LABEL).grid(row=4, column=1, sticky="w", padx=8, pady=8)        ttk.Label(frm, text="0 = 不限").grid(row=4, column=2, sticky="w", padx=2)        ttk.Label(frm, text="通达信目录:").grid(row=5, column=0, sticky="w", padx=8, pady=8)        self.tdx_var = tk.StringVar(value=self.tdx_path or "")        ttk.Entry(frm, textvariable=self.tdx_var, width=40, font=FONT_LABEL).grid(            row=5, column=1, sticky="w", padx=8, pady=8)        ttk.Button(frm, text="浏览...", command=self._browse).grid(row=5, column=2, sticky="w", padx=4)        # 控制按钮        ctrl = ttk.Frame(self.root)        ctrl.pack(fill="x", **pad)        self.start_btn = ttk.Button(ctrl, text="开始扫描", command=self._start)        self.start_btn.pack(side="left", padx=(0, 10))        self.stop_btn = ttk.Button(ctrl, text="停止", command=self._stop, state="disabled")        self.stop_btn.pack(side="left")        # 进度条 + 状态        self.pb = ttk.Progressbar(self.root, mode="determinate", maximum=100, value=0)        self.pb.pack(fill="x", **pad)        self.status = ttk.Label(self.root, text="就绪。点击「开始扫描」。", foreground="#555",                                font=FONT_STATUS)        self.status.pack(anchor="w", **pad)        # 结果树        cols = ("name", "code", "price", "break", "ratio")        self.tree = ttk.Treeview(self.root, columns=cols, show="tree headings", height=16)        self.tree.heading("#0", text="板块 / 个股")        self.tree.column("#0", width=220, minwidth=140)        self.tree.heading("name", text="名称")        self.tree.column("name", width=160)        self.tree.heading("code", text="代码")        self.tree.column("code", width=110)        self.tree.heading("price", text="现价")        self.tree.column("price", width=90)        self.tree.heading("break", text="突破幅度")        self.tree.column("break", width=100)        self.tree.heading("ratio", text="成分股新高")        self.tree.column("ratio", width=130)        self.tree.tag_configure("sector", background="#eef2f7", font=("Microsoft YaHei", 13, "bold"))        self.tree.tag_configure("stock", foreground="#222", font=FONT_TREE)        self.tree.pack(fill="both", expand=True, **pad)        # 滚动条        vsb = ttk.Scrollbar(self.tree, orient="vertical", command=self.tree.yview)        self.tree.configure(yscrollcommand=vsb.set)        vsb.pack(side="right", fill="y")    def _browse(self):        d = filedialog.askdirectory(title="选择通达信安装目录(含 T0002)")        if d:            self.tdx_var.set(d)    # ---- 启动 / 停止 ----    def _start(self):        if self.scanning:            return        # 校验输入        try:            sec_days = int(self.sec_spin.get())            stk_days = int(self.stk_spin.get())            top_n = int(self.top_n.get())            max_sec = int(self.max_sec.get())        except ValueError:            messagebox.showerror("参数错误", "日数/N/板块数 必须为整数。")            return        if sec_days < 1 or stk_days < 1:            messagebox.showerror("参数错误", "日数必须 ≥ 1。")            return        self.scanning = True        self.start_btn.config(state="disabled")        self.stop_btn.config(state="normal")        self.tree.delete(*self.tree.get_children())        self.pb.config(value=0)        params = {            "tdx_path": self.tdx_var.get().strip() or None,            "lookback_sector": sec_days,            "lookback_stock": stk_days,            "use_close": self.use_close.get(),            "top_n": top_n,            "max_sectors": max_sec,            "member_cap": 300,        }        self.worker = ScanWorker(self.q, params)        self.worker.start()    def _stop(self):        self.scanning = False        self.status.config(text="已请求停止(当前批次完成后生效)。")        self.start_btn.config(state="normal")        self.stop_btn.config(state="disabled")    # ---- 主线程轮询 queue,更新 UI ----    def _poll(self):        try:            while True:                msg = self.q.get_nowait()                self._handle(msg)        except queue.Empty:            pass        self.root.after(80, self._poll)    def _handle(self, msg):        kind = msg[0]        if kind == "status":            self.status.config(text=msg[1])        elif kind == "phase":            _, phase, total, _ = msg            self._phase = phase            self._total = total            if phase == "sector":                self.status.config(text="阶段 1/2:扫描板块创新高...")            else:                self.status.config(text="阶段 2/2:扫描板块内成分股...")        elif kind == "prog":  # 第一层进度            done, total, found = msg[1], msg[2], msg[3]            self.status.config(text=f"扫描板块 {done}/{total},已发现新高 {found} 个")            if total:                self.pb.config(value=int(done / total * 70))        elif kind == "prog2":  # 第二层进度            m_done, sum_m, sname, sdone, stotal = msg[1], msg[2], msg[3], msg[4], msg[5]            self.status.config(text=f"板块【{sname}】成分股 {sdone}/{stotal} | 总进度 {m_done}/{sum_m}")            if sum_m:                self.pb.config(value=int(70 + m_done / sum_m * 30))        elif kind == "done":            report, t0 = msg[1], msg[2]            self._fill_tree(report)            self.pb.config(value=100)            self.status.config(text=f"完成:共 {len(report)} 个创新高板块,耗时 {time.time()-t0:.1f}s")            self.scanning = False            self.start_btn.config(state="normal")            self.stop_btn.config(state="disabled")        elif kind == "error":            messagebox.showerror("扫描出错", msg[1])            self.status.config(text="出错:" + msg[1])            self.scanning = False            self.start_btn.config(state="normal")            self.stop_btn.config(state="disabled")    def _fill_tree(self, report):        self.tree.delete(*self.tree.get_children())        if not report:            self.tree.insert("", "end", text="结果",                             values=("当前没有板块创出新高。", "", "", "", ""))            return        for sec in report:            siid = f"sec_{sec['code']}"            self.tree.insert("", "end", iid=siid, text=sec["name"],                             values=(sec["name"], sec["code"], "—",                                     f"+{sec['breakout_pct']:.2f}%",                                     f"{sec.get('stock_count',0)}/{sec.get('member_total',0)} 只"),                             tags=("sector",))            for s in sec["stocks"]:                nm = s["name"] or s["full_code"]                self.tree.insert(siid, "end", text=nm,                                 values=(nm, s["full_code"], f"{s['last']:.2f}",                                         f"+{s['breakout_pct']:.2f}%", ""),                                 tags=("stock",))            if not sec["stocks"]:                self.tree.insert(siid, "end", text="(无成分股同步新高)",                                 values=("(无成分股同步新高)", "", "", "", ""),                                 tags=("stock",))def main():    ap = argparse.ArgumentParser(description="板块创新高扫描器 GUI")    ap.add_argument("--tdx-path", type=str, default=None, help="通达信安装目录(含 T0002)")    args = ap.parse_args()    # eltdx 可用性检查    try:        from eltdx import TdxClient  # noqa: F401    except ImportError:        root = tk.Tk()        root.withdraw()        messagebox.showerror("依赖缺失",            "未找到 eltdx 库。请先安装:\n\n"            "  pip install eltdx\n\n"            "或在 venv 环境中运行本程序。")        return    root = tk.Tk()    App(root, args.tdx_path)    root.mainloop()if __name__ == "__main__":    main()



您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|小黑屋| 股指标网 ( 渝ICP备2024026571号-1 )

GMT+8, 2026-10-10 06:29 Powered by Discuz! X3.5