diff --git a/Finance/stock-holder.5m.py b/Finance/stock-holder.5m.py
new file mode 100755
index 000000000..1aac5799c
--- /dev/null
+++ b/Finance/stock-holder.5m.py
@@ -0,0 +1,1009 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Stock Holder
+# 3.0
+# 股票持仓监控,实时展示盈亏金额及持仓详情,支持初始化持仓、加仓、减仓、持久化。
+# python3
+
+import sys
+import os
+import json
+import urllib.request
+import subprocess
+import threading
+from http.server import HTTPServer, BaseHTTPRequestHandler
+from datetime import datetime, date
+
+# ── 路径 ──────────────────────────────────────────────────────────────────────
+DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".config")
+STOCK_INFO_F = os.path.join(DATA_DIR, "stock_info.json")
+
+os.makedirs(DATA_DIR, exist_ok=True)
+
+# ── 常量 ──────────────────────────────────────────────────────────────────────
+LOT_SIZE_NORMAL = 100
+LOT_SIZE_STAR = 200 # 科创板 688 开头
+
+def lot_size(code):
+ return LOT_SIZE_STAR if str(code).startswith("688") else LOT_SIZE_NORMAL
+
+def build_code(s):
+ return ("sh" if s.startswith("6") else "sz") + s
+
+def fmt(val):
+ """带符号两位小数"""
+ sign = "+" if val >= 0 else ""
+ return f"{sign}{val:.2f}"
+
+# ── JSON 读写 ──────────────────────────────────────────────────────────────────
+def load_info():
+ if os.path.exists(STOCK_INFO_F):
+ try:
+ with open(STOCK_INFO_F, "r", encoding="utf-8") as f:
+ return json.load(f)
+ except Exception:
+ pass
+ return {"holdings": [], "quote": []}
+
+def save_info(info):
+ with open(STOCK_INFO_F, "w", encoding="utf-8") as f:
+ json.dump(info, f, ensure_ascii=False, indent=2)
+
+def get_holdings(info):
+ return {h["code"]: h for h in info.get("holdings", [])}
+
+def get_quotes(info):
+ return {q["code"]: q for q in info.get("quote", [])}
+
+# ── 行情接口 ──────────────────────────────────────────────────────────────────
+def fetch_quotes(codes_6digit):
+ """返回 dict: code6 -> {name, price, prev_close, change, pct}"""
+ if not codes_6digit:
+ return {}
+ full = [build_code(c) for c in codes_6digit]
+ url = "https://qt.gtimg.cn/q=" + ",".join(full)
+ req = urllib.request.Request(url, headers={"Referer": "https://finance.qq.com"})
+ raw = urllib.request.urlopen(req, timeout=10).read().decode("gbk", errors="replace")
+ result = {}
+ for line in raw.strip().splitlines():
+ if '"' not in line:
+ continue
+ fields = line.split('"')[1].split("~")
+ code6 = fields[2]
+ result[code6] = {
+ "name": fields[1],
+ "price": float(fields[3]),
+ "prev_close": float(fields[4]),
+ "change": float(fields[31]) if fields[31] else 0.0,
+ "pct": float(fields[32]) if fields[32] else 0.0,
+ }
+ return result
+
+def fetch_name(code6):
+ q = fetch_quotes([code6])
+ return q.get(code6, {}).get("name", code6)
+
+# ── 时段判断 ──────────────────────────────────────────────────────────────────
+def trading_status():
+ """weekend | open | closing_noon | closing | after_close | closed"""
+ now = datetime.now()
+ if now.weekday() >= 5: # 5=周六 6=周日
+ return "weekend"
+ t = now.time()
+ def tm(s): return datetime.strptime(s, "%H:%M").time()
+ if (tm("09:30") <= t < tm("11:30")) or (tm("13:00") <= t < tm("15:00")):
+ return "open"
+ if tm("11:30") <= t < tm("13:00"):
+ return "closing_noon" # 11:30–12:59,首次触发写入,后续读缓存
+ if tm("15:00") <= t < tm("16:00"):
+ return "closing" # 15:00–15:59,首次触发写入,后续读缓存
+ if t >= tm("16:00"):
+ return "after_close" # 16:00+ 纯展示缓存
+ return "closed"
+
+# ── holdings 操作 ──────────────────────────────────────────────────────────────
+def apply_init(info, entries):
+ """初始化:请求接口拿名称+行情,写入 holdings 和 quote"""
+ codes = [e["code"] for e in entries]
+ try:
+ quotes = fetch_quotes(codes)
+ except Exception:
+ quotes = {}
+
+ hmap = get_holdings(info)
+ qmap = get_quotes(info)
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
+
+ for e in entries:
+ code = e["code"]
+ lots = float(e["lots"])
+ cost = round(float(e["cost"]), 4)
+ q = quotes.get(code, {})
+ name = q.get("name") or code
+
+ hmap[code] = {
+ "code": code,
+ "name": name,
+ "cost": cost,
+ "lots": lots,
+ "available_lots": lots,
+ }
+
+ # 写入 quote(当前行情快照)
+ price = q.get("price")
+ if price is not None:
+ shares = lots * lot_size(code)
+ prev = q.get("prev_close") or price
+ change = q.get("change", 0.0)
+ day_pnl = change * shares
+ day_pct = (change / prev * 100) if prev else 0
+ total_pnl = (price - cost) * shares
+ total_pct = ((price - cost) / cost * 100) if cost else 0
+ qmap[code] = {
+ "code": code,
+ "price": round(price, 4),
+ "market_val": round(price * shares, 2),
+ "day_pnl": round(day_pnl, 2),
+ "day_pct": round(day_pct, 2),
+ "total_pnl": round(total_pnl, 2),
+ "total_pct": round(total_pct, 2),
+ "date": now_str,
+ }
+
+ info["holdings"] = list(hmap.values())
+ info["quote"] = list(qmap.values())
+ save_info(info)
+
+# ── operate 操作盈亏(按天隔离) ────────────────────────────────────────────────
+def _clean_operate(info):
+ """清除非今日的 operate 记录(每次读取时调用)"""
+ today = date.today().isoformat()
+ ops = info.get("operate", [])
+ info["operate"] = [o for o in ops if o.get("date") == today]
+
+def _record_operate(info, code, pnl, open_lots, today):
+ """在 operate 中累加当日该 code 的操作盈亏;open_lots 仅首次写入(代表开盘持仓手数)"""
+ ops = info.setdefault("operate", [])
+ for o in ops:
+ if o["code"] == code and o["date"] == today:
+ o["pnl"] = round(o["pnl"] + pnl, 2)
+ return
+ ops.append({"code": code, "pnl": round(pnl, 2), "open_lots": open_lots, "date": today})
+
+def get_today_operate(info):
+ """返回 {code: {pnl, open_lots}} 当日操作映射"""
+ today = date.today().isoformat()
+ return {o["code"]: o for o in info.get("operate", []) if o.get("date") == today}
+
+def apply_buy(info, entries):
+ """加仓:更新 holdings(cost/lots),记录当日操作盈亏到 operate"""
+ hmap = get_holdings(info)
+ today = date.today().isoformat()
+
+ all_codes = [e["code"] for e in entries]
+ try:
+ live = fetch_quotes(all_codes)
+ except Exception:
+ live = {}
+
+ for e in entries:
+ code = e["code"]
+ price = float(e["price"])
+ buy_lots = float(e["lots"])
+ q = live.get(code, {})
+ prev_close = q.get("prev_close") or q.get("price") or price
+
+ if code in hmap:
+ old = hmap[code]
+ new_lots = old["lots"] + buy_lots
+ new_cost = (old["cost"] * old["lots"] + price * buy_lots) / new_lots
+ hmap[code]["cost"] = round(new_cost, 4)
+ hmap[code]["lots"] = new_lots
+ hmap[code]["available_lots"] = hmap[code].get("available_lots", 0) + buy_lots
+ else:
+ name = q.get("name") or code
+ hmap[code] = {
+ "code": code,
+ "name": name,
+ "cost": round(price, 4),
+ "lots": buy_lots,
+ "available_lots": buy_lots,
+ }
+
+ # 加仓操作盈亏:(0轴 - 买入价) * 手数 * 每手股数
+ # open_lots:本次操作前的持仓手数(首次加仓该code当天时记录,作为day_pct分母基础)
+ open_lots = hmap[code]["lots"] - buy_lots # 加仓后才更新了lots,减回去还原操作前
+ op_pnl = (prev_close - price) * buy_lots * lot_size(code)
+ _record_operate(info, code, op_pnl, open_lots, today)
+
+ info["holdings"] = list(hmap.values())
+ save_info(info)
+
+def apply_sell(info, entries):
+ """entries: [{code, price, lots}] 返回收益通知列表"""
+ hmap = get_holdings(info)
+ qmap = get_quotes(info)
+ today = date.today().isoformat()
+ msgs = []
+ errors = []
+
+ all_codes = [e["code"] for e in entries if e["code"] in hmap]
+ try:
+ live = fetch_quotes(all_codes) if all_codes else {}
+ except Exception:
+ live = {}
+
+ for e in entries:
+ code = e["code"]
+ price = float(e["price"])
+ sell_lots = float(e["lots"])
+ if code not in hmap:
+ errors.append(f"{code} 无持仓记录")
+ continue
+ h = hmap[code]
+ if sell_lots > h["available_lots"]:
+ errors.append(f"{code} 可用手数不足(可用 {h['available_lots']},卖出 {sell_lots})")
+ continue
+
+ q = live.get(code, {})
+ prev_close = q.get("prev_close") or q.get("price") or h["cost"]
+
+ # 减仓操作盈亏:(卖出价 - 0轴) * 手数 * 每手股数
+ # open_lots:操作前持仓(此时 h["lots"] 还未扣减)
+ open_lots = h["lots"]
+ op_pnl = (price - prev_close) * sell_lots * lot_size(code)
+ _record_operate(info, code, op_pnl, open_lots, today)
+
+ # 更新持仓(成本不变,只减手数)
+ h["lots"] -= sell_lots
+ h["available_lots"] -= sell_lots
+
+ msgs.append(f"{h['name']} 卖出 {sell_lots} 手,操作收益 {fmt(op_pnl)}")
+
+ # 减仓至 0 时保留持仓记录(lots=0),用于当日继续展示收益
+ # 只有收盘结算后才清除,不在此处删除
+
+ info["holdings"] = list(hmap.values())
+ info["quote"] = list(qmap.values())
+ save_info(info)
+ return msgs, errors
+
+# ── 收盘写入 / 次日清空 ───────────────────────────────────────────────────────
+def write_closing_quote(info, quotes, session="afternoon"):
+ """收盘时把最终行情写入 quote;session='noon' 为中午快照,'afternoon' 为收盘定稿"""
+ now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
+ hmap = get_holdings(info)
+ rows = []
+ for code, h in hmap.items():
+ q = quotes.get(code)
+ if not q:
+ continue
+ shares = h["lots"] * lot_size(code)
+ cost = h["cost"]
+ price = q["price"]
+ prev = q.get("prev_close") or price
+ change = q.get("change", 0.0)
+ day_pnl = change * shares
+ day_pct = (change / prev * 100) if prev else 0
+ total_pnl = (price - cost) * shares
+ total_pct = ((price - cost) / cost * 100) if cost else 0
+ rows.append({
+ "code": code,
+ "price": round(price, 4),
+ "market_val": round(price * shares, 2),
+ "day_pnl": round(day_pnl, 2),
+ "day_pct": round(day_pct, 2),
+ "total_pnl": round(total_pnl, 2),
+ "total_pct": round(total_pct, 2),
+ "date": now_str,
+ })
+ info["quote"] = rows
+ # 收盘结算:清除当天减仓至 0 的持仓记录(仅下午收盘时清理)
+ if session == "afternoon":
+ info["holdings"] = [h for h in info.get("holdings", []) if h.get("lots", 0) > 0]
+ save_info(info)
+
+def reset_for_new_day(info):
+ pass # 不再需要,保留占位避免引用报错
+
+# ── HTML 表单弹窗 ──────────────────────────────────────────────────────────────
+HTML_TEMPLATE = """
+
+
+
+
+{title}
+
+
+
+
+
{title}
+
{hint}
+
+
+
+
+
+
+
+
+
+"""
+
+FORM_CONFIGS = {
+ "init": {
+ "title": "初始化持仓",
+ "hint": "填写股票代码、成本均价和持仓手数,可添加多条",
+ "fields": [
+ {"key": "code", "label": "股票代码",
+ "placeholder": "如 603601", "hint": "6位沪深股票代码"},
+ {"key": "cost", "label": "成本均价",
+ "placeholder": "如 17.23", "hint": "建仓均价(元/股)", "inputmode": "decimal"},
+ {"key": "lots", "label": "持仓手数",
+ "placeholder": "如 2", "hint": "1手=100股,科创板200股", "inputmode": "decimal"},
+ ],
+ },
+ "buy": {
+ "title": "加仓",
+ "hint": "选择已有持仓或手动输入代码,填写买入价和手数",
+ "fields": [
+ {"key": "code", "label": "股票",
+ "hint": "可从持仓选择或手动输入新代码"},
+ {"key": "price", "label": "买入价",
+ "placeholder": "如 17.50", "hint": "本次买入价格(元/股)", "inputmode": "decimal"},
+ {"key": "lots", "label": "买入手数",
+ "placeholder": "如 1", "hint": "1手=100股,科创板200股", "inputmode": "decimal"},
+ ],
+ },
+ "sell": {
+ "title": "减仓",
+ "hint": "从持仓中选择股票,填写卖出价和手数",
+ "fields": [
+ {"key": "code", "label": "持仓股票",
+ "hint": "只能从当前持仓中选择"},
+ {"key": "price", "label": "卖出价",
+ "placeholder": "如 18.00", "hint": "本次卖出价格(元/股)", "inputmode": "decimal"},
+ {"key": "lots", "label": "卖出手数",
+ "placeholder": "如 1", "hint": "不超过可用手数", "inputmode": "decimal"},
+ ],
+ },
+}
+
+def run_form(cmd):
+ """启动本地 HTTP server,弹出浏览器表单,阻塞直到用户提交或取消"""
+ cfg = FORM_CONFIGS[cmd]
+ result = {"done": False}
+ info = load_info()
+
+ # 持仓列表注入页面(code + name)
+ holdings_list = [{"code": h["code"], "name": h["name"]}
+ for h in info.get("holdings", [])]
+
+ # grid 列宽:code 列稍宽,其余等分
+ col_tpl = "1.4fr 1fr 1fr"
+
+ class Handler(BaseHTTPRequestHandler):
+ def log_message(self, *a): pass
+
+ def do_GET(self):
+ if self.path == "/":
+ html = HTML_TEMPLATE.format(
+ title = cfg["title"],
+ hint = cfg["hint"],
+ fields_json = json.dumps(cfg["fields"], ensure_ascii=False),
+ holdings_json= json.dumps(holdings_list, ensure_ascii=False),
+ cmd = cmd,
+ col_tpl = col_tpl,
+ )
+ self._respond(200, "text/html", html.encode())
+ elif self.path == "/cancel":
+ result["done"] = True
+ self._respond(200, "application/json", b'{"ok":true}')
+ else:
+ self._respond(404, "text/plain", b"not found")
+
+ def do_POST(self):
+ if self.path == "/submit":
+ length = int(self.headers.get("Content-Length", 0))
+ body = self.rfile.read(length)
+ entries = json.loads(body)
+ # 校验 + 执行
+ err = None
+ try:
+ if cmd == "init":
+ apply_init(info, entries)
+ notify(cfg["title"] + f" {len(entries)} 条已保存")
+ elif cmd == "buy":
+ apply_buy(info, entries)
+ notify(cfg["title"] + f" {len(entries)} 条已保存")
+ elif cmd == "sell":
+ msgs, errors = apply_sell(info, entries)
+ if errors:
+ err = ";".join(errors)
+ else:
+ notify("\n".join(msgs))
+ except Exception as ex:
+ err = str(ex)
+
+ if err:
+ resp = json.dumps({"ok": False, "error": err}).encode()
+ else:
+ result["done"] = True
+ resp = json.dumps({"ok": True}).encode()
+ self._respond(200, "application/json", resp)
+ else:
+ self._respond(404, "text/plain", b"not found")
+
+ def _respond(self, code, ct, body):
+ self.send_response(code)
+ self.send_header("Content-Type", ct + "; charset=utf-8")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ server = HTTPServer(("127.0.0.1", 0), Handler)
+ port = server.server_address[1]
+
+ # 后台处理请求
+ def serve():
+ while not result["done"]:
+ server.handle_request()
+ server.server_close()
+
+ t = threading.Thread(target=serve, daemon=True)
+ t.start()
+
+ subprocess.Popen(["open", f"http://127.0.0.1:{port}/"])
+ t.join(timeout=120) # 最多等 2 分钟
+
+# ── 系统通知 ──────────────────────────────────────────────────────────────────
+def notify(msg):
+ safe = msg.replace('"', '\\"')
+ subprocess.run(
+ ["osascript", "-e", f'display notification "{safe}" with title "股票助手"'],
+ capture_output=True,
+ )
+
+# ── 菜单输出 ──────────────────────────────────────────────────────────────────
+FONT = "font=Menlo size=12"
+
+# 各列视觉宽度(英文字符数):中文字符算2宽
+# col1:股票名(5中文=10) col2:今日盈亏 col3:成本/现价(涨幅) col4:总盈亏 col5:持仓/可用
+COL_WIDTHS = [16, 18, 20, 18, 8]
+
+def visual_len(s):
+ """计算字符串的视觉宽度(中文/全角算2,其余算1)"""
+ w = 0
+ for c in s:
+ w += 2 if ord(c) > 127 else 1
+ return w
+
+def pad(s, width):
+ """按视觉宽度右填充空格"""
+ return s + " " * max(0, width - visual_len(s))
+
+def _make_row(c1, c2, c3, c4, c5):
+ cols = [c1, c2, c3, c4, c5]
+ return " ".join(pad(c, w) for c, w in zip(cols, COL_WIDTHS))
+
+HEADER_ROW = _make_row("股票名/市值", "今日盈亏¥/%", "成本/现价(涨幅)", "总盈亏¥/%", "持仓/可用")
+
+def print_menu():
+ status = trading_status()
+ info = load_info()
+ _clean_operate(info) # 清除非今日操作记录
+ hmap = get_holdings(info)
+ qmap = get_quotes(info)
+
+ # 周末
+ if status == "weekend":
+ print("Freedom")
+ print("---")
+ if hmap:
+ print(f"{HEADER_ROW} | {FONT}")
+ _render_static(hmap, qmap, info)
+ _print_buttons()
+ return
+
+ # 无持仓
+ if not hmap:
+ print("Working")
+ print("---")
+ _print_buttons()
+ return
+
+ # 非交易时段:直接读 quote,不请求接口
+ if status == "closed":
+ print("Working")
+ print("---")
+ print(f"{HEADER_ROW} | {FONT}")
+ _render_static(hmap, qmap, info)
+ _print_buttons()
+ return
+
+ # 午休(11:30–12:59)或收盘时段(15:00–15:59):检查 quote 是否已有当段数据
+ if status in ("closing_noon", "closing"):
+ today = date.today().isoformat()
+ threshold = today + (" 11:30" if status == "closing_noon" else " 15:00")
+ has_today = bool(qmap) and all(q.get("date", "") >= threshold for q in qmap.values())
+ if has_today:
+ print("Working")
+ print("---")
+ print(f"{HEADER_ROW} | {FONT}")
+ _render_static(hmap, qmap, info)
+ _print_buttons()
+ return
+ # 没有今日数据,拉取实时行情并写入
+ try:
+ quotes = fetch_quotes(list(hmap.keys()))
+ except Exception:
+ print("Err")
+ print("---")
+ _print_buttons()
+ return
+ session = "noon" if status == "closing_noon" else "afternoon"
+ write_closing_quote(info, quotes, session=session)
+ info = load_info()
+ qmap = get_quotes(info)
+ print("Working")
+ print("---")
+ print(f"{HEADER_ROW} | {FONT}")
+ _render_static(hmap, qmap, info)
+ _print_buttons()
+ return
+
+ # 16:00+ 纯展示缓存
+ if status == "after_close":
+ print("Working")
+ print("---")
+ if qmap:
+ print(f"{HEADER_ROW} | {FONT}")
+ _render_static(hmap, qmap, info)
+ _print_buttons()
+ return
+
+ # 交易时段(open / closing):实时拉行情,忽略 quote
+ try:
+ quotes = fetch_quotes(list(hmap.keys()))
+ except Exception:
+ print("Err")
+ print("---")
+ _print_buttons()
+ return
+
+ _render(hmap, quotes, info)
+ _print_buttons()
+
+
+def _render(hmap, quotes, info=None):
+ if info is None:
+ info = load_info()
+ op_map = get_today_operate(info)
+ total_day_pnl = 0.0
+ rows = []
+ for code, h in hmap.items():
+ q = quotes.get(code)
+ if not q:
+ continue
+ shares = h["lots"] * lot_size(code)
+ cost = h["cost"]
+ price = q["price"]
+ market_val = price * shares
+ prev_close = q.get("prev_close") or price
+
+ # 今日行情盈亏 + 操作盈亏
+ day_pnl_market = q["change"] * shares
+ op = op_map.get(code)
+ op_pnl = op["pnl"] if op else 0.0
+ day_pnl = day_pnl_market + op_pnl
+
+ # 当日盈亏比:分母 = 昨收 × 开盘持仓股数
+ open_lots = op.get("open_lots", h["lots"]) if op else h["lots"]
+ open_shares = open_lots * lot_size(code)
+ day_base = prev_close * open_shares
+ day_pct = (day_pnl / day_base * 100) if day_base else 0
+
+ # 总盈亏 = 浮动 + 所有操作盈亏
+ all_op_pnl = sum(o["pnl"] for o in info.get("operate", []) if o["code"] == code)
+ total_pnl = (price - cost) * shares + all_op_pnl
+ # lots=0 时用开盘持仓手数作为成本基准(防止除零)
+ cost_lots = shares if shares > 0 else open_shares
+ total_cost = cost * cost_lots
+ total_pct = (total_pnl / total_cost * 100) if total_cost else 0
+
+ # 行情涨幅
+ price_pct = q.get("pct", 0.0)
+
+ total_day_pnl += day_pnl
+ rows.append((market_val, h["name"], day_pnl, day_pct, cost, price, price_pct,
+ total_pnl, total_pct, h["lots"], h["available_lots"]))
+
+ rows.sort(key=lambda r: r[0], reverse=True)
+ print(f"{fmt(total_day_pnl)}")
+ print("---")
+ print(f"{HEADER_ROW} | {FONT}")
+ for r in rows:
+ market_val, name = r[0], r[1]
+ name_mv = f"{name}/{market_val:.0f}"
+ print(f"{_row(name_mv, *r[2:])} | {FONT}")
+
+
+def _render_static(hmap, qmap, info=None):
+ """非交易时段:从 quote 缓存渲染,无行情显示 -"""
+ if info is None:
+ info = load_info()
+ op_map = get_today_operate(info)
+ print("---")
+ rows = []
+ for code, h in hmap.items():
+ q = qmap.get(code)
+ if q:
+ market_val = q.get("market_val", q["price"] * h["lots"] * lot_size(code))
+ rows.append((market_val, h, q))
+ else:
+ rows.append((0.0, h, None))
+ rows.sort(key=lambda r: r[0], reverse=True)
+ for market_val, h, q in rows:
+ if q:
+ code = h["code"]
+ shares = h["lots"] * lot_size(code)
+ cost = h["cost"]
+ price = q["price"]
+
+ op = op_map.get(code)
+ op_pnl = op["pnl"] if op else 0.0
+ day_pnl = q["day_pnl"] + op_pnl
+
+ # 当日盈亏比:分母 = 昨收价 × 开盘持仓股数
+ # 静态缓存没有昨收,用缓存价格兜底
+ open_lots = op.get("open_lots", h["lots"]) if op else h["lots"]
+ open_shares = open_lots * lot_size(code)
+ day_base = price * open_shares # 静态模式用缓存价代替昨收
+ day_pct = (day_pnl / day_base * 100) if day_base else q["day_pct"]
+
+ all_op_pnl = sum(o["pnl"] for o in info.get("operate", []) if o["code"] == code)
+ total_pnl = q["total_pnl"] + all_op_pnl
+ # lots=0 时用开盘持仓手数作为成本基准(防止除零)
+ cost_shares = shares if shares > 0 else open_shares
+ total_cost = cost * cost_shares
+ total_pct = (total_pnl / total_cost * 100) if total_cost else q["total_pct"]
+
+ price_pct = q.get("day_pct", 0.0) # 缓存里的行情涨幅
+
+ name_mv = f"{h['name']}/{market_val:.0f}"
+ line = _row(name_mv, day_pnl, day_pct, cost, price, price_pct,
+ total_pnl, total_pct, h["lots"], h["available_lots"])
+ else:
+ line = _make_row(h["name"], "-", f"{h['cost']:.2f}/-", "-",
+ f"{h['lots']:.0f}/{h['available_lots']:.0f}")
+ print(f"{line} | {FONT}")
+
+
+def _row(name, day_pnl, day_pct, cost, price, price_pct, total_pnl, total_pct, lots, avail):
+ return _make_row(
+ name,
+ f"{fmt(day_pnl)}/{fmt(day_pct)}%",
+ f"{cost:.2f}/{price:.2f}({fmt(price_pct)}%)",
+ f"{fmt(total_pnl)}/{fmt(total_pct)}%",
+ f"{lots:.0f}/{avail:.0f}",
+ )
+
+
+def _print_buttons():
+ script = os.path.abspath(__file__)
+ print("---")
+ print(f"初始化持仓 | bash={script} param1=init terminal=false refresh=true")
+ print(f"加仓 | bash={script} param1=buy terminal=false refresh=true")
+ print(f"减仓 | bash={script} param1=sell terminal=false refresh=true")
+
+
+# ── 入口 ──────────────────────────────────────────────────────────────────────
+if __name__ == "__main__":
+ if len(sys.argv) > 1:
+ cmd = sys.argv[1]
+ if cmd in ("init", "buy", "sell"):
+ run_form(cmd)
+ else:
+ print_menu()