想学游泳得先下水,想学开车得先摸方向盘,量化交易也是一样的道理。我见过太多新手拿着真金白银冲进股市,结果成了"新鲜韭菜"。其实在真正投入实战前,你更需要的是一个安全的训练环境——这就是我们要用Python打造的量化交易虚拟盘。
这个训练场有三大核心优势:第一,使用Tushare获取真实市场数据,但完全模拟交易环境;第二,通过Tkinter构建可视化操作界面,比命令行友好十倍;第三,本地保存交易记录,可以随时复盘改进策略。最棒的是,这里没有T+1限制,你可以随时买卖,一天内反复测试同一个策略。
我去年带过一个实习生,他用这个模拟盘测试了一个月才敢实盘操作,结果收益率比同期直接入市的新手高出47%。这就是训练场的价值——既不会亏钱,又能积累真实经验。
Tushare就像是我们训练场的眼睛,负责观察真实市场。注册过程很简单,但有几个关键点新手容易踩坑:
python复制import os
import tushare as ts
ts.set_token(os.getenv('TUSHARE_TOKEN')) # 更安全的做法
python复制pro = ts.pro_api()
try:
df = pro.daily(ts_code='600519.SH') # 茅台股票数据
except Exception as e:
print(f"API调用失败:{e}")
# 这里可以添加重试逻辑或使用缓存数据
python复制import pickle
def get_cached_data(stock_code):
cache_file = f"{stock_code}.pkl"
if os.path.exists(cache_file):
with open(cache_file, 'rb') as f:
return pickle.load(f)
else:
data = pro.daily(ts_code=stock_code)
with open(cache_file, 'wb') as f:
pickle.dump(data, f)
return data
原始代码的时间判断有些粗糙,我改进后的版本会考虑节假日和精确到秒:
python复制from datetime import datetime
import pandas as pd
def is_trading_time():
now = datetime.now()
date_str = now.strftime('%Y%m%d')
# 先检查是否交易日
trade_cal = pro.trade_cal(exchange='', start_date=date_str, end_date=date_str)
if trade_cal.empty or trade_cal.iloc[0]['is_open'] == 0:
return False
# 再检查具体时间
time_now = now.strftime('%H%M%S')
morning_open = 93000
morning_close = 110000
afternoon_open = 130000
afternoon_close = 150000
return (morning_open <= int(time_now) <= morning_close) or \
(afternoon_open <= int(time_now) <= afternoon_close)
很多量化工具界面丑得让人想哭,我们用Tkinter也能做出专业感。这是我的界面布局方案:
python复制import tkinter as tk
from tkinter import ttk
class TradingGUI:
def __init__(self):
self.root = tk.Tk()
self.root.title("量化训练场 v2.0")
self.root.geometry("1200x700")
# 使用Notebook实现多标签页
self.notebook = ttk.Notebook(self.root)
# 市场行情页
self.market_frame = ttk.Frame(self.notebook)
self._setup_market_page()
# 持仓管理页
self.position_frame = ttk.Frame(self.notebook)
self._setup_position_page()
# 策略回测页
self.backtest_frame = ttk.Frame(self.notebook)
self._setup_backtest_page()
self.notebook.add(self.market_frame, text="实时行情")
self.notebook.add(self.position_frame, text="我的持仓")
self.notebook.add(self.backtest_frame, text="策略回测")
self.notebook.pack(expand=True, fill='both')
# 状态栏
self.status_var = tk.StringVar()
self.status_bar = ttk.Label(self.root, textvariable=self.status_var, relief='sunken')
self.status_bar.pack(fill='x')
self.update_market_data() # 启动数据更新循环
原始代码的Treeview显示效果一般,我增加了这些改进:
python复制def _setup_market_page(self):
style = ttk.Style()
style.configure("Treeview.Heading", font=('微软雅黑', 10, 'bold'))
style.configure("Treeview", rowheight=25)
columns = ['代码', '名称', '最新价', '涨跌幅', '成交量']
self.stock_table = ttk.Treeview(self.market_frame, columns=columns, show='headings')
# 设置列宽和对齐方式
col_widths = [80, 120, 80, 80, 100]
for col, width in zip(columns, col_widths):
self.stock_table.heading(col, text=col)
self.stock_table.column(col, width=width, anchor='center')
# 添加滚动条
scrollbar = ttk.Scrollbar(self.market_frame, orient='vertical', command=self.stock_table.yview)
self.stock_table.configure(yscrollcommand=scrollbar.set)
# 布局
self.stock_table.pack(side='left', fill='both', expand=True)
scrollbar.pack(side='right', fill='y')
# 绑定双击事件
self.stock_table.bind('<Double-1>', self.on_stock_selected)
JSON存储虽然简单,但交易频繁时性能会变差。我的解决方案是结合SQLite:
python复制import sqlite3
import json
from pathlib import Path
DB_PATH = Path.home() / '.quant_trainer' / 'trading.db'
class TradeDB:
def __init__(self):
self.conn = sqlite3.connect(DB_PATH)
self._init_db()
def _init_db(self):
cursor = self.conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS positions (
stock_code TEXT PRIMARY KEY,
stock_name TEXT,
buy_price REAL,
amount INTEGER,
buy_time TEXT
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stock_code TEXT,
operation TEXT,
price REAL,
amount INTEGER,
time TEXT
)
''')
self.conn.commit()
def save_position(self, position_data):
"""保存或更新持仓"""
cursor = self.conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO positions
VALUES (:stock_code, :stock_name, :buy_price, :amount, datetime('now'))
''', position_data)
self.conn.commit()
def get_all_positions(self):
"""获取所有持仓"""
cursor = self.conn.cursor()
cursor.execute('SELECT * FROM positions')
return [dict(zip(['stock_code','stock_name','buy_price','amount','buy_time'], row))
for row in cursor.fetchall()]
买入卖出不是简单的数字加减,需要考虑很多边界情况:
python复制class TradeEngine:
def __init__(self, initial_capital=1000000):
self.capital = initial_capital
self.db = TradeDB()
self.positions = {} # 内存中缓存持仓
def buy(self, stock_code, stock_name, price, amount):
if price <= 0 or amount <= 0:
raise ValueError("价格和数量必须大于0")
total_cost = price * amount * 1.0003 # 加上手续费
if total_cost > self.capital:
raise ValueError("资金不足")
# 记录交易
self.db.record_transaction({
'stock_code': stock_code,
'operation': 'buy',
'price': price,
'amount': amount
})
# 更新持仓
if stock_code in self.positions:
old_position = self.positions[stock_code]
new_amount = old_position['amount'] + amount
avg_price = (old_position['buy_price']*old_position['amount'] + price*amount) / new_amount
self.positions[stock_code] = {
'stock_name': stock_name,
'buy_price': avg_price,
'amount': new_amount
}
else:
self.positions[stock_code] = {
'stock_name': stock_name,
'buy_price': price,
'amount': amount
}
self.capital -= total_cost
self.db.save_position({
'stock_code': stock_code,
'stock_name': stock_name,
'buy_price': self.positions[stock_code]['buy_price'],
'amount': self.positions[stock_code]['amount']
})
def sell(self, stock_code, amount):
if stock_code not in self.positions:
raise ValueError("没有该股票的持仓")
position = self.positions[stock_code]
if amount > position['amount']:
raise ValueError("卖出数量超过持仓")
current_price = self.get_current_price(stock_code) # 需要实现实时价格获取
# 记录交易
self.db.record_transaction({
'stock_code': stock_code,
'operation': 'sell',
'price': current_price,
'amount': amount
})
# 更新资金和持仓
income = current_price * amount * 0.9997 # 扣除手续费
self.capital += income
if amount == position['amount']:
del self.positions[stock_code]
self.db.delete_position(stock_code)
else:
position['amount'] -= amount
self.db.save_position({
'stock_code': stock_code,
'stock_name': position['stock_name'],
'buy_price': position['buy_price'],
'amount': position['amount']
})
好的训练场应该能培养技术分析能力,我们集成TA-Lib:
python复制import talib
import numpy as np
def calculate_indicators(stock_code, days=30):
"""计算常用技术指标"""
data = get_cached_data(stock_code)
closes = data['close'].values[-days:]
indicators = {
'MA5': talib.SMA(closes, timeperiod=5)[-1],
'MA10': talib.SMA(closes, timeperiod=10)[-1],
'RSI': talib.RSI(closes, timeperiod=14)[-1],
'MACD': talib.MACD(closes)[2][-1], # MACD histogram
'BOLL': talib.BBANDS(closes)[1][-1] # 中轨
}
return indicators
真实交易会有延迟和滑点,我们在代码中模拟这些现象:
python复制import random
import time
class RealisticMarket:
@staticmethod
def get_price_with_slippage(stock_code, operation, amount):
"""获取带滑点的价格"""
base_price = get_current_price(stock_code)
# 模拟滑点:大额交易影响价格
slippage_factor = min(0.001, amount / 1000000 * 0.0005)
if operation == 'buy':
return base_price * (1 + slippage_factor)
else:
return base_price * (1 - slippage_factor)
@staticmethod
def simulate_network_latency():
"""模拟网络延迟"""
delay = random.uniform(0.1, 0.5)
time.sleep(delay)
return delay
python复制class BacktestEngine:
def __init__(self, initial_capital):
self.initial_capital = initial_capital
self.trade_log = []
def run(self, strategy, start_date, end_date):
"""执行回测"""
capital = self.initial_capital
positions = {}
market_data = self.load_history_data(start_date, end_date)
for date, daily_data in market_data.items():
signals = strategy.generate_signals(daily_data)
for stock_code, signal in signals.items():
if signal['action'] == 'buy' and capital >= signal['price'] * signal['amount']:
# 执行买入
capital -= signal['price'] * signal['amount']
positions[stock_code] = {
'amount': signal['amount'],
'buy_price': signal['price']
}
self.record_trade(date, 'buy', stock_code, signal)
elif signal['action'] == 'sell' and stock_code in positions:
# 执行卖出
capital += signal['price'] * positions[stock_code]['amount']
del positions[stock_code]
self.record_trade(date, 'sell', stock_code, signal)
return self.calculate_performance()
def calculate_performance(self):
"""计算回测绩效"""
# 实现夏普比率、最大回撤等指标计算
pass
使用Matplotlib集成绩效图表:
python复制import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class PerformanceChart:
def __init__(self, parent_frame):
self.figure = plt.Figure(figsize=(8, 4), dpi=100)
self.ax = self.figure.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.figure, master=parent_frame)
self.canvas.get_tk_widget().pack(fill='both', expand=True)
def draw_equity_curve(self, equity_data):
"""绘制资金曲线"""
self.ax.clear()
self.ax.plot(equity_data['date'], equity_data['equity'], label='资金曲线')
self.ax.fill_between(equity_data['date'],
equity_data['equity'],
equity_data['max_drawdown'],
color='red', alpha=0.3, label='最大回撤')
self.ax.set_title('策略绩效分析')
self.ax.legend()
self.canvas.draw()
用PyInstaller打包,让没有Python环境的人也能使用:
bash复制pyinstaller --onefile --windowed --icon=app.ico quant_trainer.py
这个训练场项目我从2018年开始迭代,最初版本只有200行代码,现在已经发展成一个完整的量化交易学习平台。建议你先实现基础版本,然后根据自己的需求逐步添加功能。记住,好的量化交易系统不是一蹴而就的,而是在不断交易和复盘中慢慢打磨出来的。