scripts/plan.py
6.6 KB| 1 | # -*- coding: utf-8 -*- |
| 2 | """每日操作指令(激进版):给「买什么、卖什么、买多少、什么时候」一个明确答案。 |
| 3 | |
| 4 | 配置:核心 40%(红利 20% + 短债 20%,分批建仓,不看信号) |
| 5 | 卫星 60%(三个高弹性指数,各自站上 60 日线才进场,跌破分批退) |
| 6 | |
| 7 | 用法:python fund/plan.py |
| 8 | 台账:fund/holdings.json(cash + positions),每次成交后改这个文件。 |
| 9 | """ |
| 10 | import os, sys, json, datetime |
| 11 | import numpy as np |
| 12 | |
| 13 | HERE = os.path.dirname(os.path.abspath(__file__)) |
| 14 | Q = os.path.dirname(HERE) |
| 15 | sys.path.insert(0, Q) |
| 16 | sys.path.insert(0, HERE) |
| 17 | sys.stdout.reconfigure(encoding="utf-8", errors="replace") |
| 18 | import nav |
| 19 | |
| 20 | BOOK = os.path.join(HERE, "holdings.json") |
| 21 | |
| 22 | # 核心:底仓,不看信号,按周买满 |
| 23 | CORE = [ |
| 24 | {"key": "红利", "code": "007466", "name": "华泰柏瑞中证红利低波ETF联接A", "weight": 0.20, "dca_weeks": 4}, |
| 25 | {"key": "短债", "code": "006965", "name": "财通安瑞短债债券A", "weight": 0.20, "dca_weeks": 2}, |
| 26 | ] |
| 27 | |
| 28 | # 卫星:高弹性,站上 60 日线才买,跌破就退 |
| 29 | SAT = [ |
| 30 | {"key": "中证1000", "code": "005313", "name": "万家中证1000指数增强A", "weight": 0.20}, |
| 31 | {"key": "创业板", "code": "110026", "name": "易方达创业板ETF联接A", "weight": 0.20}, |
| 32 | {"key": "科创50", "code": "011612", "name": "华夏科创50ETF联接A", "weight": 0.20}, |
| 33 | ] |
| 34 | |
| 35 | |
| 36 | def ma(v, n): |
| 37 | o = np.full(len(v), np.nan) |
| 38 | if len(v) >= n: |
| 39 | c = np.cumsum(np.insert(v, 0, 0)) |
| 40 | o[n - 1:] = (c[n:] - c[:-n]) / n |
| 41 | return o |
| 42 | |
| 43 | |
| 44 | def probe(code): |
| 45 | h = nav.history(code, need=320) |
| 46 | if len(h) < 70: |
| 47 | return None |
| 48 | d = [x[0] for x in h] |
| 49 | c = np.array([x[1] for x in h], dtype=float) |
| 50 | m60 = ma(c, 60) |
| 51 | above = bool(c[-1] > m60[-1]) |
| 52 | side = c > m60 |
| 53 | streak = 0 |
| 54 | for i in range(len(c) - 1, -1, -1): |
| 55 | if np.isnan(m60[i]): |
| 56 | break |
| 57 | if side[i] == above: |
| 58 | streak += 1 |
| 59 | else: |
| 60 | break |
| 61 | return {"date": d[-1], "price": float(c[-1]), "ma60": float(m60[-1]), |
| 62 | "above": above, "streak": streak, "gap": float((c[-1] / m60[-1] - 1) * 100)} |
| 63 | |
| 64 | |
| 65 | def load_book(): |
| 66 | if not os.path.exists(BOOK): |
| 67 | b = {"cash": 10000.0, "capital": 10000.0, "positions": {}, "log": []} |
| 68 | with open(BOOK, "w", encoding="utf-8") as f: |
| 69 | json.dump(b, f, ensure_ascii=False, indent=2) |
| 70 | return b |
| 71 | with open(BOOK, encoding="utf-8") as f: |
| 72 | return json.load(f) |
| 73 | |
| 74 | |
| 75 | def value_of(book, key): |
| 76 | p = book["positions"].get(key) |
| 77 | return float(p.get("value", 0)) if p else 0.0 |
| 78 | |
| 79 | |
| 80 | def main(): |
| 81 | book = load_book() |
| 82 | cap = float(book.get("capital", 10000)) |
| 83 | cash = float(book.get("cash", 0)) |
| 84 | orders, info = [], {} |
| 85 | |
| 86 | # ---- 核心 ---- |
| 87 | for c in CORE: |
| 88 | p = probe(c["code"]) |
| 89 | if not p: |
| 90 | continue |
| 91 | info[c["key"]] = p |
| 92 | target = cap * c["weight"] |
| 93 | cur = value_of(book, c["key"]) |
| 94 | gap = target - cur |
| 95 | step = target / c["dca_weeks"] |
| 96 | if gap >= max(step * 0.5, 100) and cash >= 100: |
| 97 | amt = min(step, gap, cash) |
| 98 | orders.append({"act": "买入", "key": c["key"], "code": c["code"], "name": c["name"], |
| 99 | "amount": round(amt), |
| 100 | "why": "底仓没建满(%d/%d),本周第 %d 笔" % (cur, target, int(cur / step) + 1)}) |
| 101 | cash -= amt |
| 102 | |
| 103 | # ---- 卫星 ---- |
| 104 | for s in SAT: |
| 105 | p = probe(s["code"]) |
| 106 | if not p: |
| 107 | continue |
| 108 | info[s["key"]] = p |
| 109 | target = cap * s["weight"] |
| 110 | cur = value_of(book, s["key"]) |
| 111 | if p["above"] and p["streak"] <= 3 and cur < target * 0.6: |
| 112 | amt = min(target * 0.5, target - cur, cash) |
| 113 | if amt >= 100: |
| 114 | orders.append({"act": "买入", "key": s["key"], "code": s["code"], "name": s["name"], |
| 115 | "amount": round(amt), |
| 116 | "why": "刚站上 60 日线(第 %d 天,%+.1f%%),先建一半" % (p["streak"], p["gap"])}) |
| 117 | cash -= amt |
| 118 | elif p["above"] and p["streak"] >= 4 and cur < target * 0.95: |
| 119 | amt = min(target - cur, cash) |
| 120 | if amt >= 100: |
| 121 | orders.append({"act": "买入", "key": s["key"], "code": s["code"], "name": s["name"], |
| 122 | "amount": round(amt), |
| 123 | "why": "站稳 60 日线第 %d 天,把剩下的一半补上" % p["streak"]}) |
| 124 | cash -= amt |
| 125 | elif (not p["above"]) and cur > 0 and p["streak"] <= 3: |
| 126 | orders.append({"act": "卖出", "key": s["key"], "code": s["code"], "name": s["name"], |
| 127 | "amount": round(cur / 2), |
| 128 | "why": "跌破 60 日线(%+.1f%%),先退一半,别一次清" % p["gap"]}) |
| 129 | elif (not p["above"]) and cur > 0 and p["streak"] >= 4: |
| 130 | orders.append({"act": "清仓", "key": s["key"], "code": s["code"], "name": s["name"], |
| 131 | "amount": round(cur), |
| 132 | "why": "连续 %d 天在 60 日线下方(%+.1f%%),确认走弱,全部退出" % (p["streak"], p["gap"])}) |
| 133 | |
| 134 | # ---- 输出 ---- |
| 135 | today = datetime.date.today().isoformat() |
| 136 | d = (info.get("创业板") or info.get("红利") or {}).get("date", today) |
| 137 | L = ["# 今天的基金指令 · %s" % today, "", |
| 138 | "> 数据日期 %s;本金 %d,现金 %d;激进配置(核心 40%% + 卫星 60%%)" % (d, cap, book.get("cash", 0)), ""] |
| 139 | if not orders: |
| 140 | L += ["## 今天不用动", "", "没有触发任何规则,什么都不用买也不用卖。"] |
| 141 | else: |
| 142 | L += ["## 今天要做的事", "", "| 动作 | 基金 | 代码 | 金额 | 为什么 |", "|---|---|---|---|---|"] |
| 143 | for o in orders: |
| 144 | L.append("| **%s** | %s | %s | %d 元 | %s |" % (o["act"], o["name"], o["code"], o["amount"], o["why"])) |
| 145 | L += ["", "### 支付宝里怎么点", ""] |
| 146 | for o in orders: |
| 147 | L.append("- %s:搜 %s(%s),选 **A 类**,金额 %d,15:00 前提交按当天净值成交。" |
| 148 | % (o["act"], o["code"], o["name"], o["amount"])) |
| 149 | L += ["", "## 现在的位置", "", "| 标的 | 净值 | 60日线 | 偏离 | 状态 |", "|---|---|---|---|---|"] |
| 150 | for k, v in info.items(): |
| 151 | L.append("| %s | %.4f | %.4f | %+.1f%% | %s %d 天 |" % ( |
| 152 | k, v["price"], v["ma60"], v["gap"], "线上" if v["above"] else "线下", v["streak"])) |
| 153 | out = "\n".join(L) |
| 154 | print(out) |
| 155 | os.makedirs(os.path.join(Q, "out"), exist_ok=True) |
| 156 | p = os.path.join(Q, "out", "指令-%s.md" % today) |
| 157 | with open(p, "w", encoding="utf-8") as f: |
| 158 | f.write(out + "\n") |
| 159 | print("\n已写 %s" % p) |
| 160 | |
| 161 | |
| 162 | if __name__ == "__main__": |
| 163 | main() |
文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。

