最近几年做量化研究的朋友应该都深有体会,很多原本免费的金融数据API要么开始收费,要么限制调用频率。我去年就遇到过这种情况:正在跑的策略突然因为API限制而中断,导致回测结果出现偏差。更糟的是,有些历史数据在收费后,之前的免费版本就无法获取了。
这时候建立一个本地股票数据库就显得尤为重要。想象一下,你有一个私人的数据仓库,随时可以查询任意时间段的K线数据,不用担心网络延迟,不用考虑API调用限制,还能根据自己的需求定制数据格式。这就是我们今天要实现的场景。
Baostock作为目前为数不多的免费金融数据源,提供了丰富的A股历史数据。但直接每次从API获取数据不仅效率低,而且无法保证稳定性。通过将其持久化到MySQL数据库,我们既能享受SQL强大的查询能力,又能建立自己的数据备份。我在实际项目中就经常遇到需要反复查询某只股票特定时间段数据的情况,有了本地数据库后,查询速度直接从秒级降到毫秒级。
建议使用Python 3.7及以上版本,我实测过3.6也能运行,但部分新特性不支持。首先安装必要的库:
bash复制pip install baostock pandas mysql-connector-python
这里有个小坑需要注意:mysql-connector-python和MySQLdb是不同的驱动,前者是纯Python实现,后者需要系统安装MySQL客户端。我推荐使用前者,兼容性更好。
MySQL我推荐使用5.7或8.0版本。安装完成后需要创建一个专用用户和数据库:
sql复制CREATE DATABASE stock_data CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'quant'@'localhost' IDENTIFIED BY 'your_secure_password';
GRANT ALL PRIVILEGES ON stock_data.* TO 'quant'@'localhost';
特别提醒:如果使用MySQL 8.0,可能会遇到认证插件不兼容的问题。解决方法是在my.cnf中添加:
code复制[mysqld]
default_authentication_plugin=mysql_native_password
Baostock的API设计非常简洁,首先需要登录获取会话:
python复制import baostock as bs
lg = bs.login()
if lg.error_code != '0':
print(f"登录失败: {lg.error_msg}")
exit()
获取日K线数据的典型调用示例:
python复制rs = bs.query_history_k_data_plus(
"sh.600000", # 股票代码
"date,code,open,high,low,close,volume,amount", # 字段
start_date='2020-01-01',
end_date='2020-12-31',
frequency='d', # 日线
adjustflag='3' # 不复权
)
这里有个实用技巧:通过调整frequency参数可以获取不同周期的数据:
从API获取的原始数据是字符串格式,需要转换为适当的数据类型:
python复制import pandas as pd
data_list = []
while (rs.error_code == '0') & rs.next():
data_list.append(rs.get_row_data())
df = pd.DataFrame(data_list, columns=rs.fields)
# 类型转换
df['open'] = df['open'].astype(float)
df['volume'] = df['volume'].astype(float)
# 日期格式化
df['date'] = pd.to_datetime(df['date'])
我通常会添加一些衍生字段,比如涨跌幅:
python复制df['pct_change'] = df['close'].pct_change() * 100
一个健壮的K线数据库应该考虑以下因素:
这是我的推荐表结构:
sql复制CREATE TABLE `daily_kline` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`symbol` varchar(20) NOT NULL COMMENT '股票代码',
`trade_date` date NOT NULL COMMENT '交易日期',
`open` decimal(12,4) DEFAULT NULL COMMENT '开盘价',
`high` decimal(12,4) DEFAULT NULL COMMENT '最高价',
`low` decimal(12,4) DEFAULT NULL COMMENT '最低价',
`close` decimal(12,4) DEFAULT NULL COMMENT '收盘价',
`volume` bigint(20) DEFAULT NULL COMMENT '成交量(股)',
`amount` decimal(20,4) DEFAULT NULL COMMENT '成交额(元)',
`adjust_flag` tinyint(4) DEFAULT '3' COMMENT '复权状态',
`turnover` decimal(10,4) DEFAULT NULL COMMENT '换手率',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_symbol_date` (`symbol`,`trade_date`),
KEY `idx_date` (`trade_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='日K线数据';
直接使用INSERT语句逐条插入效率极低。我推荐三种高效方法:
python复制from mysql.connector import Error
def bulk_insert(conn, df, table):
try:
cursor = conn.cursor()
cols = ",".join([str(i) for i in df.columns.tolist()])
vals = ",".join(["%s"] * len(df.columns))
sql = f"INSERT INTO {table} ({cols}) VALUES ({vals})"
cursor.executemany(sql, df.values.tolist())
conn.commit()
except Error as e:
print(f"Error: {e}")
conn.rollback()
python复制df.to_csv('temp.csv', index=False)
cursor.execute("""
LOAD DATA LOCAL INFILE 'temp.csv'
INTO TABLE daily_kline
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
""")
python复制from sqlalchemy import create_engine
engine = create_engine('mysql+mysqlconnector://user:password@localhost/stock_data')
df.to_sql('daily_kline', con=engine, if_exists='append', index=False)
一个好的量化数据系统应该分为以下几个模块:
code复制stock_data_system/
├── config/ # 配置文件
├── core/ # 核心逻辑
│ ├── downloader.py # 数据下载
│ ├── storage.py # 数据存储
│ └── models.py # 数据模型
├── utils/ # 工具函数
└── main.py # 主程序
网络请求不可避免会遇到失败,必须实现健壮的重试机制:
python复制from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def safe_query_history_k_data(code, start_date, end_date):
rs = bs.query_history_k_data_plus(
code,
"date,code,open,high,low,close,volume,amount",
start_date=start_date,
end_date=end_date,
frequency='d',
adjustflag='3'
)
if rs.error_code != '0':
raise Exception(f"Query failed: {rs.error_msg}")
return rs
全量更新数据既耗时又浪费资源,我采用的增量更新逻辑:
python复制def get_last_trade_date(conn, symbol):
cursor = conn.cursor()
cursor.execute(f"""
SELECT MAX(trade_date)
FROM daily_kline
WHERE symbol='{symbol}'
""")
result = cursor.fetchone()
return result[0] if result[0] else '2005-01-01' # 默认从2005年开始
然后只需要查询最后日期之后的数据:
python复制last_date = get_last_trade_date(conn, "sh.600000")
rs = safe_query_history_k_data("sh.600000", last_date, datetime.now().strftime('%Y-%m-%d'))
除了主键索引外,建议添加:
sql复制ALTER TABLE daily_kline ADD INDEX idx_symbol (symbol);
ALTER TABLE daily_kline ADD INDEX idx_date (trade_date);
ALTER TABLE daily_kline ADD INDEX idx_close (close);
低效查询:
python复制# 每次查询都需要全表扫描
for code in stock_list:
cursor.execute(f"SELECT * FROM daily_kline WHERE symbol='{code}'")
优化后的查询:
python复制# 一次获取所有数据
cursor.execute("""
SELECT symbol, trade_date, close
FROM daily_kline
WHERE symbol IN (%s)
ORDER BY symbol, trade_date
""" % ",".join(["%s"]*len(stock_list)), stock_list)
当数据量超过千万级时,可以考虑按股票代码首字母或日期范围进行分区:
sql复制CREATE TABLE partitioned_kline (
-- 字段同上
) PARTITION BY LIST COLUMNS(symbol) (
PARTITION pA VALUES IN ('sh.600000','sh.600001'),
PARTITION pB VALUES IN ('sh.600004','sh.600005'),
-- 其他分区...
);
有了本地数据库后,计算5日、20日均线变得非常简单:
python复制def calculate_ma(symbol, start_date, end_date):
query = f"""
SELECT trade_date, close,
AVG(close) OVER (ORDER BY trade_date ROWS 4 PRECEDING) AS ma5,
AVG(close) OVER (ORDER BY trade_date ROWS 19 PRECEDING) AS ma20
FROM daily_kline
WHERE symbol='{symbol}'
AND trade_date BETWEEN '{start_date}' AND '{end_date}'
"""
df = pd.read_sql(query, conn)
return df
计算两只股票的相关性:
python复制def stock_correlation(symbol1, symbol2, start_date, end_date):
query = f"""
SELECT a.trade_date, a.close as close1, b.close as close2
FROM daily_kline a
JOIN daily_kline b ON a.trade_date = b.trade_date
WHERE a.symbol='{symbol1}'
AND b.symbol='{symbol2}'
AND a.trade_date BETWEEN '{start_date}' AND '{end_date}'
"""
df = pd.read_sql(query, conn)
return df['close1'].corr(df['close2'])
建议设置一个定时任务(如每天收盘后):
bash复制# crontab -e
0 18 * * 1-5 /usr/bin/python3 /path/to/update_script.py
更新脚本示例:
python复制def update_all_stocks():
stocks = get_stock_list() # 获取所有股票代码
for code in stocks:
last_date = get_last_trade_date(conn, code)
if last_date.date() < datetime.now().date():
update_stock_data(code, last_date)
定期检查数据完整性:
python复制def check_data_quality():
# 检查是否有缺失交易日
cursor.execute("""
SELECT symbol, COUNT(*) as cnt
FROM daily_kline
GROUP BY symbol
HAVING cnt < (SELECT COUNT(DISTINCT trade_date) FROM daily_kline)*0.9
""")
problematic = cursor.fetchall()
if problematic:
alert_admin(problematic)
除了K线数据,Baostock还提供财务数据接口,可以扩展存储:
sql复制CREATE TABLE financial_data (
id INT AUTO_INCREMENT PRIMARY KEY,
symbol VARCHAR(20) NOT NULL,
report_date DATE NOT NULL,
eps DECIMAL(10,4) COMMENT '每股收益',
roe DECIMAL(10,4) COMMENT '净资产收益率',
-- 其他财务指标...
UNIQUE KEY (symbol, report_date)
);
结合Matplotlib实现简单的数据可视化:
python复制import matplotlib.pyplot as plt
def plot_stock_trend(symbol, start_date, end_date):
df = get_stock_data(symbol, start_date, end_date)
plt.figure(figsize=(12,6))
plt.plot(df['trade_date'], df['close'], label='Close Price')
plt.plot(df['trade_date'], df['ma5'], label='5-day MA')
plt.plot(df['trade_date'], df['ma20'], label='20-day MA')
plt.title(f'{symbol} Price Trend')
plt.legend()
plt.show()
问题1:MySQL连接超时
解决方案:在连接字符串中添加连接池配置:
python复制from mysql.connector import pooling
connection_pool = pooling.MySQLConnectionPool(
pool_name="stock_pool",
pool_size=5,
host='localhost',
database='stock_data',
user='quant',
password='your_password'
)
问题2:数据插入冲突
解决方案:使用INSERT IGNORE或ON DUPLICATE KEY UPDATE:
sql复制INSERT IGNORE INTO daily_kline
(symbol, trade_date, open, high, low, close)
VALUES (%s, %s, %s, %s, %s, %s)
问题3:Baostock查询超限
解决方案:添加适当的延时:
python复制import time
for code in stock_list:
data = get_stock_data(code)
time.sleep(1) # 1秒间隔
建立本地股票数据库的过程虽然前期需要一些投入,但一旦建成,后续的研究和交易效率会得到极大提升。我在实际使用中发现,当数据量达到千万级别时,合理的索引设计能使查询性能提升数十倍。另外建议定期对数据库进行优化(如ANALYZE TABLE和OPTIMIZE TABLE),特别是频繁更新的表。