#!/usr/bin/env python3 """step42_atm_position.py — 气氛分级仓位(保留信号量,控制风险) 不硬砍信号,改为按气氛分级仓位: - 气氛好(新闻>=中位 + 连跌<=3): 满仓15% - 气氛中(新闻>=中位 或 连跌<=3): 半仓8% - 气氛差(新闻<中位 且 连跌>3): 迷你仓4% 或 不买 """ import numpy as np import pandas as pd import sqlite3 import sys sys.path.insert(0, "/tmp") from sr_calculator import SRCalculator print("=== 加载 ===", flush=True) panel = pd.read_pickle("/tmp/panel_12d.pkl") panel = panel.sort_values(["code", "date"]).reset_index(drop=True) panel["_key"] = panel["code"] + "_" + panel["date"] pos_map = {k: i for i, k in enumerate(panel["_key"])} dates = sorted(panel["date"].unique()) conn = sqlite3.connect("file:/home/hmo/MoFin/data/mofin.db?mode=ro", uri=True) idx_df = pd.read_sql("SELECT date, close, high FROM stock_daily WHERE code='sh000001' ORDER BY date", conn) idx_df["date"] = idx_df["date"].astype(str) idx_df["mkt_ret5"] = idx_df["close"].pct_change(5) * 100 idx_df["hi60"] = idx_df["high"].rolling(60).max() idx_df["mkt_dd60"] = (idx_df["close"] / idx_df["hi60"] - 1) * 100 close_arr = idx_df["close"].values down_days, streak, prev = [], 0, None for c in close_arr: streak = streak + 1 if (prev is not None and c < prev) else 0 down_days.append(streak) prev = c idx_df["mkt_down_days"] = down_days dd_map = dict(zip(idx_df["date"], idx_df["mkt_dd60"])) down_map = dict(zip(idx_df["date"], idx_df["mkt_down_days"])) panel["mkt_dd60"] = panel["date"].map(dd_map) panel["mkt_down_days"] = panel["date"].map(down_map) news_cnt = pd.read_sql("SELECT substr(date,1,10) d, COUNT(*) c FROM stock_news GROUP BY d", conn) news_cnt["d"] = news_cnt["d"].astype(str) news_cnt["mkt_news5"] = news_cnt["c"].rolling(5, min_periods=1).mean() news_map = dict(zip(news_cnt["d"], news_cnt["mkt_news5"])) panel["mkt_news5"] = panel["date"].map(lambda d: news_map.get(d, np.nan)) sig_pool = ( (panel["mkt_rsi"] < 50) & (panel["mcap_q"] < 0.2) & (panel["pe_q"] < 0.2) & (panel["news3"] >= 1) & (panel["sec_ret20"] < 0) & (panel["bias60"] < -20) & (panel["mkt_dd60"] <= -5) ) news_med = panel.loc[sig_pool, "mkt_news5"].median() print("新闻中位:", news_med, flush=True) # 气氛标签 panel["atm"] = "差" panel.loc[(panel["mkt_news5"] >= news_med) & (panel["mkt_down_days"] <= 3), "atm"] = "好" panel.loc[((panel["mkt_news5"] >= news_med) | (panel["mkt_down_days"] <= 3)) & (panel["atm"]=="差"), "atm"] = "中" cand = panel[sig_pool][["code", "date", "atm"]].copy() cand = cand.sort_values(["code", "date"]) cand["prev"] = cand.groupby("code")["date"].shift(1) cand["gap"] = (pd.to_datetime(cand["date"]) - pd.to_datetime(cand["prev"])).dt.days cand = cand[(cand["prev"].isna()) | (cand["gap"] > 30)] print("信号:", len(cand), "气氛分布:", cand["atm"].value_counts().to_dict(), flush=True) codes = cand["code"].unique().tolist() ph = ",".join("?" * len(codes)) df = pd.read_sql("SELECT code, date, close, high, low FROM stock_daily WHERE code IN ({}) ORDER BY code, date".format(ph), conn, params=codes) df["date"] = df["date"].astype(str) df["code"] = df["code"].astype(str).str.zfill(6) df = df.sort_values(["code", "date"]).reset_index(drop=True) df["_key"] = df["code"] + "_" + df["date"] dpos = {k: i for i, k in enumerate(df["_key"])} closes = df["close"].values highs = df["high"].values lows = df["low"].values cand["kidx"] = (cand["code"] + "_" + cand["date"]).map(dpos) cand = cand.dropna(subset=["kidx"]).copy() cand["kidx"] = cand["kidx"].astype(int) sr = SRCalculator() print("=== 计算支撑压力 ===", flush=True) sig_list = [] for r in cand.itertuples(): bars_code = sr.get_bars(r.code) d_idx = bars_code.index[bars_code["date"] == r.date] if len(d_idx) == 0: continue sr_full = sr.sr_full(r.code, d_idx[0]) pv = sr_full["pivot"] chip = sr_full["chip"] if not pv: continue sig_close = closes[r.kidx] support = pv["s2"] if chip and chip["chip_ss"] < sig_close: support = max(pv["s2"], chip["chip_ss"]) resist = pv["r2"] if chip and chip["chip_sr"] > sig_close: resist = min(pv["r2"], chip["chip_sr"]) if support >= sig_close or resist <= sig_close: continue sig_list.append({"code": r.code, "date": r.date, "kidx": r.kidx, "atm": r.atm, "sig_close": sig_close, "support": support, "resist": resist}) sigdf = pd.DataFrame(sig_list) print("有支撑压力:", len(sigdf), flush=True) sig_by_date = {} for r in sigdf.itertuples(): sig_by_date.setdefault(r.date, []).append(r) def run_sim(pos_map_atm, max_daily=5, slots=10, hold=40, stop_buf=0.05): """pos_map_atm: {'好':0.15, '中':0.08, '差':0.04}""" INIT_CAP = 1_000_000 positions = {} cash = INIT_CAP navs = [] for di, d in enumerate(dates): for code in list(positions.keys()): pos = positions[code] k = dpos.get(code + "_" + d) if k is None: continue hi, lo, cl = highs[k], lows[k], closes[k] exited = False if hi >= pos["tp"]: sell_qty = pos["qty"] // 2 if sell_qty > 0: cash += sell_qty * pos["tp"] pos["qty"] -= sell_qty if pos["qty"] <= 0: del positions[code] exited = True if not exited and lo <= pos["stop"]: cash += pos["qty"] * lo del positions[code] exited = True if not exited and di - pos["entry_di"] >= hold: cash += pos["qty"] * cl del positions[code] if d in sig_by_date: bought = 0 for r in sig_by_date[d]: if bought >= max_daily or len(positions) >= slots: break if r.code in positions: continue cur_nav = cash for c, p in positions.items(): k = dpos.get(c + "_" + d) px = closes[k] if k is not None else p["avg_cost"] cur_nav += p["qty"] * px pos_val = cur_nav * pos_map_atm.get(r.atm, 0.05) price = r.sig_close if price <= 0: continue qty = int(pos_val / price) if qty <= 0 or qty * price > cash: continue cash -= qty * price positions[r.code] = { "entry_di": di, "qty": qty, "avg_cost": price, "stop": r.support * (1 - stop_buf), "tp": r.resist, } bought += 1 nav = cash for c, p in positions.items(): k = dpos.get(c + "_" + d) px = closes[k] if k is not None else p["avg_cost"] nav += p["qty"] * px navs.append(nav) nav_arr = np.array(navs) final = nav_arr[-1] years = len(navs) / 250 cagr = ((final / INIT_CAP) ** (1 / years) - 1) * 100 if final > 0 else -100 cummax = np.maximum.accumulate(nav_arr) dd = ((nav_arr - cummax) / cummax).min() * 100 cash_ratio = np.mean([1 for _ in navs]) # placeholder return cagr, dd print("\n=== 气氛分级仓位 ===", flush=True) # 方案1: 好15/中8/差4 c, d = run_sim({"好": 0.15, "中": 0.08, "差": 0.04}) print("好15/中8/差4: 年化={:.2f}% 回撤={:.1f}%".format(c, d), flush=True) # 方案2: 好15/中10/差6 c, d = run_sim({"好": 0.15, "中": 0.10, "差": 0.06}) print("好15/中10/差6: 年化={:.2f}% 回撤={:.1f}%".format(c, d), flush=True) # 方案3: 好15/中12/差8 c, d = run_sim({"好": 0.15, "中": 0.12, "差": 0.08}) print("好15/中12/差8: 年化={:.2f}% 回撤={:.1f}%".format(c, d), flush=True) # 方案4: 好15/中15/差0(只差气氛不买) c, d = run_sim({"好": 0.15, "中": 0.15, "差": 0.0}) print("好15/中15/差0: 年化={:.2f}% 回撤={:.1f}%".format(c, d), flush=True) # 方案5: 全部15(基准,无分级) c, d = run_sim({"好": 0.15, "中": 0.15, "差": 0.15}) print("全部15(基准): 年化={:.2f}% 回撤={:.1f}%".format(c, d), flush=True) print("\n=== 完成 ===", flush=True)