尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于离线数据的股票量化回测系统构建指南:从数据获取到模拟交易

基于离线数据的股票量化回测系统构建指南:从数据获取到模拟交易 简介这是一套面向编程初学者与金融兴趣者的模拟炒股软件开发学习资源聚焦股票交易逻辑实现与离线数据分析实践帮助用户理解K线图展示、买卖模拟、技术指标计算等核心功能。压缩包共82个文件以55个ASP服务端脚本为主干支撑用户登录、账户管理、行情查看、交易操作及后台管理等完整业务流程辅以13个TXT说明文档、5个HTML前端页面、4个HTM文件及CSS样式表构成典型的B/S架构股票模拟系统雏形整体大小仅711KB轻量易部署。已有224人下载学习适合在本地IIS环境运行调试可直接观察用户注册、资金划转、股票持仓、K线加载依赖外部数据导入等模块交互逻辑并通过ADMIN目录下的管理ASP文件深入理解权限控制与数据维护机制。1. 项目缘起从“离线数据”到“模拟交易”的完整闭环最近在整理硬盘翻出来一个尘封已久的压缩包文件名是monichaogu.zip_K._模拟炒股代码_股票_股票离线数据_股票软件源码。看到这个名字估计很多做量化或者对股票数据感兴趣的朋友都会会心一笑。这几乎是一个典型的个人开发者或学生早期项目的“考古现场”一个包含了数据、代码、甚至可能是简陋界面的“大杂烩”。它背后反映的是一个从数据获取、处理、分析到最终模拟交易执行的完整需求链。很多人入门量化或股票分析第一步往往不是去研究高深的算法而是先想办法把数据“搞到手”然后写几行代码看看能不能跑起来最后再尝试构建一个能模拟买卖的“玩具系统”。这个压缩包的名字恰好精准地概括了这三个核心环节股票离线数据、模拟炒股代码、股票软件源码。我拆解并重构了这个项目发现其核心价值不在于代码本身有多精妙而在于它提供了一个非常接地气的、从零到一的实践范本。它避开了云端API的复杂性和网络依赖直接从最原始的离线CSV或TXT文件开始教你如何组织数据、计算指标、制定策略并模拟交易。这对于想理解量化交易底层逻辑但又苦于没有稳定数据源或担心网络延迟的新手来说是一条非常实用的学习路径。今天我就基于这个“考古”项目结合当前Python生态中最主流的工具链重新梳理出一套清晰、可复现的本地化股票分析及模拟交易系统构建指南。我们将完全使用离线数据一步步实现数据管理、策略回测和模拟交易让你彻底掌握这个闭环中的每一个技术要点。2. 基石构建离线股票数据的获取、清洗与本地化管理一切分析的基础是数据。所谓“股票离线数据”通常指已经下载到本地的、包含股票历史行情如日K线的结构化文件常见格式有CSV、TXT或数据库文件。我们的第一步就是建立一套高效、规范的本地数据管理体系。2.1 数据来源与获取策略虽然项目强调“离线”但数据总得有个最初的来源。对于学习和回测我们通常需要几年的历史日线数据。有几种常见的获取方式第三方数据平台导出这是最直接的方式。你可以使用一些财经网站如东方财富、新浪财经或专业数据终端需注意数据使用权限通过其数据导出功能将特定股票的历史行情数据以CSV格式下载到本地。通常包含日期、开盘价、最高价、最低价、收盘价、成交量等字段。使用开源库批量抓取用于初始构建像akshare、yfinance(针对美股) 这样的Python库可以方便地获取历史数据。关键在于我们只需在项目初始化时运行一次抓取脚本将数据持久化到本地后续所有分析都基于这份本地副本从而实现真正的“离线”。这避免了每次运行代码都去请求网络也解决了某些API有调用频率限制的问题。这里给出一个使用akshare初始化本地数据仓库的示例脚本import akshare as ak import pandas as pd import os from pathlib import Path def fetch_and_save_stock_data(stock_code, start_date20180101, end_date20231231): 获取单只股票历史数据并保存到本地CSV :param stock_code: 股票代码如 sh600000akshare格式 :param start_date: 开始日期 :param end_date: 结束日期 # 创建数据存储目录 data_dir Path(./stock_data/ashare/daily) data_dir.mkdir(parentsTrue, exist_okTrue) try: # 使用 ak.stock_zh_a_hist 获取A股日线数据 df ak.stock_zh_a_hist(symbolstock_code, perioddaily, start_datestart_date, end_dateend_date, adjustqfq) # 这里使用前复权保证价格连续性 if df.empty: print(f未获取到数据: {stock_code}) return # 标准化列名不同数据源列名可能不同 df.rename(columns{ 日期: date, 开盘: open, 最高: high, 最低: low, 收盘: close, 成交量: volume, 成交额: amount, 振幅: amplitude, 涨跌幅: pct_chg, 涨跌额: change, 换手率: turnover }, inplaceTrue) # 确保日期列为datetime类型并设为索引便于后续resample等操作 df[date] pd.to_datetime(df[date]) df.set_index(date, inplaceTrue) # 保存到CSV文件名如 sh600000.csv file_path data_dir / f{stock_code}.csv df.to_csv(file_path) print(f数据已保存至: {file_path}) except Exception as e: print(f获取 {stock_code} 数据时出错: {e}) # 示例获取几只股票的数据 stock_list [sh600000, sz000001, sz300750] for code in stock_list: fetch_and_save_stock_data(code)注意数据获取涉及网络请求和第三方数据源务必遵守相关网站的使用条款不要进行高频、大量的请求以免对对方服务器造成压力或被封禁IP。本示例仅用于演示数据本地化的流程。2.2 本地数据仓库的架构设计一个良好的本地数据仓库能极大提升后续分析和回测的效率。我建议采用如下目录结构stock_data/ ├── ashar/ # A股数据 │ ├── daily/ # 日线数据 (CSV格式每只股票一个文件) │ ├── 5min/ # 5分钟线数据可选 │ └── basics/ # 基本面数据如财务报表可选 ├── index/ # 指数数据如上证指数 │ └── daily/ ├── futures/ # 期货数据可选 │ └── daily/ └── metadata.pkl # 元数据文件记录所有已下载股票的代码、名称、更新日期等为什么这样设计按市场和品种分离清晰明了避免混淆。按周期分离不同分析策略需要不同时间粒度的数据。元数据文件这是关键。当你有成百上千只股票的数据时一个集中的元数据文件可以快速告诉你本地有哪些数据、数据更新到哪一天方便做增量更新和批量操作。2.3 数据清洗与一致性检查从网络获取的数据难免有脏数据或格式问题加载本地CSV后必须进行清洗import pandas as pd def load_and_clean_data(file_path): 加载并清洗单个股票数据文件 df pd.read_csv(file_path, index_coldate, parse_datesTrue) # 1. 处理缺失值对于价格数据通常用前向填充成交量可以填0 # 但更常见的做法是直接删除缺失行因为交易日数据本应连续 df.dropna(subset[open, high, low, close], inplaceTrue) # 2. 检查并处理价格异常值如收盘价为0或负值 # 这可能是数据源错误需要根据情况处理删除或标记 abnormal df[(df[close] 0) | (df[volume] 0)] if not abnormal.empty: print(f发现异常数据行将删除: {len(abnormal)} 行) df.drop(abnormal.index, inplaceTrue) # 3. 确保数据按日期升序排列 df.sort_index(inplaceTrue) # 4. 去重同一日期可能出现多次 df df[~df.index.duplicated(keepfirst)] # 5. 计算并添加常用衍生列也可在策略中实时计算但预处理能提升回测速度 df[returns] df[close].pct_change() # 日收益率 df[ma5] df[close].rolling(window5).mean() # 5日均线 df[ma20] df[close].rolling(window20).mean() # 20日均线 # ... 可以计算更多指标如ATR, RSI, MACD等 return df实操心得数据清洗是量化分析的“隐形工程”做得不扎实后面所有策略结果都可能失真。务必对每一只股票的数据进行上述基本检查并形成清洗日志。对于A股要特别注意除权除息日的数据使用前复权adjust“qfq”数据能保证价格序列的连续性是回测的标准选择。3. 核心引擎模拟交易与回测系统的设计与实现有了干净的数据接下来就是构建系统的核心——模拟交易引擎。这个引擎需要模拟真实交易的几个关键要素资金管理、订单执行、持仓跟踪、绩效统计。我们将完全基于Pandas和NumPy来实现一个轻量级但功能完整的回测框架。3.1 定义核心数据结构账户与订单首先我们需要定义交易发生时需要记录的信息。from dataclasses import dataclass from datetime import datetime from typing import Optional dataclass class Order: 订单类记录一次买卖操作 order_id: int symbol: str # 股票代码 direction: str # ‘buy’ 或 ‘sell’ order_type: str # ‘market’ (市价单按下一个Bar的open价成交) quantity: int # 数量股 filled_price: float # 成交价格 filled_time: datetime # 成交时间 commission: float 0.0 # 佣金手续费 dataclass class Position: 持仓类记录当前持有某只股票的状态 symbol: str quantity: int 0 # 持有数量正数为多仓负数为空仓本例只做多 avg_cost: float 0.0 # 平均成本 market_value: float 0.0 # 当前市值quantity * current_price class BacktestAccount: 回测账户类管理资金、持仓和订单历史 def __init__(self, initial_cash100000.0): self.initial_cash initial_cash self.current_cash initial_cash self.positions {} # symbol - Position self.orders [] # 所有已成交订单列表 self.order_id_counter 1 self.equity_curve [] # 每日总权益记录 [(date, total_equity), ...] property def total_equity(self, current_prices: dict None): 计算当前总权益现金 所有持仓市值 total self.current_cash if current_prices: for sym, pos in self.positions.items(): if sym in current_prices: pos.market_value pos.quantity * current_prices[sym] total pos.market_value else: # 如果没有提供当前价则用成本价近似不准确仅备用 total pos.quantity * pos.avg_cost else: for pos in self.positions.values(): total pos.quantity * pos.avg_cost return total3.2 实现订单执行与资金结算逻辑这是引擎最复杂的部分需要处理买卖信号、检查资金是否充足、更新持仓和现金。def place_market_order(self, symbol, direction, quantity, fill_price, fill_time, commission_rate0.0003): 下达并执行一个市价订单。 :param commission_rate: 佣金率默认万分之三0.03% if quantity 0: raise ValueError(订单数量必须大于0) # 计算佣金买卖都收最低5元是A股规则此处简化按比例计算 commission fill_price * quantity * commission_rate # 模拟最低佣金限制 commission max(commission, 5.0) if commission_rate 0 else 0 order_value fill_price * quantity if direction buy: # 检查资金是否足够金额佣金 required_cash order_value commission if self.current_cash required_cash: print(f[{fill_time}] 资金不足无法买入 {symbol}。所需: {required_cash:.2f}, 可用: {self.current_cash:.2f}) return False # 执行买入 self.current_cash - required_cash if symbol not in self.positions: self.positions[symbol] Position(symbol) pos self.positions[symbol] # 计算新的平均成本 total_cost pos.avg_cost * pos.quantity order_value pos.quantity quantity pos.avg_cost total_cost / pos.quantity if pos.quantity 0 else 0 elif direction sell: if symbol not in self.positions or self.positions[symbol].quantity quantity: print(f[{fill_time}] 持仓不足无法卖出 {symbol}。) return False # 执行卖出 pos self.positions[symbol] self.current_cash order_value - commission pos.quantity - quantity if pos.quantity 0: pos.avg_cost 0.0 # 可以选择删除零持仓的条目 # del self.positions[symbol] else: # 卖出不影响剩余持仓的平均成本 pass else: raise ValueError(direction 必须是 buy 或 sell) # 记录订单 order Order(order_idself.order_id_counter, symbolsymbol, directiondirection, order_typemarket, quantityquantity, filled_pricefill_price, filled_timefill_time, commissioncommission) self.orders.append(order) self.order_id_counter 1 return True为什么这样设计订单逻辑市价单简化回测中普遍使用下一个Bar的开盘价open作为成交价这避免了盘中复杂的价格撮合是行业通用做法。佣金模型这里实现了按比例收取且有最低限额的佣金模型更贴近A股实际。你也可以扩展印花税、过户费等。平均成本法这是股票持仓成本核算最常用的方法简单且符合会计原则。3.3 构建策略回测循环现在我们将数据、账户和策略串联起来。策略就是一个函数它接收当前的市场数据、账户状态并输出交易信号。def simple_ma_crossover_strategy(data, account, current_date, lookback_days20): 一个简单的双均线交叉策略示例。 当短期均线上穿长期均线时买入下穿时卖出。 signals [] # 返回信号列表每个信号是 (symbol, direction, quantity) # 假设data是一个包含多只股票DataFrame的字典索引是日期 for symbol, df in data.items(): # 确保有足够的历史数据 if current_date not in df.index or len(df.loc[:current_date]) lookback_days 1: continue # 获取到当前日期为止的数据切片 hist df.loc[:current_date] current_bar hist.loc[current_date] prev_bar hist.iloc[-2] if len(hist) 1 else None # 计算指标这里应在数据清洗时预计算此处为演示 short_ma hist[close].rolling(window5).mean().iloc[-1] long_ma hist[close].rolling(window20).mean().iloc[-1] prev_short_ma hist[close].rolling(window5).mean().iloc[-2] if prev_bar is not None else None prev_long_ma hist[close].rolling(window20).mean().iloc[-2] if prev_bar is not None else None # 生成信号 position account.positions.get(symbol, Position(symbol)) if prev_short_ma and prev_long_ma: # 金叉短期均线上穿长期均线且当前无持仓 if prev_short_ma prev_long_ma and short_ma long_ma and position.quantity 0: # 计算买入数量例如使用固定资金比例 cash_to_use account.current_cash * 0.95 # 使用95%的可用现金 price current_bar[open] # 假设以次日开盘价买入 quantity int(cash_to_use / price / 100) * 100 # A股按手100股交易 if quantity 0: signals.append((symbol, buy, quantity)) # 死叉短期均线下穿长期均线且当前有持仓 elif prev_short_ma prev_long_ma and short_ma long_ma and position.quantity 0: signals.append((symbol, sell, position.quantity)) # 全仓卖出 return signals def run_backtest(data_dict, initial_cash100000.0, start_dateNone, end_dateNone): 运行回测的主函数。 :param data_dict: 字典{股票代码: 对应的清洗后的DataFrame} # 确定回测日期范围所有股票数据的交集 common_dates None for df in data_dict.values(): if common_dates is None: common_dates set(df.index) else: common_dates common_dates.intersection(set(df.index)) common_dates sorted(list(common_dates)) if start_date: common_dates [d for d in common_dates if d pd.Timestamp(start_date)] if end_date: common_dates [d for d in common_dates if d pd.Timestamp(end_date)] account BacktestAccount(initial_cashinitial_cash) for current_date in common_dates: # 在交易日开始时先记录前一日收盘后的权益用于画曲线 # 这里简化处理记录当日开盘前的权益需用前一日收盘价计算持仓市值 if account.equity_curve: prev_date account.equity_curve[-1][0] # 获取前一日收盘价计算市值 prev_prices {} for sym, df in data_dict.items(): if prev_date in df.index: prev_prices[sym] df.loc[prev_date, close] account.equity_curve.append((current_date, account.total_equity(prev_prices))) else: # 第一天记录初始权益 account.equity_curve.append((current_date, initial_cash)) # 运行策略获取交易信号 signals simple_ma_crossover_strategy(data_dict, account, current_date) # 执行信号假设以当日开盘价成交 for symbol, direction, quantity in signals: if symbol in data_dict and current_date in data_dict[symbol].index: fill_price data_dict[symbol].loc[current_date, open] fill_time current_date account.place_market_order(symbol, direction, quantity, fill_price, fill_time) # 回测结束计算最终权益使用最后一天的收盘价 final_prices {} last_date common_dates[-1] for sym, df in data_dict.items(): if last_date in df.index: final_prices[sym] df.loc[last_date, close] final_equity account.total_equity(final_prices) account.equity_curve.append((last_date, final_equity)) # 记录最终权益点 return account踩坑提醒回测中一个经典的陷阱是“未来函数”。在上述策略中我们严格使用hist df.loc[:current_date]来获取截至当前日期的数据计算指标。这意味着在current_date这一天我们只能看到这一天及之前的数据绝对不能用current_date之后的数据。这是回测系统可信度的生命线。4. 绩效评估与可视化从数字到洞见回测跑完了账户里有一堆订单和一条权益曲线但这远远不够。我们需要一套科学的评估体系来衡量策略的好坏并直观地展示出来。4.1 关键绩效指标计算仅仅看最终收益率是片面的。我们需要多维度评估策略的风险和收益特征。import numpy as np import pandas as pd def calculate_performance_metrics(account, initial_cash, risk_free_rate0.03): 计算一系列回测绩效指标。 :param risk_free_rate: 年化无风险利率用于计算夏普比率等默认3% # 将权益曲线转为DataFrame equity_df pd.DataFrame(account.equity_curve, columns[date, equity]) equity_df.set_index(date, inplaceTrue) equity_df[returns] equity_df[equity].pct_change().fillna(0) # 1. 基础收益指标 total_return (equity_df[equity].iloc[-1] - initial_cash) / initial_cash annualized_return (1 total_return) ** (252 / len(equity_df)) - 1 # 假设252个交易日 # 2. 风险指标 volatility equity_df[returns].std() * np.sqrt(252) # 年化波动率 # 3. 风险调整后收益指标 excess_returns equity_df[returns] - risk_free_rate / 252 sharpe_ratio excess_returns.mean() / excess_returns.std() * np.sqrt(252) if excess_returns.std() ! 0 else 0 # 最大回撤Max Drawdown - 非常重要 equity_series equity_df[equity] rolling_max equity_series.expanding().max() drawdown (equity_series - rolling_max) / rolling_max max_drawdown drawdown.min() # 计算回撤持续期可选 underwater drawdown 0 underwater_start underwater.ne(underwater.shift()) underwater underwater_end underwater.ne(underwater.shift(-1)) underwater # 这里可以进一步分析每次回撤的起止时间和深度 # 4. 交易相关指标 orders_df pd.DataFrame([(o.symbol, o.direction, o.filled_price, o.quantity, o.commission, o.filled_time) for o in account.orders], columns[symbol, direction, price, quantity, commission, time]) if not orders_df.empty: orders_df.set_index(time, inplaceTrue) win_trades [] loss_trades [] # 简化计算遍历订单匹配买卖对计算单笔盈亏实际应基于持仓标记 # 更严谨的做法是在订单类或持仓类中记录每笔交易的盈亏 total_commission orders_df[commission].sum() # 这里省略复杂的逐笔交易盈亏匹配计算... total_trades len(orders_df) # 胜率、平均盈亏比等需要更精细的交易记录此处略过 metrics { 初始资金: initial_cash, 最终权益: equity_df[equity].iloc[-1], 总收益率: f{total_return:.2%}, 年化收益率: f{annualized_return:.2%}, 年化波动率: f{volatility:.2%}, 夏普比率: f{sharpe_ratio:.2f}, 最大回撤: f{max_drawdown:.2%}, 总交易次数: total_trades if total_trades in locals() else 0, 总手续费: total_commission if total_commission in locals() else 0.0, } return metrics, equity_df, drawdown4.2 使用Matplotlib进行可视化图表能让绩效评估结果一目了然。import matplotlib.pyplot as plt import matplotlib.dates as mdates def plot_backtest_results(equity_df, drawdown, metrics): 绘制回测结果图表 fig, axes plt.subplots(3, 1, figsize(12, 10), sharexTrue) # 1. 资产曲线 ax1 axes[0] ax1.plot(equity_df.index, equity_df[equity], label策略净值, linewidth2, colorblue) ax1.axhline(ymetrics[初始资金], colorgrey, linestyle--, alpha0.7, label初始资金) ax1.set_ylabel(资产净值 (元)) ax1.set_title(策略净值曲线) ax1.legend() ax1.grid(True, alpha0.3) # 2. 回撤曲线 ax2 axes[1] ax2.fill_between(drawdown.index, 0, drawdown.values * 100, colorred, alpha0.3) ax2.plot(drawdown.index, drawdown.values * 100, colordarkred, linewidth1) ax2.set_ylabel(回撤 (%)) ax2.set_title(策略回撤曲线) ax2.grid(True, alpha0.3) # 3. 每日收益率分布直方图 ax3 axes[2] returns equity_df[returns].dropna() * 100 # 转换为百分比 ax3.hist(returns, bins50, edgecolorblack, alpha0.7, colorsteelblue) ax3.axvline(xreturns.mean(), colorred, linestyle--, labelf均值: {returns.mean():.2f}%) ax3.axvline(xreturns.std(), colororange, linestyle--, alpha0.7, labelf标准差: {returns.std():.2f}%) ax3.axvline(x-returns.std(), colororange, linestyle--, alpha0.7) ax3.set_xlabel(日收益率 (%)) ax3.set_ylabel(频次) ax3.set_title(日收益率分布) ax3.legend() ax3.grid(True, alpha0.3) # 格式化x轴日期 for ax in axes: ax.xaxis.set_major_formatter(mdates.DateFormatter(%Y-%m)) ax.xaxis.set_major_locator(mdates.MonthLocator(interval3)) plt.tight_layout() plt.show() # 打印关键指标 print( 回测绩效摘要 ) for key, value in metrics.items(): print(f{key}: {value})经验之谈最大回撤是比收益率更重要的指标。它告诉你策略在最坏的情况下会亏多少钱直接关系到你的心理承受能力和策略的生存能力。一个年化收益20%但最大回撤50%的策略很可能在一次大的回调中被击穿止损线而被迫出局实际根本拿不住。务必把回撤曲线和净值曲线放在一起看。5. 从回测到“软件”构建图形化前端与实战优化原始的monichaogu.zip里可能包含一些简陋的GUI代码比如用Tkinter或PyQt写的界面。对于个人项目一个图形界面能极大提升交互体验。这里我们探讨如何用现代一点的方式例如Plotly Dash或Streamlit快速构建一个Web应用将上述分析模块封装起来。5.1 使用Streamlit快速搭建原型Streamlit是目前构建数据应用最快捷的工具之一几行代码就能把数据分析脚本变成交互式Web应用。# 文件: app.py import streamlit as st import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots import sys sys.path.append(.) from backtest_engine import run_backtest, calculate_performance_metrics # 假设我们的回测代码在 backtest_engine.py st.set_page_config(page_title股票模拟交易回测系统, layoutwide) st.title( 本地股票模拟交易与回测平台) # 侧边栏参数配置 st.sidebar.header(回测参数设置) initial_cash st.sidebar.number_input(初始资金元, min_value10000, value100000, step10000) start_date st.sidebar.date_input(开始日期, pd.to_datetime(2020-01-01)) end_date st.sidebar.date_input(结束日期, pd.to_datetime(2023-12-31)) # 股票选择假设本地数据目录已存在一些CSV文件 import os data_files [f for f in os.listdir(./stock_data/ashare/daily) if f.endswith(.csv)] symbol_options [f.replace(.csv, ) for f in data_files] selected_symbols st.sidebar.multiselect(选择股票代码, symbol_options, defaultsymbol_options[:3]) # 策略选择 strategy_name st.sidebar.selectbox(选择策略, [双均线交叉, 动量突破, 均值回归]) # 运行回测按钮 if st.sidebar.button(开始回测, typeprimary): if not selected_symbols: st.warning(请至少选择一只股票。) else: with st.spinner(正在加载数据并运行回测...): # 1. 加载数据 data_dict {} for sym in selected_symbols: file_path f./stock_data/ashare/daily/{sym}.csv try: df pd.read_csv(file_path, index_coldate, parse_datesTrue) df df.loc[start_date:end_date] # 切片 if not df.empty: data_dict[sym] df except FileNotFoundError: st.error(f未找到股票 {sym} 的数据文件。) if not data_dict: st.error(未能加载任何有效数据请检查数据文件。) else: # 2. 运行回测这里需要根据strategy_name动态选择策略函数 # 为简化我们固定使用之前写的双均线策略 account run_backtest(data_dict, initial_cashinitial_cash, start_datestart_date, end_dateend_date) # 3. 计算绩效 metrics, equity_df, drawdown_series calculate_performance_metrics(account, initial_cash) # 4. 展示结果 col1, col2, col3 st.columns(3) with col1: st.metric(总收益率, metrics[总收益率]) st.metric(年化收益率, metrics[年化收益率]) with col2: st.metric(夏普比率, metrics[夏普比率]) st.metric(最大回撤, metrics[最大回撤]) with col3: st.metric(总交易次数, metrics[总交易次数]) st.metric(总手续费, f{metrics[总手续费]:.2f} 元) # 5. 使用Plotly绘制交互式图表 fig make_subplots(rows2, cols1, shared_xaxesTrue, vertical_spacing0.05, subplot_titles(策略净值曲线, 回撤曲线), row_heights[0.7, 0.3]) # 净值曲线 fig.add_trace(go.Scatter(xequity_df.index, yequity_df[equity], modelines, name净值, linedict(colorroyalblue, width2)), row1, col1) fig.add_hline(yinitial_cash, line_dashdash, line_colorgrey, annotation_text初始资金, row1, col1) # 回撤曲线 fig.add_trace(go.Scatter(xdrawdown_series.index, ydrawdown_series.values * 100, # 转百分比 filltozeroy, modelines, name回撤, linedict(colorred, width1), fillcolorrgba(255,0,0,0.3)), row2, col1) fig.update_layout(height600, showlegendTrue, hovermodex unified) fig.update_xaxes(title_text日期, row2, col1) fig.update_yaxes(title_text净值 (元), row1, col1) fig.update_yaxes(title_text回撤 (%), row2, col1) st.plotly_chart(fig, use_container_widthTrue) # 6. 展示交易记录 st.subheader(交易记录) if account.orders: orders_df pd.DataFrame([(o.filled_time, o.symbol, o.direction, o.quantity, o.filled_price, o.commission) for o in account.orders], columns[时间, 代码, 方向, 数量, 成交价, 手续费]) st.dataframe(orders_df.sort_values(时间, ascendingFalse)) else: st.info(本次回测期间无交易。) # 如果没有运行回测显示一些介绍信息 else: st.info(请在左侧配置参数然后点击【开始回测】。) st.markdown( ### 系统功能说明 1. **数据完全离线**所有分析基于本地CSV文件无需网络连接。 2. **灵活策略回测**支持自定义策略需修改代码。 3. **专业绩效分析**提供夏普比率、最大回撤等关键指标。 4. **交互式可视化**图表可缩放、平移方便细节分析。 )运行这个Streamlit应用只需要在终端执行streamlit run app.py。它提供了一个直观的界面让用户可以点选股票、调整参数、运行回测并即时查看图表和结果这比直接运行脚本友好得多。5.2 策略优化与过拟合陷阱当你有了一个能跑通的回测系统很容易陷入不断修改参数如均线周期、仓位比例以使历史曲线更漂亮的陷阱。这就是过拟合。为了避免生产一个“纸上谈兵”的策略你必须样本外测试将历史数据分为训练集用于开发策略和测试集用于验证策略。绝对不能用测试集的数据来优化参数。交叉验证对于时间序列数据可以使用“前向滚动”交叉验证Walk-Forward Analysis。例如用2000-2010年的数据优化参数在2011年测试然后用2001-2011年优化在2012年测试以此类推。保持策略逻辑简单策略的核心逻辑应基于坚实的经济或行为金融学原理如趋势跟踪、均值回归而不是通过数据挖掘找到的复杂、脆弱的数学关系。考虑交易成本与滑点我们的回测已经包含了佣金但真实的交易还有滑点订单成交价与预期价的偏差在快速波动的市场中尤其明显。可以在回测中引入一个固定的滑点如0.1%来让模型更稳健。一个简单的样本外测试框架思路def walk_forward_analysis(full_data, train_years3, test_years1): 前向滚动分析示例框架。 :param full_data: 完整时间序列的股票数据字典。 :param train_years: 训练集年数。 :param test_years: 测试集年数。 # 获取所有数据的共同日期范围 # ... (代码略) start_date common_dates[0] end_date common_dates[-1] all_results [] current_train_start start_date while True: current_train_end current_train_start pd.DateOffset(yearstrain_years) current_test_start current_train_end current_test_end current_test_start pd.DateOffset(yearstest_years) if current_test_end end_date: break # 1. 在训练集上优化策略参数例如网格搜索最佳均线周期 # train_data 切片数据[current_train_start: current_train_end] # best_params optimize_strategy(train_data) # 2. 使用最优参数在测试集上运行回测 # test_data 切片数据[current_test_start: current_test_end] # account run_backtest(test_data, strategy_with_best_params) # metrics calculate_performance_metrics(account) # all_results.append(metrics) print(f训练期: {current_train_start.date()} 至 {current_train_end.date()}, f测试期: {current_test_start.date()} 至 {current_test_end.date()}) # 滚动窗口 current_train_start current_train_start pd.DateOffset(years1) # 每年滚动一次 # 分析 all_results 中各项指标如年化收益、夏普比率的均值和稳定性 return all_results重构并扩展这个古老的monichaogu.zip项目其意义远不止于让一段旧代码重新运行。它是一次完整的、从数据到决策的工程实践。你不仅学会了如何处理离线数据、构建回测引擎、评估策略绩效还接触到了如何将其产品化为一个可交互的工具并深刻理解了回测中最重要的“过拟合”陷阱。这套方法论和代码骨架完全可以作为你探索更复杂量化策略的起点。记住所有复杂的系统都是从这样一个简单的闭环开始迭代的。本文还有配套的精品资源点击获取
返回列表