Hi5个人魔改版投资策略与自动化执行手册

Hi5个人魔改版投资策略与自动化执行手册

作者: shisaq 日期: February 3, 2026

这份笔记旨在参考雷公的原策略基础上,客观记录”新Hi5”投资组合的构建邏輯、交易规则、建仓策略及自动化监控系统的技术实现。内容去除了情绪化表达,仅保留核心参数与操作指南。

2026-06-12 修订:本金确定为 $150,000;恢复 GLD 10%(对冲滞胀,压低 IWY/SPMO 的 AI 重叠敞口);VIX 极端模式由 all-in 改为 35/45/55 三段阶梯;新增 executions.csv 执行台账与”傻瓜执行单”邮件;明确建仓完成后的巡航模式规则;回撤目标改为诚实的两档表述。代码以 GitHub 仓库 为准。


核心理念: 核心-卫星策略 (Core-Satellite) + 平均成本法 (DCA) + 逆势布局 (BTD)

风险偏好: 进取型(含防御机制)

收益目标: 7-10% 平均年化;回撤容忍:常规熊市 ≤20%,极端年份 ≤25%


1. 资产配置 (Target Allocation)

组合采用六资产配置,每年8月日历再平衡 + ±5个百分点偏离带(先到先触发)。

代码 资产类型 角色定位 权重 备注
IWY 罗素Top200成长 Alpha (矛) 15% 大盘成长,捕捉科技巨头收益
SPMO 标普500动量 Alpha (风) 15% 动量因子,捕捉市场最强趋势
RSP 标普500等权 Beta (锚) 20% 均值回归,市场宽度的物理化身
SCHD 红利增长 Shield (盾) 20% 防御性价值
GLD 黄金 Shield (滞胀盾) 10% 股债双杀/滞胀场景的对冲位
SGOV 超短债/现金 Cash (粮) 20% 极低波动,危机时刻的抄底弹药

注 1:原策略中的 PFF (优先股) 和 VNQ (地产) 因流动性风险及利率敏感度被剔除,分别由 SGOV 和 SCHD 替代。

注 2 (2026-06):IWY 与 SPMO 在 AI 行情下持仓高度重叠(英伟达/博通等半导体在两者前排均占重仓),两者合计从 40% 降至 30%,腾出的 10% 给 GLD——组合的真实科技集中度应按约 30-35% 估计,而非表面权重。


2. 交易规则 (Trading Rules)

2.1 常态操作 (DCA,仅建仓期)

  • 时间:每月 1 日。
  • 操作:固定金额买入,按五只风险 ETF 的目标权重比例拆分(IWY/SPMO 各 18.75%、RSP/SCHD 各 25%、GLD 12.5%)。

2.2 逆势加仓 (BTD - Buy The Dip,仅建仓期)

基于 RSP (标普等权) 的表现判断市场情绪:

  1. 一级信号:RSP 单日跌幅达到 -1%
    • 操作:当月实施第一次额外增持(每月最多一次,由执行台账判定)。
  2. 二级信号:RSP 月度累计跌幅达到 -5%
    • 操作:实施第二次大额增持(黄金坑)。
  3. 保底机制
    • 若执行台账显示当月无任何买入,在第三个星期五收盘前强制实施一次增持。

2.3 极端行情应对:VIX 阶梯 (取代旧版 all-in)

旧版”VIX>35 卖出全部现金 all-in”在 2008/2020 式行情(VIX 冲 80)下会过早打光弹药,已废弃。新规则:

档位 触发条件 投入金额
一档 VIX 收盘 > 35 剩余现金弹药的 1/3
二档 VIX 收盘 > 45 再 1/3
三档 VIX 收盘 > 55 最后 1/3
  • 触发期间暂停所有常规 DCA 和 BTD
  • 重置规则:VIX 收盘回落破 25 视为本轮恐慌结束;未触发档位作废,剩余资金并入之后三次月度定投摊平。
  • 已触发档位由 executions.csv 台账识别,同一轮恐慌不重复触发。
  • 阶梯买入采用狙击配置:IWY 30% / RSP 30% / SCHD 20% / SPMO 10% / GLD 10%。

:低配 SPMO 是因为在市场急转弯(V型反转)时,动量因子容易崩盘 (momentum crash)。

2.4 再平衡规则

  • 日历再平衡:每年 8 月 1-5 日,无条件恢复目标权重。
  • 偏离带再平衡:任一资产权重绝对偏离目标 >5 个百分点(如 20% 漂到 >25% 或 <15%)即触发。建仓期内偏离带不生效。
  • 执行原则:优先用现金流修正(新买入全部投向最低配资产),尽量少卖出。

2.5 信号优先级

当多个条件同日满足时,按以下优先级执行:

  1. 年度再平衡 > 一切(8月1-5日无条件执行)
  2. VIX 阶梯 > 所有常规操作(触发期间 DCA/BTD 全部暂停)
  3. 月跌 -5% > 日跌 -1%(同日触发时,只执行 -5% 的大额加仓,不重复加仓)
  4. 月度 DCABTD 可叠加(1号且日跌1%时,两笔都执行)
  5. 保底机制 仅在台账显示本月无任何买入时触发

3. 过渡期建仓策略 (Transition Strategy)

针对本金 $150,000 全额现金、市场处于高位的现状,采用”非对称加速建仓”方案:闲置资金即刻停泊于 SGOV(吃约 4% 无风险收益),随信号分批转入五只风险 ETF。

目标:风险资产五件套合计 80%($120,000),SGOV 余量 20%($30,000)。

周期:预计 12-16 个月(市场波动越大,建仓自动越快)。

资金参数配置 (BUILD_RULES):

场景 触发条件 投入金额 (USD) 资金来源 倍数
基础定投 每月 1 日 $7,500 SGOV 1x
轻微回调 RSP 日跌 -1% $7,500 SGOV 1x
深度回调 RSP 月跌 -5% $18,000 → $22,500 SGOV 3x
极端恐慌 VIX 阶梯 35/45/55 剩余弹药各 1/3 SGOV

建仓完成判定:现金(SGOV)占总资产比例降至 20% 时,系统邮件会提示将代码中 IS_BUILDING_PHASE 置为 False,切换巡航模式。

巡航模式 (建仓完成后)

本金固定、无新增资金的运行形态。月度 DCA、BTD、保底机制全部关闭,系统只剩三个零件:

  1. 8月日历再平衡 + 偏离带——再平衡接管 BTD 职能:暴跌时卖 SGOV/GLD 低位买股票,复苏后获利了结补回弹药,自我闭环;
  2. VIX 阶梯——紧急加速器,巡航期 SGOV 保留 10% 底仓不打光;
  3. 每月 1 号组合体检报告——持仓、权重、偏离度,保持系统存在感。

4. 自动化系统架构 (Technical Implementation)

系统基于 GitHub Actions 和 Python 实现无服务器自动监控,代码仓库:permanent-portfolio-monitor

  • 运行时间:每个交易日 UTC 21:30 (美股收盘后)。
  • 数据源:Yahoo Finance (yfinance)。
  • 通知方式:SMTP 邮件推送。

4.1 目录结构

1
2
3
4
5
6
7
.
├── .github/
│   └── workflows/
│       └── hi5_us_check.yml  # 调度配置文件
├── monitor_hi5.py            # 核心策略脚本
├── executions.csv            # 执行台账(系统的状态存储)
└── ...

4.2 执行台账 (executions.csv)

2026-06 新增。脚本本身无状态,台账是它的记忆:每笔实际成交后手动追加一行并 push。

date,type,ticker,shares,price,amount,note
2026-07-01,DCA,IWY,4.92,285.91,1406,7月定投
  • type: DCA / BTD1 / BTD2 / VIX35 / VIX45 / VIX55 / REBAL;卖出记负数。
  • 台账驱动四个能力:① 邮件显示本月执行笔数与距上次买入天数(0 笔时红字警告——把”观望”变成可见的事实);② 保底机制可真实判断本月是否买过;③ VIX 阶梯识别已触发档位;④ 巡航模式由台账+实时价格算出当前权重,监控偏离带。
  • 信号邮件同时是傻瓜执行单:直接给出每只 ETF 的美元金额和估算股数,照抄下单,全程无需思考。

4.3 核心代码 (monitor_hi5.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import yfinance as yf
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
import datetime
import os
import csv
import pandas as pd

# --- 环境变量 (兼容现有 Secrets) ---
EMAIL_SENDER = os.environ.get("MAIL_USER")
EMAIL_PASSWORD = os.environ.get("MAIL_PASS")
EMAIL_RECEIVER = os.environ.get("MAIL_USER")

# ==============================================================================
# 🏗️ 全局配置 (2026-06 修订版)
# 当建仓完成 (现金余量 <= 总资产20%),请将 IS_BUILDING_PHASE 改为 False
# ==============================================================================
IS_BUILDING_PHASE = True

TOTAL_CAPITAL = 150_000  # 总本金 (USD),闲置部分停泊于 SGOV

# 目标配置:六件套 (IWY/SPMO 降为各15%,恢复 GLD 10% 对冲滞胀)
TARGET_WEIGHTS = {
    "IWY": 0.15,   # 矛: 大盘成长
    "SPMO": 0.15,  # 风: 动量
    "RSP": 0.20,   # 锚: 标普等权
    "SCHD": 0.20,  # 盾: 红利增长
    "GLD": 0.10,   # 滞胀盾: 黄金
    "SGOV": 0.20,  # 粮: 现金等价物
}
RISK_TICKERS = ["IWY", "SPMO", "RSP", "SCHD", "GLD"]  # 建仓买入对象 (SGOV=资金来源)

# 建仓期资金规则 (单位: 美元)。买入金额按风险资产目标权重比例拆分。
BUILD_RULES = {
    "monthly_base": 7500,   # 每月1号固定投入 (DCA) ≈ 本金的5%
    "dip_1pct": 7500,       # RSP日跌1% 额外投入 (BTD小,每月最多一次)
    "dip_5pct": 22500,      # RSP月跌5% 额外投入 (BTD大, 3x)
    "source_asset": "SGOV", # 资金来源
}

# VIX 阶梯 (取代旧版 all-in):每档触发后投入"剩余现金"的对应比例
VIX_LADDER = [(35, 0.33), (45, 0.33), (55, 0.34)]
VIX_RESET = 25  # VIX 收盘回落破此值 => 本轮恐慌结束,未触发档位作废,余量并入后续定投

# 狙击模式买入比例 (VIX 阶梯触发时):低配 SPMO 防动量崩盘
SNIPER_WEIGHTS = {"IWY": 0.30, "RSP": 0.30, "SCHD": 0.20, "SPMO": 0.10, "GLD": 0.10}

# 再平衡:8月日历再平衡 + 5个百分点绝对偏离带,先到先触发;建仓期偏离带不生效
REBALANCE_BAND = 0.05

EXECUTIONS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "executions.csv")
# ==============================================================================


def get_market_data():
    """获取行情:RSP/VIX 一年历史 + 全部标的最新价"""
    rsp_hist = yf.Ticker("RSP").history(period="1y")
    vix_hist = yf.Ticker("^VIX").history(period="1y")
    prices = {}
    for t in RISK_TICKERS + ["SGOV"]:
        try:
            prices[t] = yf.Ticker(t).history(period="5d")["Close"].iloc[-1]
        except Exception:
            prices[t] = None
    return rsp_hist, vix_hist, prices


def load_executions():
    """读取执行台账。格式: date,type,ticker,shares,price,amount,note
    type: DCA / BTD1 / BTD2 / VIX35 / VIX45 / VIX55 / REBAL
    卖出记负数 shares/amount。SGOV 行会被忽略 (现金按 本金-净投入 推算)。"""
    rows = []
    if not os.path.exists(EXECUTIONS_FILE):
        return rows
    with open(EXECUTIONS_FILE, newline="", encoding="utf-8") as f:
        for row in csv.DictReader(f):
            if not row.get("date"):
                continue
            try:
                rows.append({
                    "date": datetime.date.fromisoformat(row["date"].strip()),
                    "type": row.get("type", "").strip().upper(),
                    "ticker": row.get("ticker", "").strip().upper(),
                    "shares": float(row.get("shares") or 0),
                    "amount": float(row.get("amount") or 0),
                })
            except ValueError:
                continue
    return rows


def portfolio_state(executions, prices):
    """由台账推算持仓、现金与当前权重"""
    holdings = {t: 0.0 for t in RISK_TICKERS}
    invested = 0.0
    for r in executions:
        if r["ticker"] in holdings:
            holdings[r["ticker"]] += r["shares"]
            invested += r["amount"]
    cash = TOTAL_CAPITAL - invested  # 停泊于 SGOV,按面值近似
    values = {t: holdings[t] * (prices.get(t) or 0) for t in RISK_TICKERS}
    total = sum(values.values()) + cash
    weights = {t: (values[t] / total if total else 0) for t in RISK_TICKERS}
    weights["SGOV"] = cash / total if total else 1.0
    return holdings, cash, total, weights


def vix_episode_start(vix_hist):
    """本轮恐慌起点 = 最近一次 VIX 收盘 < VIX_RESET 的日期"""
    calm = vix_hist[vix_hist["Close"] < VIX_RESET]
    return calm.index[-1].date() if not calm.empty else None


def fired_rungs(executions, episode_start):
    """本轮恐慌中已触发的 VIX 档位"""
    fired = set()
    for r in executions:
        if r["type"] in ("VIX35", "VIX45", "VIX55"):
            if episode_start is None or r["date"] > episode_start:
                fired.add(int(r["type"][3:]))
    return fired


def split_order(amount, weights, prices):
    """按比例拆单,输出每只 ETF 的金额与估算股数 (傻瓜执行单)"""
    lines = []
    for t, w in weights.items():
        dollars = amount * w
        p = prices.get(t)
        qty = f"约 {dollars / p:.2f} 股 @ ${p:.2f}" if p else "价格获取失败,请按金额下单"
        lines.append(f"   👉 {t}: ${dollars:,.0f} ({qty})")
    return lines


def build_buy_weights():
    """建仓期买入按风险资产目标权重的相对比例拆分"""
    risk_total = sum(TARGET_WEIGHTS[t] for t in RISK_TICKERS)
    return {t: TARGET_WEIGHTS[t] / risk_total for t in RISK_TICKERS}


def analyze_strategy(rsp_hist, vix_hist, prices, executions):
    signals = []
    today_date = datetime.datetime.now().date()

    today_close = rsp_hist["Close"].iloc[-1]
    prev_close = rsp_hist["Close"].iloc[-2]

    month_start = pd.Timestamp(today_date.replace(day=1), tz=rsp_hist.index.tz)
    month_data = rsp_hist[rsp_hist.index >= month_start]
    mtd_change = (today_close - month_data["Open"].iloc[0]) / month_data["Open"].iloc[0] if not month_data.empty else 0.0

    year_start = pd.Timestamp(today_date.replace(month=1, day=1), tz=rsp_hist.index.tz)
    year_data = rsp_hist[rsp_hist.index >= year_start]
    ytd_change = (today_close - year_data["Open"].iloc[0]) / year_data["Open"].iloc[0] if not year_data.empty else 0.0

    current_vix = vix_hist["Close"].iloc[-1]
    daily_change = (today_close - prev_close) / prev_close

    holdings, cash, total, weights = portfolio_state(executions, prices)
    month_execs = [r for r in executions if r["date"].year == today_date.year and r["date"].month == today_date.month]
    last_buy = max((r["date"] for r in executions if r["amount"] > 0), default=None)
    days_since_buy = (today_date - last_buy).days if last_buy else None

    # ========== 策略逻辑 (按优先级) ==========
    # 年度再平衡 > VIX阶梯 > BTD月跌5% > BTD日跌1% > 月度DCA > 保底检查
    # VIX 阶梯触发时,常规 DCA/BTD 暂停;月跌5% 触发时抑制日跌1%

    is_monthly_dip = mtd_change <= -0.05
    is_daily_dip = daily_change <= -0.01
    is_dca_day = today_date.day == 1
    is_rebalance = today_date.month == 8 and 1 <= today_date.day <= 5
    is_safety_net = today_date.weekday() == 4 and 15 <= today_date.day <= 21

    # 1. 年度再平衡 (最高优先级)
    if is_rebalance:
        tgt = " / ".join(f"{t} {w:.0%}" for t, w in TARGET_WEIGHTS.items())
        signals.append(f"📅【年度再平衡】8月窗口:请将组合恢复至目标配置 ({tgt})。优先用新买入修正,少卖出。")

    # 2. VIX 阶梯 (抑制常规 DCA/BTD)
    episode_start = vix_episode_start(vix_hist)
    fired = fired_rungs(executions, episode_start)
    pending_rungs = [(lv, ratio) for lv, ratio in VIX_LADDER if current_vix > lv and lv not in fired]
    vix_mode = current_vix > VIX_LADDER[0][0]

    if vix_mode:
        signals.append(f"🔥【VIX 阶梯模式】VIX = {current_vix:.2f}。⚠️ 常规定投/BTD 暂停。")
        if pending_rungs:
            ammo_floor = 0 if IS_BUILDING_PHASE else total * 0.10  # 巡航期 SGOV 保留 10% 底仓
            available = max(cash - ammo_floor, 0)
            for lv, ratio in pending_rungs:
                amt = available * ratio
                signals.append(f"   🎯 触发 {lv} 档:投入剩余弹药的 {ratio:.0%} ≈ ${amt:,.0f}(卖出 SGOV,狙击配置如下,台账 type 记 VIX{lv})")
                signals.extend(split_order(amt, SNIPER_WEIGHTS, prices))
                available -= amt
        else:
            signals.append(f"   ✅ 当前档位均已执行(本轮已触发: {sorted(fired) or '无'}),按兵不动等更高档或 VIX 回落 < {VIX_RESET}。")
    else:
        if fired and episode_start and current_vix < VIX_RESET:
            unfired = [lv for lv, _ in VIX_LADDER if lv not in fired]
            if unfired:
                signals.append(f"🕊️【恐慌解除】VIX 已回落至 {current_vix:.2f}(<{VIX_RESET})。本轮未触发档位 {unfired} 作废,剩余弹药并入之后三次月度定投。")

        # --- 常规模式 ---
        buy_w = build_buy_weights()

        # 3. BTD 月跌 -5%
        if is_monthly_dip:
            already = any(r["type"] == "BTD2" for r in month_execs)
            if already:
                signals.append(f"🕳️ RSP 本月累跌 {mtd_change:.2%},但本月黄金坑加仓已执行过,不重复。")
            elif IS_BUILDING_PHASE:
                amt = min(BUILD_RULES["dip_5pct"], cash)
                signals.append(f"🕳️【建仓-黄金坑】RSP 本月累跌 {mtd_change:.2%}。请卖出 ${amt:,.0f} {BUILD_RULES['source_asset']},按下单(台账 type 记 BTD2):")
                signals.extend(split_order(amt, buy_w, prices))
                if is_daily_dip:
                    signals.append(f"   ℹ️ 今日同时触发日跌信号 ({daily_change:.2%}),已合并,不重复加仓。")
            else:
                signals.append(f"🕳️【黄金坑提示】RSP 本月跌幅 {mtd_change:.2%}。巡航模式无定投资金,关注偏离带是否触发再平衡。")

        # 4. BTD 日跌 -1% (每月最多一次)
        elif is_daily_dip and IS_BUILDING_PHASE:
            already = any(r["type"] == "BTD1" for r in month_execs)
            if already:
                signals.append(f"📉 RSP 日跌 {daily_change:.2%},但本月日跌加仓已执行过,不重复。")
            else:
                amt = min(BUILD_RULES["dip_1pct"], cash)
                signals.append(f"📉【建仓-加速】RSP 日跌 {daily_change:.2%}。请卖出 ${amt:,.0f} {BUILD_RULES['source_asset']},按下单(台账 type 记 BTD1):")
                signals.extend(split_order(amt, buy_w, prices))

        # 5. 月度定投 (仅建仓期)
        if is_dca_day and IS_BUILDING_PHASE:
            amt = min(BUILD_RULES["monthly_base"], cash)
            signals.append(f"🏗️【建仓-月度定投】每月1号。请卖出 ${amt:,.0f} {BUILD_RULES['source_asset']},按下单(台账 type 记 DCA):")
            signals.extend(split_order(amt, buy_w, prices))

        # 5b. 巡航模式:偏离带检查 + 每月体检
        if not IS_BUILDING_PHASE:
            if executions:
                breaches = [(t, weights[t] - TARGET_WEIGHTS[t]) for t in TARGET_WEIGHTS
                            if abs(weights[t] - TARGET_WEIGHTS[t]) > REBALANCE_BAND]
                for t, dev in breaches:
                    signals.append(f"⚖️【偏离带触发】{t} 当前 {weights[t]:.1%},偏离目标 {dev:+.1%}(>±{REBALANCE_BAND:.0%})。请再平衡(台账 type 记 REBAL)。")
            else:
                signals.append("⚠️ 巡航模式但 executions.csv 为空,无法监控权重偏离,请补录持仓。")
            if is_dca_day:
                signals.append("🩺【月度体检】巡航模式例行报告,见下方持仓快照。")

        # 6. 保底增持 (仅建仓期,台账可知本月是否已买)
        if is_safety_net and IS_BUILDING_PHASE:
            bought = any(r["amount"] > 0 for r in month_execs)
            if bought:
                signals.append(f"🛡️【保底检查】本月已执行 {len([r for r in month_execs if r['amount'] > 0])} 笔买入,保底机制无需启动。")
            else:
                amt = min(BUILD_RULES["monthly_base"], cash)
                signals.append(f"🛡️【保底强制执行】本月第三个周五且本月 0 笔买入!请立即执行月度定投 ${amt:,.0f}(台账 type 记 DCA):")
                signals.extend(split_order(amt, build_buy_weights(), prices))

    # 7. 建仓完成检查
    if IS_BUILDING_PHASE and total and cash / total <= TARGET_WEIGHTS["SGOV"] + 0.01:
        signals.append(f"🎉【建仓完成】现金占比已降至 {cash / total:.1%}(目标 {TARGET_WEIGHTS['SGOV']:.0%})。请将代码中 IS_BUILDING_PHASE 改为 False,切换巡航模式。")

    stats = {
        "rsp_price": today_close, "rsp_mtd": mtd_change, "rsp_ytd": ytd_change,
        "vix_val": current_vix,
        "vix_p120": calculate_vix_percentile(current_vix, vix_hist, 120),
        "holdings": holdings, "cash": cash, "total": total, "weights": weights,
        "month_exec_count": len([r for r in month_execs if r["amount"] > 0]),
        "days_since_buy": days_since_buy,
        "has_ledger": bool(executions),
        "prices": prices,
    }
    return signals, stats


def calculate_vix_percentile(current_val, history, days):
    if len(history) < days:
        return 0.0
    recent_data = history["Close"].tail(days)
    return (recent_data < current_val).sum() / len(recent_data) * 100


def send_email(signals, stats):
    if not EMAIL_SENDER or not EMAIL_PASSWORD:
        print("未配置邮箱 Secrets,跳过发送")
        return

    msg = MIMEMultipart()
    msg["From"] = Header("Hi5 Strategy", "utf-8")
    msg["To"] = Header("Master Investor", "utf-8")
    msg["Subject"] = Header(f"【Hi5 美股日报】{datetime.datetime.now().strftime('%Y-%m-%d')}", "utf-8")

    signal_html = ""
    if signals:
        signal_html = "<h3>🚀 行动指令:</h3><ul>" + "".join(
            f"<li style='color:#d9534f;font-weight:bold;font-size:16px;margin-bottom:8px;'>{s}</li>" for s in signals
        ) + "</ul><hr>"
    else:
        signal_html = "<h3>✅ 今日无操作信号,持仓躺平。</h3><hr>"

    phase_info = (
        f"<p style='color:blue; font-weight:bold;'>当前处于:🏗️ 建仓模式 (本金 ${TOTAL_CAPITAL:,},来源 {BUILD_RULES['source_asset']})</p>"
        if IS_BUILDING_PHASE else
        "<p style='color:green; font-weight:bold;'>当前处于:🛳️ 巡航模式 (日历+偏离带再平衡 / VIX 阶梯)</p>"
    )

    # 执行追踪:把"观望"变成红字事实
    if stats["has_ledger"]:
        dsb = stats["days_since_buy"]
        exec_color = "green" if stats["month_exec_count"] > 0 else "red"
        exec_info = (
            f"<p style='color:{exec_color};font-weight:bold;'>📒 执行台账:本月已执行 {stats['month_exec_count']} 笔买入"
            + (f",距上次买入 {dsb} 天" if dsb is not None else "")
            + f"。现金余量 ${stats['cash']:,.0f} ({stats['weights']['SGOV']:.1%})</p>"
        )
        rows = "".join(
            f"<tr><td>{t}</td><td>{stats['holdings'][t]:.2f}</td><td>{stats['weights'][t]:.1%}</td><td>{TARGET_WEIGHTS[t]:.0%}</td></tr>"
            for t in RISK_TICKERS
        )
        holding_html = f"""
        <h3>📦 持仓快照 (总值 ${stats['total']:,.0f})</h3>
        <table border="1" style="border-collapse: collapse; width: 450px; text-align: left;">
            <tr style="background-color: #f2f2f2;"><th>标的</th><th>股数</th><th>当前权重</th><th>目标</th></tr>
            {rows}
            <tr><td>SGOV/现金</td><td>—</td><td>{stats['weights']['SGOV']:.1%}</td><td>{TARGET_WEIGHTS['SGOV']:.0%}</td></tr>
        </table><br>"""
    else:
        exec_info = "<p style='color:red;font-weight:bold;'>📒 执行台账为空:executions.csv 尚无记录。每笔操作后请追加一行,邮件才能追踪执行率。</p>"
        holding_html = ""

    body = f"""
    <html><body>
        {phase_info}
        {exec_info}
        {signal_html}
        {holding_html}
        <h3>📊 市场快照</h3>
        <table border="1" style="border-collapse: collapse; width: 450px; text-align: left;">
            <tr style="background-color: #f2f2f2;"><th>指标</th><th>数值 / 状态</th></tr>
            <tr><td>RSP 价格</td><td>${stats['rsp_price']:.2f}</td></tr>
            <tr><td>RSP 本月涨跌</td><td style="color:{'red' if stats['rsp_mtd'] < 0 else 'green'}"><b>{stats['rsp_mtd']:.2%}</b></td></tr>
            <tr><td>RSP 今年涨跌</td><td style="color:{'red' if stats['rsp_ytd'] < 0 else 'green'}">{stats['rsp_ytd']:.2%}</td></tr>
            <tr><td>VIX 指数</td><td><b>{stats['vix_val']:.2f}</b></td></tr>
            <tr><td>VIX 历史分位(120日)</td><td>{stats['vix_p120']:.0f}% (越高越恐慌)</td></tr>
        </table>
        <br>
        <div style="font-size:12px; color:gray;">
            <p>Strategy Rules (2026-06 修订):</p>
            <ul>
                <li>目标配置: IWY 15% / SPMO 15% / RSP 20% / SCHD 20% / GLD 10% / SGOV 20%</li>
                <li>DCA: 每月1号定投 ${BUILD_RULES['monthly_base']:,}(建仓期)</li>
                <li>BTD-1: RSP日跌1% 加投 ${BUILD_RULES['dip_1pct']:,}(每月一次)</li>
                <li>BTD-2: RSP月跌5% 加投 ${BUILD_RULES['dip_5pct']:,}</li>
                <li>VIX 阶梯: 35/45/55 各投剩余弹药 1/3,回落破 {VIX_RESET} 重置</li>
                <li>再平衡: 8月日历 + 偏离带 ±{REBALANCE_BAND:.0%}(建仓期偏离带停用)</li>
            </ul>
            <p><em>System generated by GitHub Actions.</em></p>
        </div>
    </body></html>
    """
    msg.attach(MIMEText(body, "html", "utf-8"))

    try:
        server = smtplib.SMTP_SSL("smtp.gmail.com", 465)
        server.login(EMAIL_SENDER, EMAIL_PASSWORD)
        server.sendmail(EMAIL_SENDER, EMAIL_RECEIVER, msg.as_string())
        server.quit()
        print("邮件发送成功")
    except Exception as e:
        print(f"邮件失败: {e}")


if __name__ == "__main__":
    rsp, vix, prices = get_market_data()
    executions = load_executions()
    signals, stats = analyze_strategy(rsp, vix, prices, executions)
    for s in signals:
        print(s)
    send_email(signals, stats)

4.4 配置文件 (.github/workflows/hi5_us_check.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
name: Hi5 US Strategy Check
on:
  schedule:
    - cron: '30 21 * * 1-5'  # UTC 21:30 (美股收盘后)
  workflow_dispatch:
jobs:
  run_hi5:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with: { python-version: '3.9' }
      - run: pip install yfinance pandas requests
      - run: python monitor_hi5.py
        env:
          MAIL_USER: ${{ secrets.MAIL_USER }}
          MAIL_PASS: ${{ secrets.MAIL_PASS }}

5. 修订记录

日期 变更
2026-02-03 初版:五件套等权 + DCA/BTD + VIX>35 all-in
2026-06-12 本金定为 $150k;+GLD 10%(IWY/SPMO 各降至 15%);VIX 阶梯 35/45/55 取代 all-in;新增执行台账与傻瓜执行单;明确巡航模式;回撤目标改为 常规≤20% / 极端≤25%