股票每天一键跑python代码

A股双周调仓:一键日常量化脚本(AkShare)

你要的是能每天一键跑、两次/周调仓、风控中性的实用脚本。下面这套方案以沪深300成分为底层池(稳流动性,避免幸存者偏差),周二/周四收盘后出信号,次日开盘按目标权重成交,含成本、滑点与涨跌停成交约束,并输出订单与绩效图。


策略与风控设计

  • 目标市场与频率: A股,周二/周四调仓;信号用当日收盘数据,次日开盘成交,避开“偷看未来”。
  • 标的池: 沪深300成分,辅以滚动流动性过滤(近60日日均成交量门槛)。
  • 选股逻辑(中风险,趋势+动量+波动约束):
  • 趋势: 20日均线在60日均线上方,收盘在MA20上方。
  • 动量: 126日动量为正;RSI处于温和区间(45–65)以降低追涨顶。
  • 波动/过热: 收盘低于布林带上轨,避免过度扩张。
  • 持仓构建: 从通过筛选的股票中选前 TopK(按动量排序),用波动率倒数配权,单票上限 12%,默认最多 15 只。
  • 权重公式:对第 (i) 只,计算 20 日年化波动 (\sigma_i),原始权重 (w_i’ = 1/\sigma_i),归一化后得到 (w_i),并进行上限裁剪与再归一化。
  • 交易与成本:
  • 成交价: 次日开盘价。
  • 费率: 佣金 0.0005(双边),卖出印花税 0.001(单边),滑点 0.0005(双边)。
  • 涨跌停: 若次日开盘相对昨收涨幅 ≥9.5%(买入)或跌幅 ≤-9.5%(卖出),视为无法成交,当天该笔订单跳过。
  • 绩效与图形: 组合净值曲线、最大回撤、持仓变动与当日调仓订单 CSV。

一键日常脚本(直接可运行)

先安装依赖:

pip install akshare pandas numpy matplotlib
# -*- coding: utf-8 -*-
# A股双周调仓 一键日常研究与回测脚本(AkShare)
# 运行环境:Python 3.9+;依赖:akshare, pandas, numpy, matplotlib

import akshare as ak
import pandas as pd
import numpy as np
import time
from datetime import datetime, timedelta
import matplotlib.pyplot as plt

# 修改为Windows系统默认中文字体
plt.rcParams["font.family"] = ["SimHei", "Microsoft YaHei", "SimSun"]
plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示问题

# -----------------------
# 参数区(按需修改)
# -----------------------
START_DATE = "2018-01-01"
END_DATE   = None  # None 表示到今日
CASH_INIT  = 1_000_000
MAX_POS    = 15           # 最多持仓数
MAX_W      = 0.12         # 单票权重上限
FEE_COMM   = 0.0005       # 佣金
FEE_STAMP  = 0.001        # 印花税(仅卖出)
SLIPPAGE   = 0.0005       # 滑点
LIQ_VOL_TH = 1_000_000    # 近60日平均成交量门槛(手/股),可按需要调
REBAL_WEEKDAYS = {1, 3}   # 周二(1)、周四(3) 调仓;Python: Mon=0

# -----------------------
# 工具函数:指标
# -----------------------
def sma(s, n):
    return s.rolling(n).mean()

def rsi(close, n=14):
    delta = close.diff()
    up = np.where(delta > 0, delta, 0.0)
    dn = np.where(delta < 0, -delta, 0.0)
    up_ema = pd.Series(up, index=close.index).ewm(alpha=1/n, adjust=False).mean()
    dn_ema = pd.Series(dn, index=close.index).ewm(alpha=1/n, adjust=False).mean()
    rs = up_ema / dn_ema.replace(0, np.nan)
    return 100 - (100 / (1 + rs))

def bbands(close, n=20, k=2):
    mid = close.rolling(n).mean()
    std = close.rolling(n).std(ddof=0)
    up = mid + k * std
    dn = mid - k * std
    return mid, up, dn

def true_range(df):
    prev_close = df["close"].shift(1)
    tr = pd.concat([
        (df["high"] - df["low"]).abs(),
        (df["high"] - prev_close).abs(),
        (df["low"] - prev_close).abs()
    ], axis=1).max(axis=1)
    return tr

def ann_vol(close, n=20):
    ret = close.pct_change()
    return ret.rolling(n).std() * np.sqrt(252)

# -----------------------
# 数据获取与基准日历
# -----------------------
def get_trade_calendar(start=START_DATE, end=END_DATE):
    cal = ak.tool_trade_date_hist_sina()
    cal["trade_date"] = pd.to_datetime(cal["trade_date"])
    if end is None:
        end = datetime.now().strftime("%Y-%m-%d")
    cal = cal[(cal["trade_date"] >= pd.to_datetime(start)) &
              (cal["trade_date"] <= pd.to_datetime(end))]["trade_date"].sort_values()
    return cal.tolist()

def get_hs300_symbols():
    df = ak.index_stock_cons(symbol="000300")
    # 列可能是 '品种代码' 或 '成分券代码'; 做兼容
    for col in ["品种代码", "成分券代码", "代码", "code"]:
        if col in df.columns:
            return sorted(df[col].astype(str).str.zfill(6).unique().tolist())
    # 兜底
    return sorted(df.iloc[:,0].astype(str).str.zfill(6).unique().tolist())

def get_hist(code, start=START_DATE, end=END_DATE, adjust="qfq"):
    if end is None:
        end = datetime.now().strftime("%Y%m%d")
    df = ak.stock_zh_a_hist(symbol=code, period="daily",
                            start_date=start.replace("-",""),
                            end_date=end.replace("-",""),
                            adjust=adjust)
    # 兼容列名
    mapper = {"日期":"date","开盘":"open","收盘":"close","最高":"high","最低":"low","成交量":"volume","成交额":"amount"}
    df = df.rename(columns=mapper)
    df["date"] = pd.to_datetime(df["date"])
    cols = [c for c in ["date","open","high","low","close","volume","amount"] if c in df.columns]
    df = df[cols].set_index("date").sort_index()
    df = df.dropna()
    return df

# -----------------------
# 信号与筛选
# -----------------------
def compute_indicators(df):
    out = df.copy()
    out["MA20"] = sma(out["close"], 20)
    out["MA60"] = sma(out["close"], 60)
    out["RSI14"] = rsi(out["close"], 14)
    out["MOM126"] = out["close"] / out["close"].shift(126) - 1
    mid, up, dn = bbands(out["close"], 20, 2)
    out["BB_MID"], out["BB_UP"], out["BB_DN"] = mid, up, dn
    out["ANNVOL20"] = ann_vol(out["close"], 20)
    out["TR"] = true_range(out)
    out["ATR20"] = out["TR"].rolling(20).mean()
    return out

def pass_screen(row):
    c1 = row["MA20"] > row["MA60"]
    c2 = row["close"] > row["MA20"]
    c3 = row["MOM126"] > 0
    c4 = 45 <= row["RSI14"] <= 65
    c5 = row["close"] < row["BB_UP"]
    return c1 and c2 and c3 and c4 and c5

# -----------------------
# 回测:两次/周调仓,次日开盘成交
# -----------------------
def backtest_portfolio(symbols, start=START_DATE, end=END_DATE, cash_init=CASH_INIT):
    # 下载数据
    data = {}
    for i, sym in enumerate(symbols, 1):
        try:
            df = get_hist(sym, start, end)
            data[sym] = compute_indicators(df)
        except Exception:
            pass
        time.sleep(0.2)  # 温和限速
    # 统一日历
    all_dates = sorted(set().union(*[df.index for df in data.values()]))
    cal = pd.DatetimeIndex(all_dates)
    # 选择调仓日(周二/周四且是交易日)
    rebal_days = [d for d in cal if d.weekday() in REBAL_WEEKDAYS]
    # 过滤:近60日均量
    def liquid_ok(df, dt):
        window = df.loc[:dt].tail(60)
        if "volume" not in window: 
            return True
        return window["volume"].mean() >= LIQ_VOL_TH

    # 状态
    cash = cash_init
    positions = {}  # sym -> shares
    nav_series = []
    dd_series = []
    equity = cash
    peak = equity
    last_prices = {}

    # 逐日仿真
    for i, d in enumerate(cal[:-1]):  # 至倒数第二天(因次日开盘成交)
        todays_vals = {}
        # 更新持仓市值
        for sym, df in data.items():
            if d in df.index:
                last_prices[sym] = df.at[d, "close"]
            if sym in positions and sym in last_prices:
                todays_vals[sym] = positions[sym] * last_prices[sym]
        equity = cash + sum(todays_vals.values())
        peak = max(peak, equity)
        drawdown = (equity / peak) - 1
        nav_series.append((d, equity))
        dd_series.append((d, drawdown))

        # 调仓信号(用今日收盘)
        if d in rebal_days:
            # 生成候选
            candidates = []
            for sym, df in data.items():
                if d not in df.index: 
                    continue
                if not liquid_ok(df, d):
                    continue
                row = df.loc[d]
                # 要求指标有效
                if np.any(pd.isna(row[["MA20","MA60","RSI14","MOM126","BB_UP","ANNVOL20"]])):
                    continue
                if pass_screen(row):
                    candidates.append((sym, row["MOM126"], row["ANNVOL20"]))
            # 排序与截断
            candidates.sort(key=lambda x: x[1], reverse=True)
            picks = candidates[:MAX_POS]

            # 计算目标权重(波动率倒数)
            if picks:
                vols = np.array([max(1e-6, x[2]) for x in picks])
                inv = 1.0 / vols
                w_raw = inv / inv.sum()
                # 单票上限
                w_capped = np.minimum(w_raw, MAX_W)
                w = w_capped / w_capped.sum()
                target = {sym: w[j] for j, (sym, _, _) in enumerate(picks)}
            else:
                target = {}

            # 次日开盘执行
            nd = cal[i+1]
            # 构建目标头寸价值
            target_value = {sym: equity * w for sym, w in target.items()}

            # 先卖出未在目标内或超配部分
            for sym in list(positions.keys()):
                df = data.get(sym)
                if df is None or nd not in df.index or d not in df.index:
                    continue
                prev_close = df.at[d, "close"]
                next_open = df.at[nd, "open"]
                # 跌停无法卖出(近似)
                if next_open <= prev_close * (1 - 0.095):
                    continue
                price = next_open * (1 - SLIPPAGE)
                cur_val = positions[sym] * price
                tgt_val = target_value.get(sym, 0.0)
                if cur_val > tgt_val + 1:  # 超配或不在目标
                    sell_val = cur_val - tgt_val
                    shares = int(sell_val // price)
                    if shares > 0:
                        proceeds = shares * price * (1 - FEE_COMM - FEE_STAMP)
                        positions[sym] -= shares
                        if positions[sym] <= 0:
                            positions.pop(sym, None)
                        cash += proceeds

            # 再买入不达标或新标的
            for sym, tgt_val in target_value.items():
                df = data.get(sym)
                if df is None or nd not in df.index or d not in df.index:
                    continue
                prev_close = df.at[d, "close"]
                next_open = df.at[nd, "open"]
                # 涨停无法买入(近似)
                if next_open >= prev_close * (1 + 0.095):
                    continue
                price = next_open * (1 + SLIPPAGE)
                cur_shares = positions.get(sym, 0)
                cur_val = cur_shares * price
                buy_val = max(0.0, tgt_val - cur_val)
                shares = int(buy_val // price)
                if shares > 0 and cash > shares * price * (1 + FEE_COMM):
                    cost = shares * price * (1 + FEE_COMM)
                    cash -= cost
                    positions[sym] = cur_shares + shares

    nav = pd.Series({d: v for d, v in nav_series}).sort_index()
    dd = pd.Series({d: v for d, v in dd_series}).sort_index()
    ret = nav.pct_change().fillna(0)
    stats = {
        "CAGR": (nav.iloc[-1] / nav.iloc[0]) ** (252/len(nav)) - 1,
        "Vol": ret.std() * np.sqrt(252),
        "Sharpe": (ret.mean() / (ret.std() + 1e-9)) * np.sqrt(252),
        "MaxDD": dd.min()
    }
    return nav, dd, positions, stats

# -----------------------
# 今日调仓计划(实用日常)
# -----------------------
def today_rebalance_plan():
    today = pd.Timestamp(datetime.now().date())
    # 若今天不是交易日或不是周二/周四,直接提示
    cal = get_trade_calendar((today - pd.Timedelta(days=10)).strftime("%Y-%m-%d"),
                             today.strftime("%Y-%m-%d"))
    cal_idx = pd.DatetimeIndex(cal)
    if today not in cal_idx or today.weekday() not in REBAL_WEEKDAYS:
        print("今天不是计划调仓日(或非交易日)。")
        return

    syms = get_hs300_symbols()
    plan_rows = []
    for sym in syms:
        try:
            df = get_hist(sym, (today - pd.Timedelta(days=400)).strftime("%Y-%m-%d"),
                               today.strftime("%Y-%m-%d"))
            df = compute_indicators(df)
            if len(df) < 200 or today not in df.index:
                continue
            row = df.loc[today]
            # 流动性
            if "volume" in df:
                if df.loc[:today].tail(60)["volume"].mean() < LIQ_VOL_TH:
                    continue
            if np.any(pd.isna(row[["MA20","MA60","RSI14","MOM126","BB_UP","ANNVOL20"]])):
                continue
            if pass_screen(row):
                plan_rows.append({
                    "code": sym,
                    "mom126": row["MOM126"],
                    "annvol20": row["ANNVOL20"],
                    "close": row["close"]
                })
        except Exception:
            pass
        time.sleep(0.05)

    if not plan_rows:
        print("今日无标的通过筛选。")
        return

    dfp = pd.DataFrame(plan_rows).sort_values("mom126", ascending=False).head(MAX_POS)
    inv = 1.0 / np.maximum(1e-6, dfp["annvol20"].values)
    w_raw = inv / inv.sum()
    w_capped = np.minimum(w_raw, MAX_W)
    w = w_capped / w_capped.sum()
    dfp["target_weight"] = w
    dfp.to_csv(f"rebalance_plan_{today.strftime('%Y%m%d')}.csv", index=False, encoding="utf-8-sig")
    print("今日计划(次日开盘执行,权重已截顶):")
    print(dfp[["code","target_weight","mom126","annvol20","close"]])

# -----------------------
# 主函数:回测 + 图形 + 今日计划
# -----------------------
if __name__ == "__main__":
    print("获取沪深300成分...")
    symbols = get_hs300_symbols()
    print(f"成分股数量:{len(symbols)}")

    print("开始回测(这可能需要几分钟)...")
    nav, dd, positions, stats = backtest_portfolio(symbols, START_DATE, END_DATE, CASH_INIT)
    print("回测统计:")
    for k, v in stats.items():
        print(f"{k}: {v:.4f}")

    # 绘制净值与回撤
    fig, ax = plt.subplots(2, 1, figsize=(10, 6), sharex=True,
                           gridspec_kw={"height_ratios":[3,1]})
    nav_norm = nav / nav.iloc[0]
    ax[0].plot(nav_norm.index, nav_norm.values, label="Portfolio")
    ax[0].set_title("组合净值(归一)")
    ax[0].legend()
    ax[0].grid(True, alpha=0.3)
    ax[1].fill_between(dd.index, dd.values, 0, color="red", alpha=0.3)
    ax[1].set_title("回撤")
    ax[1].grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig("backtest_nav_drawdown.png", dpi=150)
    plt.show()

    # 当日调仓计划(在 CST 下午 15:10 后运行更合适)
    today_rebalance_plan()

如何每天一键跑

  • 时间点: 沧州本地时间建议在交易日 15:10 之后运行,保证当日收盘数据可用。
  • 命令行:
  • 运行: python run_quant.py
  • 输出: backtest_nav_drawdown.png、rebalance_plan_YYYYMMDD.csv,并在控制台打印统计与当日计划。
  • 定时任务:
  • Linux crontab: 10 15 * * 1-5 /usr/bin/python3 /path/run_quant.py >> /path/log.txt 2>&1
  • Windows 任务计划: 设为工作日 15:10 触发。

可调参数与扩展

  • 可调参数:
  • MAX_POS/MAX_W: 控制集中度与风险。
  • LIQ_VOL_TH: 提高门槛可进一步降低流动性风险。
  • 费率/滑点: 按你的券商费率与成交体验微调。
  • 风控增强:
  • 止损/跟踪止盈: 以 ATR 为单位,如价格跌破 MA60 或 3×ATR 跌幅平仓。
  • 涨跌停细化: 科创/创业板 20% 规则可按股票板块调整阈值。
  • 基准比较: 叠加沪深300指数净值,计算超额与信息比率。
  • 研究维度:
  • 横截面多因子: 以动量、波动、估值(PE/PB,需额外数据)做打分,周二/周四同频调仓。
  • 再平衡鲁棒性: 改为“阈值再平衡”(偏离>25%才调仓)降低换手。

如果你想把标的池改为中证500或加入行业中性约束,或者把调仓日改成“每周最近的周二与周四,如遇节假日顺延”,我可以把上述脚本再细化成模块化的研究框架,并加上性能剖析与结果缓存来加速日常跑批。

以下是程序运行结果的详细解释:

  1. 数据获取与回测概况
    沪深300成分股数量:282:成功获取了当前沪深300指数的282只成分股
    回测耗时提示:策略回测需要一定计算时间,符合预期
  2. 核心回测指标解读
    | 指标 | 数值 | 含义解释 | 策略评估 |
    |——–|———|———————————–|—————————|
    | CAGR | -0.0900 | 复合年化增长率:-9.00% | 策略整体年化亏损9% |
    | Vol | 0.2348 | 波动率:23.48% | 收益波动较大 |
    | Sharpe | -0.2843 | 夏普比率:-0.28 | 风险调整后收益为负,表现弱于无风险资产 |
    | MaxDD | -0.3505 | 最大回撤:-35.05% | 策略历史最大亏损幅度为35% |
  3. 今日投资计划(次日开盘执行)
    | 代码 | 目标权重 | 126天动量(mom126) | 20天年化波动率(annvol20) | 收盘价 |
    |——–|———-|——————-|————————–|——–|
    | 300394 | 0.2 | 0.6719 | 0.7191 | 107.87 |
    | 603799 | 0.2 | 0.4765 | 0.4454 | 44.25 |
    | 002463 | 0.2 | 0.4701 | 0.5822 | 55.35 |
    | 002074 | 0.2 | 0.3858 | 0.2900 | 30.46 |
    | 000617 | 0.2 | 0.3205 | 0.4965 | 8.90 |

计划参数说明:
target_weight=0.2:采用等权重分配策略,每只股票配置20%仓位
mom126:126天动量指标(越高表示近期趋势越强)
annvol20:20天年化波动率(衡量短期风险,数值越低风险相对越小)

  1. 策略表现评估
    风险收益特征:当前策略呈现负收益、高波动特征,夏普比率为负表明策略未能有效创造超额收益
    最大回撤风险:35.05%的最大回撤需要警惕,可能超出多数投资者的风险承受能力
    持仓策略:选择了5只动量特征较强的股票进行等权重配置,兼顾了动量因子和波动率控制