AI 生成

ps:本人只是把csdn当成自己的草稿本在使用,偶尔发的文章是别人可以直接使用的成品,但大多数时候是这种零散的资料收集。这种你们就不用看了

买股票的“策略设计”可以像你做软件架构一样,先分层、再逐步细化。一个完整的买入策略通常包含四个核心部分:目标设定 → 策略框架 → 执行机制 → 风险控制


1. 明确目标

在设计策略前,你要先回答:

  • 你的目标是 长期增值(类似买指数基金,3-5年以上持有),还是 短期套利(几天到几周的机会)?
  • 你愿意承受多大的波动?(10%?30%?)
  • 你期望的年化回报是多少?

这一步类似于“需求分析”,决定了后面所有的设计。


2. 策略框架

可以从几个维度来组合:

(1)基本面驱动

  • 场景:适合长期投资。
  • 方法:关注公司盈利能力(ROE、利润增长)、行业位置、护城河。
  • 案例:如果你相信 AI 发展趋势,就挑行业龙头并长期持有。

(2)技术面驱动

  • 场景:适合短期或波段操作。
  • 方法:用均线、成交量、形态来判断买点和卖点。
  • 案例:突破60日均线时买入,跌破时卖出。

(3)事件驱动

  • 场景:中短期机会。
  • 方法:关注财报、政策、并购消息。
  • 案例:新能源补贴政策发布 → 买入龙头。

(4)量化/程序化

  • 场景:更偏专业。
  • 方法:用算法(趋势跟随、均值回归、因子模型)筛选买点。
  • 案例:设置 RSI < 30 自动买入,>70 自动卖出。

3. 执行机制

  • 买入条件:明确触发点(例如“PE < 20 且营收增长 > 15%” 或 “MACD 金叉 + 放量”)。
  • 仓位管理:一次买满还是分批买?通常建议分批买入。
  • 卖出条件:必须事先设定(止损线,比如-10%;止盈线,比如+30%)。

4. 风险控制

就像软件容错一样,风险管理比“选哪只股票”更关键:

  • 单只股票仓位不超过总资金的20%
  • 设置止损(亏损到某个程度自动卖出)。
  • 行业/资产分散:不要把钱全压在一个行业或国家。

5. 一个例子(技术+基本面混合)

  • 先筛选:年营收增速 > 20%、市盈率合理(<30)、行业趋势向上。
  • 买点:股价突破 120 日均线,并且成交量 > 20 日平均成交量。
  • 仓位:总资金的 30%,分三次买入。
  • 止损:跌破 120 日均线 5% 卖出。
  • 止盈:上涨 30% 卖一半,剩下长期持有。

要不要我帮你设计一个 可执行的示例策略(含条件、买卖点、仓位分配),然后给你做成像伪代码/流程图那样的结构?这样你可以像跑程序一样“回测”这个策略。
太好了,我们把策略落地成 Python,并顺手给你一套可直接回测的脚本和示例数据。

我已在你的会话里生成了这些文件(可直接下载):

  • 下载脚本:ma_breakout_backtest.py
  • 下载示例数据:synthetic_TEST.csv
  • 示例回测图:equity_curve_synthetic.png

脚本内容要点(默认可改的超参见脚本顶部 Params 和命令行参数):

  • 买点:收盘价上穿并站上 SMA(120),且当日成交量 > 20 日均量,且之前 10 天一直在均线下方(避免假突破)。
  • 加仓:分三笔各 1/3 仓位:信号日入第一笔;价格较第一笔涨 +3% 入第二笔;涨 +6% 入第三笔。
  • 卖点/风控:收盘价跌破 SMA120 的 5% 缓冲 或 跌破 持仓均价的 10%(硬止损)即清仓;+30% 先止盈一半,剩余靠卖点退出。
  • 交易成本:默认双向各 5 bps 手续费和 5 bps 滑点(可改)。
  • 输出:终端打印收益统计;可绘制策略净值 vs. 买入并持有。

如何本地跑历史数据

  1. 用你自己的 CSV(字段:Date,Open,High,Low,Close,Volume):
python ma_breakout_backtest.py --csv your_stock.csv --ticker YOURTICK --plot
  1. 若本地能联网,可用 yfinance 抓数据:
pip install yfinance
python ma_breakout_backtest.py --ticker AAPL --start 2015-01-01 --end 2025-09-01 --plot
  1. 直接跑示例合成数据:
python ma_breakout_backtest.py --csv synthetic_TEST.csv --ticker TEST --plot

常用参数调优(命令行)

  • 均线与量能窗口:--sma_len 120 --avgvol_len 20
  • 假突破过滤:--lookback_below 10
  • 加仓阈值:--add1 0.03 --add2 0.06
  • 卖点:--sma_exit_buf 0.05 --hard_stop 0.10 --tp 0.30
  • 成本:--comm_bps 5 --slip_bps 5
  • 初始资金:--initial 100000

接下来可以怎么迭代(前瞻性建议)

  • 多标的 & 资金分配:把脚本扩展为投资组合回测(多只票+等权或波动率预算)。
  • 更稳健的入场:加入 回撤后再突破突破后 N 日内不回落 的确认条件。
  • 风控替代:尝试 ATR 跟踪止损移动均线止盈,对比回撤与胜率。
  • 参数鲁棒性:做 参数网格搜索滚动/步进回测(walk-forward),避免过拟合。
  • 交易成本敏感性:高/低成本场景下对比净值和夏普,评估策略在真实市场的可行性。
  • 事件过滤:财报日前后避开或减仓,看看对回撤的改善。

如果你愿意,我可以把脚本升级为多标的组合回测版本,或加上参数网格搜索性能热力图导出,方便你系统调参。

#!/usr/bin/env python3
"""
MA Breakout Backtester (single-stock)
-------------------------------------

Strategy (default parameters, configurable via CLI):
- Entry: Close crosses and *closes above* SMA(120) AND Volume > AvgVolume(20),
         and price has been below SMA for at least `lookback_below` days prior (default 10).
- Position sizing: 3 tranches (each ~1/3 of target position).
  * 1st tranche on entry signal day at close.
  * 2nd tranche when price is +3% above first entry price.
  * 3rd tranche when price is +6% above first entry price.
- Risk controls:
  * Exit if Close < SMA(120)*(1 - sma_exit_buf)  OR
    Close < avg_entry_price * (1 - hard_stop), whichever triggers first.
  * Take-profit: when the position PnL reaches +30%, sell 1/2; keep the rest until exit.
- Transaction costs:
  * Commission in bps (default 5 bps = 0.05% per trade).
  * Slippage in bps (default 5 bps) applied to fills.

Data sources:
- Preferred: CSV with columns: Date, Open, High, Low, Close, Volume (case-insensitive), Date parseable.
- Optional: yfinance (if internet available).

Usage examples:
    # Load from CSV
    python ma_breakout_backtest.py --csv mydata.csv --ticker TEST

    # Fetch from yfinance (needs internet)
    python ma_breakout_backtest.py --ticker AAPL --start 2015-01-01 --end 2025-09-01

Outputs:
- Summary metrics printed to stdout
- Optional: save equity curve to PNG

Author: ChatGPT (GPT-5 Thinking)
"""
import argparse
import sys
import math
from dataclasses import dataclass
from typing import Optional, Tuple, List

import numpy as np
import pandas as pd

# yfinance is optional; only import if requested
def _try_import_yf():
    try:
        import yfinance as yf
        return yf
    except Exception:
        return None

@dataclass
class Params:
    sma_len: int = 120
    avgvol_len: int = 20
    lookback_below: int = 10
    add1_level: float = 0.03   # +3% for 2nd tranche
    add2_level: float = 0.06   # +6% for 3rd tranche
    sma_exit_buf: float = 0.05 # 5% below SMA exit buffer
    hard_stop: float = 0.10    # 10% hard stop from avg entry
    take_profit: float = 0.30  # take half at +30%
    commission_bps: float = 5.0
    slippage_bps: float = 5.0
    initial_capital: float = 100000.0
    risk_per_trade: float = 1.0   # % of equity to target per new position (not enforced tightly in tranche model)

def load_data_from_csv(path: str) -> pd.DataFrame:
    df = pd.read_csv(path)
    cols = {c.lower(): c for c in df.columns}
    required = ['date','open','high','low','close','volume']
    for k in required:
        if k not in cols:
            raise ValueError(f"CSV missing column: {k}")
    df['Date'] = pd.to_datetime(df[cols['date']])
    df = df.rename(columns={
        cols['open']: 'Open', cols['high']:'High', cols['low']:'Low',
        cols['close']:'Close', cols['volume']:'Volume'
    })[['Date','Open','High','Low','Close','Volume']].sort_values('Date').reset_index(drop=True)
    return df

def load_data_yf(ticker: str, start: Optional[str], end: Optional[str]) -> pd.DataFrame:
    yf = _try_import_yf()
    if yf is None:
        raise RuntimeError("yfinance not installed or import failed. Install with: pip install yfinance")
    data = yf.download(ticker, start=start, end=end, auto_adjust=True, progress=False)
    if data is None or data.empty:
        raise RuntimeError("No data returned from yfinance.")
    data = data.reset_index()
    # yfinance columns: Date, Open, High, Low, Close, Adj Close, Volume
    df = data[['Date','Open','High','Low','Close','Volume']].copy()
    df = df.sort_values('Date').reset_index(drop=True)
    return df

def prepare_indicators(df: pd.DataFrame, p: Params) -> pd.DataFrame:
    df = df.copy()
    df['SMA'] = df['Close'].rolling(p.sma_len).mean()
    df['AvgVol'] = df['Volume'].rolling(p.avgvol_len).mean()
    # track whether price has been below SMA for lookback days prior
    below = df['Close'] < df['SMA']
    df['BelowLookback'] = below.rolling(p.lookback_below, min_periods=p.lookback_below).apply(lambda x: float(all(x)), raw=False).fillna(0.0) > 0.5
    return df

@dataclass
class PositionState:
    qty: float = 0.0
    avg_price: float = 0.0
    first_entry_price: float = 0.0
    added_add1: bool = False
    added_add2: bool = False
    realized_pnl: float = 0.0

def _apply_tc(price: float, commission_bps: float, slippage_bps: float, side: int) -> float:
    # side: +1 buy, -1 sell
    price_slip = price * (1 + slippage_bps/10000.0 * side)
    commission = price_slip * (commission_bps/10000.0)
    return price_slip + commission * side

def backtest(df: pd.DataFrame, p: Params, ticker: str = "TICK") -> Tuple[pd.DataFrame, dict]:
    df = prepare_indicators(df, p)
    equity = p.initial_capital
    pos = PositionState()
    equity_curve = []
    trades: List[dict] = []

    for i, row in df.iterrows():
        date = row['Date']
        close = float(row['Close'])
        sma = float(row['SMA']) if not math.isnan(row['SMA']) else None
        avgvol = float(row['AvgVol']) if not math.isnan(row['AvgVol']) else None
        below_lb = bool(row['BelowLookback'])

        # Mark-to-market
        m2m = pos.qty * close
        equity_now = equity + m2m
        equity_curve.append({'Date': date, 'Equity': equity_now, 'Close': close})

        # Skip until indicators ready
        if sma is None or avgvol is None:
            continue

        # Generate signals
        entry_signal = (close > sma) and (row['Volume'] > avgvol) and below_lb
        exit_sma = close < sma * (1 - p.sma_exit_buf)
        exit_hard = (pos.qty > 0) and (close < pos.avg_price * (1 - p.hard_stop))
        take_half = (pos.qty > 0) and ((close - pos.avg_price)/pos.avg_price >= p.take_profit)

        # Position sizing target: use full equity, but we add in tranches.
        # For illustration, we'll size such that full position is 90% of equity (aggressive),
        # but tranches keep the staged entry behavior.
        target_position_value = equity_now * 0.9

        # ENTRY
        if entry_signal and pos.qty == 0:
            # First tranche at today's close (with TC)
            fill_px = _apply_tc(close, p.commission_bps, p.slippage_bps, side=+1)
            tranche_value = target_position_value / 3.0
            qty = tranche_value / fill_px
            cost = qty * fill_px
            pos.qty += qty
            pos.avg_price = fill_px
            pos.first_entry_price = fill_px
            equity -= cost  # cash out
            trades.append({'Date': date, 'Action': 'BUY_1', 'Price': fill_px, 'Qty': qty})
        elif pos.qty > 0:
            # ADD-ON 1
            if (not pos.added_add1) and (close >= pos.first_entry_price * (1 + p.add1_level)):
                fill_px = _apply_tc(close, p.commission_bps, p.slippage_bps, side=+1)
                tranche_value = target_position_value / 3.0
                qty = tranche_value / fill_px
                cost = qty * fill_px
                pos.avg_price = (pos.avg_price * pos.qty + fill_px * qty) / (pos.qty + qty)
                pos.qty += qty
                equity -= cost
                pos.added_add1 = True
                trades.append({'Date': date, 'Action': 'BUY_2', 'Price': fill_px, 'Qty': qty})
            # ADD-ON 2
            if (not pos.added_add2) and (close >= pos.first_entry_price * (1 + p.add2_level)):
                fill_px = _apply_tc(close, p.commission_bps, p.slippage_bps, side=+1)
                tranche_value = target_position_value / 3.0
                qty = tranche_value / fill_px
                cost = qty * fill_px
                pos.avg_price = (pos.avg_price * pos.qty + fill_px * qty) / (pos.qty + qty)
                pos.qty += qty
                equity -= cost
                pos.added_add2 = True
                trades.append({'Date': date, 'Action': 'BUY_3', 'Price': fill_px, 'Qty': qty})

        # EXIT logic (priority: take-profit partial, then full exits)
        if pos.qty > 0:
            # Take-profit: sell half if threshold reached (only once)
            if take_half and not any(t['Action']=='TP_HALF' for t in trades if t['Date']==date):
                sell_qty = pos.qty * 0.5
                fill_px = _apply_tc(close, p.commission_bps, p.slippage_bps, side=-1)
                proceeds = sell_qty * fill_px
                equity += proceeds
                pos.qty -= sell_qty
                # avg_price remains the same for remaining position
                trades.append({'Date': date, 'Action': 'TP_HALF', 'Price': fill_px, 'Qty': sell_qty})

            # Full exit conditions
            if (close < sma * (1 - p.sma_exit_buf)) or (close < pos.avg_price * (1 - p.hard_stop)):
                fill_px = _apply_tc(close, p.commission_bps, p.slippage_bps, side=-1)
                proceeds = pos.qty * fill_px
                equity += proceeds
                trades.append({'Date': date, 'Action': 'EXIT', 'Price': fill_px, 'Qty': pos.qty})
                pos.qty = 0.0
                pos.avg_price = 0.0
                pos.first_entry_price = 0.0
                pos.added_add1 = False
                pos.added_add2 = False

    # Final mark-to-market
    if pos.qty > 0:
        equity_now = equity + pos.qty * df.iloc[-1]['Close']
    else:
        equity_now = equity

    ec = pd.DataFrame(equity_curve).dropna()
    ec['Returns'] = ec['Equity'].pct_change().fillna(0.0)
    # Buy & Hold benchmark
    close = df['Close'].values
    if len(close) > 0:
        bh_returns = np.zeros_like(close, dtype=float)
        bh_returns[1:] = close[1:] / close[0] - 1.0
        ec['BH'] = (1 + pd.Series(np.r_[0, np.diff(close)/close[:-1]], index=ec.index)).cumprod() * (p.initial_capital) 
        # align BH to initial capital for visual comparison
    else:
        ec['BH'] = np.nan

    stats = compute_stats(ec['Equity'].values, p.initial_capital, ec['Returns'].values)
    return ec, stats

def compute_stats(equity_curve: np.ndarray, initial: float, daily_returns: np.ndarray) -> dict:
    total_return = equity_curve[-1] / initial - 1.0
    # CAGR
    n = len(equity_curve)
    if n <= 1:
        cagr = 0.0
    else:
        years = n / 252.0
        cagr = (equity_curve[-1]/initial) ** (1/years) - 1 if years > 0 else 0.0
    # Max Drawdown
    peaks = np.maximum.accumulate(equity_curve)
    drawdowns = equity_curve / peaks - 1.0
    max_dd = drawdowns.min() if len(drawdowns) else 0.0
    # Sharpe (daily, risk-free ~ 0)
    if daily_returns.std() > 0:
        sharpe = daily_returns.mean() / daily_returns.std() * np.sqrt(252)
    else:
        sharpe = 0.0
    return {
        'Total Return': total_return,
        'CAGR': cagr,
        'Max Drawdown': max_dd,
        'Sharpe': sharpe,
        'Last Equity': float(equity_curve[-1]) if len(equity_curve) else initial,
        'Bars': int(n)
    }

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--ticker', type=str, default='TICK')
    ap.add_argument('--csv', type=str, default=None, help='Path to CSV with Date,Open,High,Low,Close,Volume')
    ap.add_argument('--start', type=str, default=None)
    ap.add_argument('--end', type=str, default=None)
    ap.add_argument('--plot', action='store_true')
    ap.add_argument('--save_plot', type=str, default=None)
    # Hyperparameters
    ap.add_argument('--sma_len', type=int, default=120)
    ap.add_argument('--avgvol_len', type=int, default=20)
    ap.add_argument('--lookback_below', type=int, default=10)
    ap.add_argument('--add1', type=float, default=0.03)
    ap.add_argument('--add2', type=float, default=0.06)
    ap.add_argument('--sma_exit_buf', type=float, default=0.05)
    ap.add_argument('--hard_stop', type=float, default=0.10)
    ap.add_argument('--tp', type=float, default=0.30)
    ap.add_argument('--comm_bps', type=float, default=5.0)
    ap.add_argument('--slip_bps', type=float, default=5.0)
    ap.add_argument('--initial', type=float, default=100000.0)

    args = ap.parse_args()

    p = Params(
        sma_len=args.sma_len,
        avgvol_len=args.avgvol_len,
        lookback_below=args.lookback_below,
        add1_level=args.add1,
        add2_level=args.add2,
        sma_exit_buf=args.sma_exit_buf,
        hard_stop=args.hard_stop,
        take_profit=args.tp,
        commission_bps=args.comm_bps,
        slippage_bps=args.slip_bps,
        initial_capital=args.initial,
    )

    if args.csv:
        df = load_data_from_csv(args.csv)
    else:
        df = load_data_yf(args.ticker, args.start, args.end)

    ec, stats = backtest(df, p, ticker=args.ticker)
    print("=== Summary ===")
    for k,v in stats.items():
        if isinstance(v, float):
            print(f"{k:>15}: {v:.4f}")
        else:
            print(f"{k:>15}: {v}")

    if args.plot or args.save_plot:
        import matplotlib.pyplot as plt
        plt.figure()
        plt.plot(ec['Date'], ec['Equity'], label='Strategy')
        plt.plot(ec['Date'], ec['BH'], label='Buy & Hold')
        plt.legend()
        plt.title(f"Equity Curve: {args.ticker}")
        plt.xlabel("Date")
        plt.ylabel("Equity")
        if args.save_plot:
            plt.savefig(args.save_plot, dpi=150, bbox_inches='tight')
        if args.plot:
            plt.show()

if __name__ == "__main__":
    main()

A 股的数据获取和美股不一样,yfinance 在国内基本没法直接拿。主流有两条路:


1. 免费数据源(Python 库)

🔹 Tushare

  • 特点:A 股最常用的数据接口,日线、分钟线、财报都有。
  • 限制:需要申请 token(免费用户每天大概 200 次调用,够做日线回测)。
  • 安装 & 用法
pip install tushare
import tushare as ts
import pandas as pd

# 用你的 token 初始化
ts.set_token("your_token_here")
pro = ts.pro_api()

# 拉取日线数据
df = pro.daily(ts_code='600519.SH', start_date='20150101', end_date='20250901')
df = df.rename(columns={
    'trade_date':'Date', 'open':'Open', 'high':'High', 
    'low':'Low', 'close':'Close', 'vol':'Volume'
})
df['Date'] = pd.to_datetime(df['Date'])
df = df.sort_values('Date')
df.to_csv("600519.csv", index=False)

然后就能直接喂给我给你的回测脚本 ma_breakout_backtest.py


🔹 AkShare

  • 特点:完全免费,不用注册,数据接口多。
  • 安装 & 用法
pip install akshare
import akshare as ak

# 获取贵州茅台的日线
df = ak.stock_zh_a_daily(symbol="sh600519", adjust="qfq")
df = df.reset_index()
df = df.rename(columns={
    'date':'Date','open':'Open','high':'High',
    'low':'Low','close':'Close','volume':'Volume'
})
df.to_csv("600519.csv", index=False)

2. 本地化使用

获取到 csv 以后,你就能这样跑:

python ma_breakout_backtest.py --csv 600519.csv --ticker 600519 --plot

3. 前瞻性建议

  • 分钟线回测:Tushare/AkShare 都有分钟级别数据,你可以把策略下探到更高频。
  • 多标的批量测试:把一篮子 A 股股票下载下来,统一丢进脚本,测试组合收益。
  • 财报/因子结合:A 股对政策、财务因子敏感,可以在买点前加个“基本面过滤”。

要不要我帮你改一版 支持直接调用 Tushare 或 AkShare 的 backtest 脚本,这样你连 CSV 都不用存,每次调用就能自动取数据跑回测?

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐