{"id":6062,"date":"2025-08-15T15:20:05","date_gmt":"2025-08-15T07:20:05","guid":{"rendered":"http:\/\/czliutz.pgrm.cc\/?p=6062"},"modified":"2025-08-15T16:35:14","modified_gmt":"2025-08-15T08:35:14","slug":"%e8%82%a1%e7%a5%a8%e8%b0%83%e6%b5%8b%e4%bb%a3%e7%a0%81","status":"publish","type":"post","link":"http:\/\/cnliutz.wicp.vip\/?p=6062","title":{"rendered":"\u80a1\u7968\u6bcf\u5929\u4e00\u952e\u8dd1python\u4ee3\u7801"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">A\u80a1\u53cc\u5468\u8c03\u4ed3\uff1a\u4e00\u952e\u65e5\u5e38\u91cf\u5316\u811a\u672c\uff08AkShare\uff09<\/h1>\n\n\n\n<p>\u4f60\u8981\u7684\u662f\u80fd\u6bcf\u5929\u4e00\u952e\u8dd1\u3001\u4e24\u6b21\/\u5468\u8c03\u4ed3\u3001\u98ce\u63a7\u4e2d\u6027\u7684\u5b9e\u7528\u811a\u672c\u3002\u4e0b\u9762\u8fd9\u5957\u65b9\u6848\u4ee5\u6caa\u6df1300\u6210\u5206\u4e3a\u5e95\u5c42\u6c60\uff08\u7a33\u6d41\u52a8\u6027\uff0c\u907f\u514d\u5e78\u5b58\u8005\u504f\u5dee\uff09\uff0c\u5468\u4e8c\/\u5468\u56db\u6536\u76d8\u540e\u51fa\u4fe1\u53f7\uff0c\u6b21\u65e5\u5f00\u76d8\u6309\u76ee\u6807\u6743\u91cd\u6210\u4ea4\uff0c\u542b\u6210\u672c\u3001\u6ed1\u70b9\u4e0e\u6da8\u8dcc\u505c\u6210\u4ea4\u7ea6\u675f\uff0c\u5e76\u8f93\u51fa\u8ba2\u5355\u4e0e\u7ee9\u6548\u56fe\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u7b56\u7565\u4e0e\u98ce\u63a7\u8bbe\u8ba1<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u76ee\u6807\u5e02\u573a\u4e0e\u9891\u7387:<\/strong> A\u80a1\uff0c\u5468\u4e8c\/\u5468\u56db\u8c03\u4ed3\uff1b\u4fe1\u53f7\u7528\u5f53\u65e5\u6536\u76d8\u6570\u636e\uff0c\u6b21\u65e5\u5f00\u76d8\u6210\u4ea4\uff0c\u907f\u5f00\u201c\u5077\u770b\u672a\u6765\u201d\u3002<\/li>\n\n\n\n<li><strong>\u6807\u7684\u6c60:<\/strong> \u6caa\u6df1300\u6210\u5206\uff0c\u8f85\u4ee5\u6eda\u52a8\u6d41\u52a8\u6027\u8fc7\u6ee4\uff08\u8fd160\u65e5\u65e5\u5747\u6210\u4ea4\u91cf\u95e8\u69db\uff09\u3002<\/li>\n\n\n\n<li><strong>\u9009\u80a1\u903b\u8f91\uff08\u4e2d\u98ce\u9669\uff0c\u8d8b\u52bf+\u52a8\u91cf+\u6ce2\u52a8\u7ea6\u675f\uff09:<\/strong><\/li>\n\n\n\n<li><strong>\u8d8b\u52bf:<\/strong> 20\u65e5\u5747\u7ebf\u572860\u65e5\u5747\u7ebf\u4e0a\u65b9\uff0c\u6536\u76d8\u5728MA20\u4e0a\u65b9\u3002<\/li>\n\n\n\n<li><strong>\u52a8\u91cf:<\/strong> 126\u65e5\u52a8\u91cf\u4e3a\u6b63\uff1bRSI\u5904\u4e8e\u6e29\u548c\u533a\u95f4\uff0845\u201365\uff09\u4ee5\u964d\u4f4e\u8ffd\u6da8\u9876\u3002<\/li>\n\n\n\n<li><strong>\u6ce2\u52a8\/\u8fc7\u70ed:<\/strong> \u6536\u76d8\u4f4e\u4e8e\u5e03\u6797\u5e26\u4e0a\u8f68\uff0c\u907f\u514d\u8fc7\u5ea6\u6269\u5f20\u3002<\/li>\n\n\n\n<li><strong>\u6301\u4ed3\u6784\u5efa:<\/strong> \u4ece\u901a\u8fc7\u7b5b\u9009\u7684\u80a1\u7968\u4e2d\u9009\u524d TopK\uff08\u6309\u52a8\u91cf\u6392\u5e8f\uff09\uff0c\u7528\u6ce2\u52a8\u7387\u5012\u6570\u914d\u6743\uff0c\u5355\u7968\u4e0a\u9650 12%\uff0c\u9ed8\u8ba4\u6700\u591a 15 \u53ea\u3002<\/li>\n\n\n\n<li>\u6743\u91cd\u516c\u5f0f\uff1a\u5bf9\u7b2c (i) \u53ea\uff0c\u8ba1\u7b97 20 \u65e5\u5e74\u5316\u6ce2\u52a8 (\\sigma_i)\uff0c\u539f\u59cb\u6743\u91cd (w_i&#8217; = 1\/\\sigma_i)\uff0c\u5f52\u4e00\u5316\u540e\u5f97\u5230 (w_i)\uff0c\u5e76\u8fdb\u884c\u4e0a\u9650\u88c1\u526a\u4e0e\u518d\u5f52\u4e00\u5316\u3002<\/li>\n\n\n\n<li><strong>\u4ea4\u6613\u4e0e\u6210\u672c:<\/strong><\/li>\n\n\n\n<li><strong>\u6210\u4ea4\u4ef7:<\/strong> \u6b21\u65e5\u5f00\u76d8\u4ef7\u3002<\/li>\n\n\n\n<li><strong>\u8d39\u7387:<\/strong> \u4f63\u91d1 0.0005\uff08\u53cc\u8fb9\uff09\uff0c\u5356\u51fa\u5370\u82b1\u7a0e 0.001\uff08\u5355\u8fb9\uff09\uff0c\u6ed1\u70b9 0.0005\uff08\u53cc\u8fb9\uff09\u3002<\/li>\n\n\n\n<li><strong>\u6da8\u8dcc\u505c:<\/strong> \u82e5\u6b21\u65e5\u5f00\u76d8\u76f8\u5bf9\u6628\u6536\u6da8\u5e45 \u22659.5%\uff08\u4e70\u5165\uff09\u6216\u8dcc\u5e45 \u2264-9.5%\uff08\u5356\u51fa\uff09\uff0c\u89c6\u4e3a\u65e0\u6cd5\u6210\u4ea4\uff0c\u5f53\u5929\u8be5\u7b14\u8ba2\u5355\u8df3\u8fc7\u3002<\/li>\n\n\n\n<li><strong>\u7ee9\u6548\u4e0e\u56fe\u5f62:<\/strong> \u7ec4\u5408\u51c0\u503c\u66f2\u7ebf\u3001\u6700\u5927\u56de\u64a4\u3001\u6301\u4ed3\u53d8\u52a8\u4e0e\u5f53\u65e5\u8c03\u4ed3\u8ba2\u5355 CSV\u3002<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u4e00\u952e\u65e5\u5e38\u811a\u672c\uff08\u76f4\u63a5\u53ef\u8fd0\u884c\uff09<\/h2>\n\n\n\n<p>\u5148\u5b89\u88c5\u4f9d\u8d56\uff1a<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pip install akshare pandas numpy matplotlib<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code># -*- coding: utf-8 -*-\n# A\u80a1\u53cc\u5468\u8c03\u4ed3 \u4e00\u952e\u65e5\u5e38\u7814\u7a76\u4e0e\u56de\u6d4b\u811a\u672c\uff08AkShare\uff09\n# \u8fd0\u884c\u73af\u5883\uff1aPython 3.9+\uff1b\u4f9d\u8d56\uff1aakshare, pandas, numpy, matplotlib\n\nimport akshare as ak\nimport pandas as pd\nimport numpy as np\nimport time\nfrom datetime import datetime, timedelta\nimport matplotlib.pyplot as plt\n\n# \u4fee\u6539\u4e3aWindows\u7cfb\u7edf\u9ed8\u8ba4\u4e2d\u6587\u5b57\u4f53\nplt.rcParams&#91;\"font.family\"] = &#91;\"SimHei\", \"Microsoft YaHei\", \"SimSun\"]\nplt.rcParams&#91;\"axes.unicode_minus\"] = False  # \u89e3\u51b3\u8d1f\u53f7\u663e\u793a\u95ee\u9898\n\n# -----------------------\n# \u53c2\u6570\u533a\uff08\u6309\u9700\u4fee\u6539\uff09\n# -----------------------\nSTART_DATE = \"2018-01-01\"\nEND_DATE   = None  # None \u8868\u793a\u5230\u4eca\u65e5\nCASH_INIT  = 1_000_000\nMAX_POS    = 15           # \u6700\u591a\u6301\u4ed3\u6570\nMAX_W      = 0.12         # \u5355\u7968\u6743\u91cd\u4e0a\u9650\nFEE_COMM   = 0.0005       # \u4f63\u91d1\nFEE_STAMP  = 0.001        # \u5370\u82b1\u7a0e\uff08\u4ec5\u5356\u51fa\uff09\nSLIPPAGE   = 0.0005       # \u6ed1\u70b9\nLIQ_VOL_TH = 1_000_000    # \u8fd160\u65e5\u5e73\u5747\u6210\u4ea4\u91cf\u95e8\u69db\uff08\u624b\/\u80a1\uff09\uff0c\u53ef\u6309\u9700\u8981\u8c03\nREBAL_WEEKDAYS = {1, 3}   # \u5468\u4e8c(1)\u3001\u5468\u56db(3) \u8c03\u4ed3\uff1bPython: Mon=0\n\n# -----------------------\n# \u5de5\u5177\u51fd\u6570\uff1a\u6307\u6807\n# -----------------------\ndef sma(s, n):\n    return s.rolling(n).mean()\n\ndef rsi(close, n=14):\n    delta = close.diff()\n    up = np.where(delta &gt; 0, delta, 0.0)\n    dn = np.where(delta &lt; 0, -delta, 0.0)\n    up_ema = pd.Series(up, index=close.index).ewm(alpha=1\/n, adjust=False).mean()\n    dn_ema = pd.Series(dn, index=close.index).ewm(alpha=1\/n, adjust=False).mean()\n    rs = up_ema \/ dn_ema.replace(0, np.nan)\n    return 100 - (100 \/ (1 + rs))\n\ndef bbands(close, n=20, k=2):\n    mid = close.rolling(n).mean()\n    std = close.rolling(n).std(ddof=0)\n    up = mid + k * std\n    dn = mid - k * std\n    return mid, up, dn\n\ndef true_range(df):\n    prev_close = df&#91;\"close\"].shift(1)\n    tr = pd.concat(&#91;\n        (df&#91;\"high\"] - df&#91;\"low\"]).abs(),\n        (df&#91;\"high\"] - prev_close).abs(),\n        (df&#91;\"low\"] - prev_close).abs()\n    ], axis=1).max(axis=1)\n    return tr\n\ndef ann_vol(close, n=20):\n    ret = close.pct_change()\n    return ret.rolling(n).std() * np.sqrt(252)\n\n# -----------------------\n# \u6570\u636e\u83b7\u53d6\u4e0e\u57fa\u51c6\u65e5\u5386\n# -----------------------\ndef get_trade_calendar(start=START_DATE, end=END_DATE):\n    cal = ak.tool_trade_date_hist_sina()\n    cal&#91;\"trade_date\"] = pd.to_datetime(cal&#91;\"trade_date\"])\n    if end is None:\n        end = datetime.now().strftime(\"%Y-%m-%d\")\n    cal = cal&#91;(cal&#91;\"trade_date\"] &gt;= pd.to_datetime(start)) &amp;\n              (cal&#91;\"trade_date\"] &lt;= pd.to_datetime(end))]&#91;\"trade_date\"].sort_values()\n    return cal.tolist()\n\ndef get_hs300_symbols():\n    df = ak.index_stock_cons(symbol=\"000300\")\n    # \u5217\u53ef\u80fd\u662f '\u54c1\u79cd\u4ee3\u7801' \u6216 '\u6210\u5206\u5238\u4ee3\u7801'; \u505a\u517c\u5bb9\n    for col in &#91;\"\u54c1\u79cd\u4ee3\u7801\", \"\u6210\u5206\u5238\u4ee3\u7801\", \"\u4ee3\u7801\", \"code\"]:\n        if col in df.columns:\n            return sorted(df&#91;col].astype(str).str.zfill(6).unique().tolist())\n    # \u515c\u5e95\n    return sorted(df.iloc&#91;:,0].astype(str).str.zfill(6).unique().tolist())\n\ndef get_hist(code, start=START_DATE, end=END_DATE, adjust=\"qfq\"):\n    if end is None:\n        end = datetime.now().strftime(\"%Y%m%d\")\n    df = ak.stock_zh_a_hist(symbol=code, period=\"daily\",\n                            start_date=start.replace(\"-\",\"\"),\n                            end_date=end.replace(\"-\",\"\"),\n                            adjust=adjust)\n    # \u517c\u5bb9\u5217\u540d\n    mapper = {\"\u65e5\u671f\":\"date\",\"\u5f00\u76d8\":\"open\",\"\u6536\u76d8\":\"close\",\"\u6700\u9ad8\":\"high\",\"\u6700\u4f4e\":\"low\",\"\u6210\u4ea4\u91cf\":\"volume\",\"\u6210\u4ea4\u989d\":\"amount\"}\n    df = df.rename(columns=mapper)\n    df&#91;\"date\"] = pd.to_datetime(df&#91;\"date\"])\n    cols = &#91;c for c in &#91;\"date\",\"open\",\"high\",\"low\",\"close\",\"volume\",\"amount\"] if c in df.columns]\n    df = df&#91;cols].set_index(\"date\").sort_index()\n    df = df.dropna()\n    return df\n\n# -----------------------\n# \u4fe1\u53f7\u4e0e\u7b5b\u9009\n# -----------------------\ndef compute_indicators(df):\n    out = df.copy()\n    out&#91;\"MA20\"] = sma(out&#91;\"close\"], 20)\n    out&#91;\"MA60\"] = sma(out&#91;\"close\"], 60)\n    out&#91;\"RSI14\"] = rsi(out&#91;\"close\"], 14)\n    out&#91;\"MOM126\"] = out&#91;\"close\"] \/ out&#91;\"close\"].shift(126) - 1\n    mid, up, dn = bbands(out&#91;\"close\"], 20, 2)\n    out&#91;\"BB_MID\"], out&#91;\"BB_UP\"], out&#91;\"BB_DN\"] = mid, up, dn\n    out&#91;\"ANNVOL20\"] = ann_vol(out&#91;\"close\"], 20)\n    out&#91;\"TR\"] = true_range(out)\n    out&#91;\"ATR20\"] = out&#91;\"TR\"].rolling(20).mean()\n    return out\n\ndef pass_screen(row):\n    c1 = row&#91;\"MA20\"] &gt; row&#91;\"MA60\"]\n    c2 = row&#91;\"close\"] &gt; row&#91;\"MA20\"]\n    c3 = row&#91;\"MOM126\"] &gt; 0\n    c4 = 45 &lt;= row&#91;\"RSI14\"] &lt;= 65\n    c5 = row&#91;\"close\"] &lt; row&#91;\"BB_UP\"]\n    return c1 and c2 and c3 and c4 and c5\n\n# -----------------------\n# \u56de\u6d4b\uff1a\u4e24\u6b21\/\u5468\u8c03\u4ed3\uff0c\u6b21\u65e5\u5f00\u76d8\u6210\u4ea4\n# -----------------------\ndef backtest_portfolio(symbols, start=START_DATE, end=END_DATE, cash_init=CASH_INIT):\n    # \u4e0b\u8f7d\u6570\u636e\n    data = {}\n    for i, sym in enumerate(symbols, 1):\n        try:\n            df = get_hist(sym, start, end)\n            data&#91;sym] = compute_indicators(df)\n        except Exception:\n            pass\n        time.sleep(0.2)  # \u6e29\u548c\u9650\u901f\n    # \u7edf\u4e00\u65e5\u5386\n    all_dates = sorted(set().union(*&#91;df.index for df in data.values()]))\n    cal = pd.DatetimeIndex(all_dates)\n    # \u9009\u62e9\u8c03\u4ed3\u65e5\uff08\u5468\u4e8c\/\u5468\u56db\u4e14\u662f\u4ea4\u6613\u65e5\uff09\n    rebal_days = &#91;d for d in cal if d.weekday() in REBAL_WEEKDAYS]\n    # \u8fc7\u6ee4\uff1a\u8fd160\u65e5\u5747\u91cf\n    def liquid_ok(df, dt):\n        window = df.loc&#91;:dt].tail(60)\n        if \"volume\" not in window: \n            return True\n        return window&#91;\"volume\"].mean() &gt;= LIQ_VOL_TH\n\n    # \u72b6\u6001\n    cash = cash_init\n    positions = {}  # sym -&gt; shares\n    nav_series = &#91;]\n    dd_series = &#91;]\n    equity = cash\n    peak = equity\n    last_prices = {}\n\n    # \u9010\u65e5\u4eff\u771f\n    for i, d in enumerate(cal&#91;:-1]):  # \u81f3\u5012\u6570\u7b2c\u4e8c\u5929\uff08\u56e0\u6b21\u65e5\u5f00\u76d8\u6210\u4ea4\uff09\n        todays_vals = {}\n        # \u66f4\u65b0\u6301\u4ed3\u5e02\u503c\n        for sym, df in data.items():\n            if d in df.index:\n                last_prices&#91;sym] = df.at&#91;d, \"close\"]\n            if sym in positions and sym in last_prices:\n                todays_vals&#91;sym] = positions&#91;sym] * last_prices&#91;sym]\n        equity = cash + sum(todays_vals.values())\n        peak = max(peak, equity)\n        drawdown = (equity \/ peak) - 1\n        nav_series.append((d, equity))\n        dd_series.append((d, drawdown))\n\n        # \u8c03\u4ed3\u4fe1\u53f7\uff08\u7528\u4eca\u65e5\u6536\u76d8\uff09\n        if d in rebal_days:\n            # \u751f\u6210\u5019\u9009\n            candidates = &#91;]\n            for sym, df in data.items():\n                if d not in df.index: \n                    continue\n                if not liquid_ok(df, d):\n                    continue\n                row = df.loc&#91;d]\n                # \u8981\u6c42\u6307\u6807\u6709\u6548\n                if np.any(pd.isna(row&#91;&#91;\"MA20\",\"MA60\",\"RSI14\",\"MOM126\",\"BB_UP\",\"ANNVOL20\"]])):\n                    continue\n                if pass_screen(row):\n                    candidates.append((sym, row&#91;\"MOM126\"], row&#91;\"ANNVOL20\"]))\n            # \u6392\u5e8f\u4e0e\u622a\u65ad\n            candidates.sort(key=lambda x: x&#91;1], reverse=True)\n            picks = candidates&#91;:MAX_POS]\n\n            # \u8ba1\u7b97\u76ee\u6807\u6743\u91cd\uff08\u6ce2\u52a8\u7387\u5012\u6570\uff09\n            if picks:\n                vols = np.array(&#91;max(1e-6, x&#91;2]) for x in picks])\n                inv = 1.0 \/ vols\n                w_raw = inv \/ inv.sum()\n                # \u5355\u7968\u4e0a\u9650\n                w_capped = np.minimum(w_raw, MAX_W)\n                w = w_capped \/ w_capped.sum()\n                target = {sym: w&#91;j] for j, (sym, _, _) in enumerate(picks)}\n            else:\n                target = {}\n\n            # \u6b21\u65e5\u5f00\u76d8\u6267\u884c\n            nd = cal&#91;i+1]\n            # \u6784\u5efa\u76ee\u6807\u5934\u5bf8\u4ef7\u503c\n            target_value = {sym: equity * w for sym, w in target.items()}\n\n            # \u5148\u5356\u51fa\u672a\u5728\u76ee\u6807\u5185\u6216\u8d85\u914d\u90e8\u5206\n            for sym in list(positions.keys()):\n                df = data.get(sym)\n                if df is None or nd not in df.index or d not in df.index:\n                    continue\n                prev_close = df.at&#91;d, \"close\"]\n                next_open = df.at&#91;nd, \"open\"]\n                # \u8dcc\u505c\u65e0\u6cd5\u5356\u51fa\uff08\u8fd1\u4f3c\uff09\n                if next_open &lt;= prev_close * (1 - 0.095):\n                    continue\n                price = next_open * (1 - SLIPPAGE)\n                cur_val = positions&#91;sym] * price\n                tgt_val = target_value.get(sym, 0.0)\n                if cur_val &gt; tgt_val + 1:  # \u8d85\u914d\u6216\u4e0d\u5728\u76ee\u6807\n                    sell_val = cur_val - tgt_val\n                    shares = int(sell_val \/\/ price)\n                    if shares &gt; 0:\n                        proceeds = shares * price * (1 - FEE_COMM - FEE_STAMP)\n                        positions&#91;sym] -= shares\n                        if positions&#91;sym] &lt;= 0:\n                            positions.pop(sym, None)\n                        cash += proceeds\n\n            # \u518d\u4e70\u5165\u4e0d\u8fbe\u6807\u6216\u65b0\u6807\u7684\n            for sym, tgt_val in target_value.items():\n                df = data.get(sym)\n                if df is None or nd not in df.index or d not in df.index:\n                    continue\n                prev_close = df.at&#91;d, \"close\"]\n                next_open = df.at&#91;nd, \"open\"]\n                # \u6da8\u505c\u65e0\u6cd5\u4e70\u5165\uff08\u8fd1\u4f3c\uff09\n                if next_open &gt;= prev_close * (1 + 0.095):\n                    continue\n                price = next_open * (1 + SLIPPAGE)\n                cur_shares = positions.get(sym, 0)\n                cur_val = cur_shares * price\n                buy_val = max(0.0, tgt_val - cur_val)\n                shares = int(buy_val \/\/ price)\n                if shares &gt; 0 and cash &gt; shares * price * (1 + FEE_COMM):\n                    cost = shares * price * (1 + FEE_COMM)\n                    cash -= cost\n                    positions&#91;sym] = cur_shares + shares\n\n    nav = pd.Series({d: v for d, v in nav_series}).sort_index()\n    dd = pd.Series({d: v for d, v in dd_series}).sort_index()\n    ret = nav.pct_change().fillna(0)\n    stats = {\n        \"CAGR\": (nav.iloc&#91;-1] \/ nav.iloc&#91;0]) ** (252\/len(nav)) - 1,\n        \"Vol\": ret.std() * np.sqrt(252),\n        \"Sharpe\": (ret.mean() \/ (ret.std() + 1e-9)) * np.sqrt(252),\n        \"MaxDD\": dd.min()\n    }\n    return nav, dd, positions, stats\n\n# -----------------------\n# \u4eca\u65e5\u8c03\u4ed3\u8ba1\u5212\uff08\u5b9e\u7528\u65e5\u5e38\uff09\n# -----------------------\ndef today_rebalance_plan():\n    today = pd.Timestamp(datetime.now().date())\n    # \u82e5\u4eca\u5929\u4e0d\u662f\u4ea4\u6613\u65e5\u6216\u4e0d\u662f\u5468\u4e8c\/\u5468\u56db\uff0c\u76f4\u63a5\u63d0\u793a\n    cal = get_trade_calendar((today - pd.Timedelta(days=10)).strftime(\"%Y-%m-%d\"),\n                             today.strftime(\"%Y-%m-%d\"))\n    cal_idx = pd.DatetimeIndex(cal)\n    if today not in cal_idx or today.weekday() not in REBAL_WEEKDAYS:\n        print(\"\u4eca\u5929\u4e0d\u662f\u8ba1\u5212\u8c03\u4ed3\u65e5\uff08\u6216\u975e\u4ea4\u6613\u65e5\uff09\u3002\")\n        return\n\n    syms = get_hs300_symbols()\n    plan_rows = &#91;]\n    for sym in syms:\n        try:\n            df = get_hist(sym, (today - pd.Timedelta(days=400)).strftime(\"%Y-%m-%d\"),\n                               today.strftime(\"%Y-%m-%d\"))\n            df = compute_indicators(df)\n            if len(df) &lt; 200 or today not in df.index:\n                continue\n            row = df.loc&#91;today]\n            # \u6d41\u52a8\u6027\n            if \"volume\" in df:\n                if df.loc&#91;:today].tail(60)&#91;\"volume\"].mean() &lt; LIQ_VOL_TH:\n                    continue\n            if np.any(pd.isna(row&#91;&#91;\"MA20\",\"MA60\",\"RSI14\",\"MOM126\",\"BB_UP\",\"ANNVOL20\"]])):\n                continue\n            if pass_screen(row):\n                plan_rows.append({\n                    \"code\": sym,\n                    \"mom126\": row&#91;\"MOM126\"],\n                    \"annvol20\": row&#91;\"ANNVOL20\"],\n                    \"close\": row&#91;\"close\"]\n                })\n        except Exception:\n            pass\n        time.sleep(0.05)\n\n    if not plan_rows:\n        print(\"\u4eca\u65e5\u65e0\u6807\u7684\u901a\u8fc7\u7b5b\u9009\u3002\")\n        return\n\n    dfp = pd.DataFrame(plan_rows).sort_values(\"mom126\", ascending=False).head(MAX_POS)\n    inv = 1.0 \/ np.maximum(1e-6, dfp&#91;\"annvol20\"].values)\n    w_raw = inv \/ inv.sum()\n    w_capped = np.minimum(w_raw, MAX_W)\n    w = w_capped \/ w_capped.sum()\n    dfp&#91;\"target_weight\"] = w\n    dfp.to_csv(f\"rebalance_plan_{today.strftime('%Y%m%d')}.csv\", index=False, encoding=\"utf-8-sig\")\n    print(\"\u4eca\u65e5\u8ba1\u5212\uff08\u6b21\u65e5\u5f00\u76d8\u6267\u884c\uff0c\u6743\u91cd\u5df2\u622a\u9876\uff09\uff1a\")\n    print(dfp&#91;&#91;\"code\",\"target_weight\",\"mom126\",\"annvol20\",\"close\"]])\n\n# -----------------------\n# \u4e3b\u51fd\u6570\uff1a\u56de\u6d4b + \u56fe\u5f62 + \u4eca\u65e5\u8ba1\u5212\n# -----------------------\nif __name__ == \"__main__\":\n    print(\"\u83b7\u53d6\u6caa\u6df1300\u6210\u5206...\")\n    symbols = get_hs300_symbols()\n    print(f\"\u6210\u5206\u80a1\u6570\u91cf\uff1a{len(symbols)}\")\n\n    print(\"\u5f00\u59cb\u56de\u6d4b\uff08\u8fd9\u53ef\u80fd\u9700\u8981\u51e0\u5206\u949f\uff09...\")\n    nav, dd, positions, stats = backtest_portfolio(symbols, START_DATE, END_DATE, CASH_INIT)\n    print(\"\u56de\u6d4b\u7edf\u8ba1\uff1a\")\n    for k, v in stats.items():\n        print(f\"{k}: {v:.4f}\")\n\n    # \u7ed8\u5236\u51c0\u503c\u4e0e\u56de\u64a4\n    fig, ax = plt.subplots(2, 1, figsize=(10, 6), sharex=True,\n                           gridspec_kw={\"height_ratios\":&#91;3,1]})\n    nav_norm = nav \/ nav.iloc&#91;0]\n    ax&#91;0].plot(nav_norm.index, nav_norm.values, label=\"Portfolio\")\n    ax&#91;0].set_title(\"\u7ec4\u5408\u51c0\u503c\uff08\u5f52\u4e00\uff09\")\n    ax&#91;0].legend()\n    ax&#91;0].grid(True, alpha=0.3)\n    ax&#91;1].fill_between(dd.index, dd.values, 0, color=\"red\", alpha=0.3)\n    ax&#91;1].set_title(\"\u56de\u64a4\")\n    ax&#91;1].grid(True, alpha=0.3)\n    plt.tight_layout()\n    plt.savefig(\"backtest_nav_drawdown.png\", dpi=150)\n    plt.show()\n\n    # \u5f53\u65e5\u8c03\u4ed3\u8ba1\u5212\uff08\u5728 CST \u4e0b\u5348 15:10 \u540e\u8fd0\u884c\u66f4\u5408\u9002\uff09\n    today_rebalance_plan()<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u5982\u4f55\u6bcf\u5929\u4e00\u952e\u8dd1<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u65f6\u95f4\u70b9:<\/strong> \u6ca7\u5dde\u672c\u5730\u65f6\u95f4\u5efa\u8bae\u5728\u4ea4\u6613\u65e5 15:10 \u4e4b\u540e\u8fd0\u884c\uff0c\u4fdd\u8bc1\u5f53\u65e5\u6536\u76d8\u6570\u636e\u53ef\u7528\u3002<\/li>\n\n\n\n<li><strong>\u547d\u4ee4\u884c:<\/strong><\/li>\n\n\n\n<li><strong>\u8fd0\u884c:<\/strong> python run_quant.py<\/li>\n\n\n\n<li><strong>\u8f93\u51fa:<\/strong> backtest_nav_drawdown.png\u3001rebalance_plan_YYYYMMDD.csv\uff0c\u5e76\u5728\u63a7\u5236\u53f0\u6253\u5370\u7edf\u8ba1\u4e0e\u5f53\u65e5\u8ba1\u5212\u3002<\/li>\n\n\n\n<li><strong>\u5b9a\u65f6\u4efb\u52a1:<\/strong><\/li>\n\n\n\n<li><strong>Linux crontab:<\/strong> 10 15 * * 1-5 \/usr\/bin\/python3 \/path\/run_quant.py &gt;&gt; \/path\/log.txt 2&gt;&amp;1<\/li>\n\n\n\n<li><strong>Windows \u4efb\u52a1\u8ba1\u5212:<\/strong> \u8bbe\u4e3a\u5de5\u4f5c\u65e5 15:10 \u89e6\u53d1\u3002<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u53ef\u8c03\u53c2\u6570\u4e0e\u6269\u5c55<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u53ef\u8c03\u53c2\u6570:<\/strong><\/li>\n\n\n\n<li><strong>MAX_POS\/MAX_W:<\/strong> \u63a7\u5236\u96c6\u4e2d\u5ea6\u4e0e\u98ce\u9669\u3002<\/li>\n\n\n\n<li><strong>LIQ_VOL_TH:<\/strong> \u63d0\u9ad8\u95e8\u69db\u53ef\u8fdb\u4e00\u6b65\u964d\u4f4e\u6d41\u52a8\u6027\u98ce\u9669\u3002<\/li>\n\n\n\n<li><strong>\u8d39\u7387\/\u6ed1\u70b9:<\/strong> \u6309\u4f60\u7684\u5238\u5546\u8d39\u7387\u4e0e\u6210\u4ea4\u4f53\u9a8c\u5fae\u8c03\u3002<\/li>\n\n\n\n<li><strong>\u98ce\u63a7\u589e\u5f3a:<\/strong><\/li>\n\n\n\n<li><strong>\u6b62\u635f\/\u8ddf\u8e2a\u6b62\u76c8:<\/strong> \u4ee5 ATR \u4e3a\u5355\u4f4d\uff0c\u5982\u4ef7\u683c\u8dcc\u7834 MA60 \u6216 3\u00d7ATR \u8dcc\u5e45\u5e73\u4ed3\u3002<\/li>\n\n\n\n<li><strong>\u6da8\u8dcc\u505c\u7ec6\u5316:<\/strong> \u79d1\u521b\/\u521b\u4e1a\u677f 20% \u89c4\u5219\u53ef\u6309\u80a1\u7968\u677f\u5757\u8c03\u6574\u9608\u503c\u3002<\/li>\n\n\n\n<li><strong>\u57fa\u51c6\u6bd4\u8f83:<\/strong> \u53e0\u52a0\u6caa\u6df1300\u6307\u6570\u51c0\u503c\uff0c\u8ba1\u7b97\u8d85\u989d\u4e0e\u4fe1\u606f\u6bd4\u7387\u3002<\/li>\n\n\n\n<li><strong>\u7814\u7a76\u7ef4\u5ea6:<\/strong><\/li>\n\n\n\n<li><strong>\u6a2a\u622a\u9762\u591a\u56e0\u5b50:<\/strong> \u4ee5\u52a8\u91cf\u3001\u6ce2\u52a8\u3001\u4f30\u503c\uff08PE\/PB\uff0c\u9700\u989d\u5916\u6570\u636e\uff09\u505a\u6253\u5206\uff0c\u5468\u4e8c\/\u5468\u56db\u540c\u9891\u8c03\u4ed3\u3002<\/li>\n\n\n\n<li><strong>\u518d\u5e73\u8861\u9c81\u68d2\u6027:<\/strong> \u6539\u4e3a\u201c\u9608\u503c\u518d\u5e73\u8861\u201d\uff08\u504f\u79bb&gt;25%\u624d\u8c03\u4ed3\uff09\u964d\u4f4e\u6362\u624b\u3002<\/li>\n<\/ul>\n\n\n\n<p>\u5982\u679c\u4f60\u60f3\u628a\u6807\u7684\u6c60\u6539\u4e3a\u4e2d\u8bc1500\u6216\u52a0\u5165\u884c\u4e1a\u4e2d\u6027\u7ea6\u675f\uff0c\u6216\u8005\u628a\u8c03\u4ed3\u65e5\u6539\u6210\u201c\u6bcf\u5468\u6700\u8fd1\u7684\u5468\u4e8c\u4e0e\u5468\u56db\uff0c\u5982\u9047\u8282\u5047\u65e5\u987a\u5ef6\u201d\uff0c\u6211\u53ef\u4ee5\u628a\u4e0a\u8ff0\u811a\u672c\u518d\u7ec6\u5316\u6210\u6a21\u5757\u5316\u7684\u7814\u7a76\u6846\u67b6\uff0c\u5e76\u52a0\u4e0a\u6027\u80fd\u5256\u6790\u4e0e\u7ed3\u679c\u7f13\u5b58\u6765\u52a0\u901f\u65e5\u5e38\u8dd1\u6279\u3002<\/p>\n\n\n\n<p>\u4ee5\u4e0b\u662f\u7a0b\u5e8f\u8fd0\u884c\u7ed3\u679c\u7684\u8be6\u7ec6\u89e3\u91ca\uff1a<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>\u6570\u636e\u83b7\u53d6\u4e0e\u56de\u6d4b\u6982\u51b5<br>\u6caa\u6df1300\u6210\u5206\u80a1\u6570\u91cf\uff1a282\uff1a\u6210\u529f\u83b7\u53d6\u4e86\u5f53\u524d\u6caa\u6df1300\u6307\u6570\u7684282\u53ea\u6210\u5206\u80a1<br>\u56de\u6d4b\u8017\u65f6\u63d0\u793a\uff1a\u7b56\u7565\u56de\u6d4b\u9700\u8981\u4e00\u5b9a\u8ba1\u7b97\u65f6\u95f4\uff0c\u7b26\u5408\u9884\u671f<\/li>\n\n\n\n<li>\u6838\u5fc3\u56de\u6d4b\u6307\u6807\u89e3\u8bfb<br>| \u6307\u6807 | \u6570\u503c | \u542b\u4e49\u89e3\u91ca | \u7b56\u7565\u8bc4\u4f30 | <br>|&#8212;&#8212;&#8211;|&#8212;&#8212;&#8212;|&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;|&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;|<br> | CAGR | -0.0900 | \u590d\u5408\u5e74\u5316\u589e\u957f\u7387\uff1a-9.00% | \u7b56\u7565\u6574\u4f53\u5e74\u5316\u4e8f\u635f9% |<br> | Vol | 0.2348 | \u6ce2\u52a8\u7387\uff1a23.48% | \u6536\u76ca\u6ce2\u52a8\u8f83\u5927 |<br> | Sharpe | -0.2843 | \u590f\u666e\u6bd4\u7387\uff1a-0.28 | \u98ce\u9669\u8c03\u6574\u540e\u6536\u76ca\u4e3a\u8d1f\uff0c\u8868\u73b0\u5f31\u4e8e\u65e0\u98ce\u9669\u8d44\u4ea7 |<br> | MaxDD | -0.3505 | \u6700\u5927\u56de\u64a4\uff1a-35.05% | \u7b56\u7565\u5386\u53f2\u6700\u5927\u4e8f\u635f\u5e45\u5ea6\u4e3a35% |<\/li>\n\n\n\n<li>\u4eca\u65e5\u6295\u8d44\u8ba1\u5212\uff08\u6b21\u65e5\u5f00\u76d8\u6267\u884c\uff09<br>| \u4ee3\u7801 | \u76ee\u6807\u6743\u91cd | 126\u5929\u52a8\u91cf(mom126) | 20\u5929\u5e74\u5316\u6ce2\u52a8\u7387(annvol20) | \u6536\u76d8\u4ef7 | <br>|&#8212;&#8212;&#8211;|&#8212;&#8212;&#8212;-|&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-|&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;|&#8212;&#8212;&#8211;|<br> | 300394 | 0.2 | 0.6719 | 0.7191 | 107.87 |<br> | 603799 | 0.2 | 0.4765 | 0.4454 | 44.25 | <br>| 002463 | 0.2 | 0.4701 | 0.5822 | 55.35 |<br> | 002074 | 0.2 | 0.3858 | 0.2900 | 30.46 | <br>| 000617 | 0.2 | 0.3205 | 0.4965 | 8.90 |<\/li>\n<\/ol>\n\n\n\n<p>\u8ba1\u5212\u53c2\u6570\u8bf4\u660e\uff1a<br>target_weight=0.2\uff1a\u91c7\u7528\u7b49\u6743\u91cd\u5206\u914d\u7b56\u7565\uff0c\u6bcf\u53ea\u80a1\u7968\u914d\u7f6e20%\u4ed3\u4f4d<br>mom126\uff1a126\u5929\u52a8\u91cf\u6307\u6807\uff08\u8d8a\u9ad8\u8868\u793a\u8fd1\u671f\u8d8b\u52bf\u8d8a\u5f3a\uff09<br>annvol20\uff1a20\u5929\u5e74\u5316\u6ce2\u52a8\u7387\uff08\u8861\u91cf\u77ed\u671f\u98ce\u9669\uff0c\u6570\u503c\u8d8a\u4f4e\u98ce\u9669\u76f8\u5bf9\u8d8a\u5c0f\uff09<\/p>\n\n\n\n<ol start=\"4\" class=\"wp-block-list\">\n<li>\u7b56\u7565\u8868\u73b0\u8bc4\u4f30<br>\u98ce\u9669\u6536\u76ca\u7279\u5f81\uff1a\u5f53\u524d\u7b56\u7565\u5448\u73b0\u8d1f\u6536\u76ca\u3001\u9ad8\u6ce2\u52a8\u7279\u5f81\uff0c\u590f\u666e\u6bd4\u7387\u4e3a\u8d1f\u8868\u660e\u7b56\u7565\u672a\u80fd\u6709\u6548\u521b\u9020\u8d85\u989d\u6536\u76ca<br>\u6700\u5927\u56de\u64a4\u98ce\u9669\uff1a35.05%\u7684\u6700\u5927\u56de\u64a4\u9700\u8981\u8b66\u60d5\uff0c\u53ef\u80fd\u8d85\u51fa\u591a\u6570\u6295\u8d44\u8005\u7684\u98ce\u9669\u627f\u53d7\u80fd\u529b<br>\u6301\u4ed3\u7b56\u7565\uff1a\u9009\u62e9\u4e865\u53ea\u52a8\u91cf\u7279\u5f81\u8f83\u5f3a\u7684\u80a1\u7968\u8fdb\u884c\u7b49\u6743\u91cd\u914d\u7f6e\uff0c\u517c\u987e\u4e86\u52a8\u91cf\u56e0\u5b50\u548c\u6ce2\u52a8\u7387\u63a7\u5236<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>A\u80a1\u53cc\u5468\u8c03\u4ed3\uff1a\u4e00\u952e\u65e5\u5e38\u91cf\u5316\u811a\u672c\uff08AkShare\uff09 \u4f60\u8981\u7684\u662f\u80fd\u6bcf\u5929\u4e00\u952e\u8dd1\u3001\u4e24\u6b21\/\u5468 <span class=\"readmore\"><a href=\"http:\/\/cnliutz.wicp.vip\/?p=6062\">Continue Reading<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,24],"tags":[],"class_list":["post-6062","post","type-post","status-publish","format-standard","hentry","category-2","category-24"],"_links":{"self":[{"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=\/wp\/v2\/posts\/6062","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6062"}],"version-history":[{"count":4,"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=\/wp\/v2\/posts\/6062\/revisions"}],"predecessor-version":[{"id":6067,"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=\/wp\/v2\/posts\/6062\/revisions\/6067"}],"wp:attachment":[{"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6062"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6062"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/cnliutz.wicp.vip\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6062"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}