"你写的第一个策略回测年化收益 300%。然后你用实盘跑了一个月,亏了 30%。"
这不是你的策略错了——是你的数据从根子上就错了。
程序员写量化策略,最大的坑不在算法,而在数据本身。同一个品种,A 网站告诉你"今天收盘价 150",B 网站显示"148.5",C 网站写的是"148.5(后复权)"。三个数字都是对的,但如果你不理解背后的含义,回测结果会天差地别。
本文不教你选股,不教你择时,只解决一个问题:程序员转量化,你必须先搞懂的 5 个金融概念。这 5 个概念不是"锦上添花",而是写代码前必须搞清楚的基本功——搞不清楚,你的策略回测和实盘之间永远隔着一道你看不见的墙。
一、复权:为什么你回测的数据和实盘价格对不上
问题的根源:股票会"拆细"
一家公司股票价格 500 元的时候,决定"一股拆成五股",拆股后每股变成 100 元。
这不是涨跌,是数学游戏。拆股后持有 100 股的人,手里还是同等比例的公司所有权。但如果你直接拿拆股后的价格去和拆股前的价格画在一起,你会看到一根"断崖式"下跌的 K 线——而实际上公司基本面什么都没发生。
复权就是把这个数学游戏还原回去。
| 复权类型 | 定义 | 适用场景 |
|---|---|---|
| 前复权(Adjusted Close) | 以当前价格为基准,向前回溯调整历史价格 | 技术分析、指标计算(MA、RSI 等) |
| 后复权(Unadjusted Close) | 以历史价格为基准,向后调整 | 计算真实回报率、与原始研究报告对比 |
前复权:程序员最常用的方式
前复权的核心逻辑是:假设你从一开始就持有当前这股数,倒推历史每个时间点的"等价价格"。换句话说,K 线上的每一根柱子都代表"如果当时我有今天这么多股,价格应该是多少"。
在 TickDB 中,/v1/market/kline 接口默认返回前复权数据,可以直接用于计算均线、RSI、MACD 等技术指标:
import os
import requests
import pandas as pd
headers = {"X-API-Key": os.environ.get("TICKDB_API_KEY")}
# TickDB 默认返回前复权 K 线,可直接用于指标计算
response = requests.get(
"https://api.tickdb.ai/v1/market/kline",
headers=headers,
params={
"symbol": "AAPL.US",
"interval": "1d", # 日线
"limit": 500 # 最近 500 个交易日
},
timeout=(3.05, 10) # ⚠️ 连接超时 3.05s,读取超时 10s
)
data = response.json()
if data.get("code") != 0:
raise RuntimeError(f"API 错误 {data.get('code')}: {data.get('message')}")
df = pd.DataFrame(data["data"])
# 直接计算均线,无需额外处理复权问题
df["MA20"] = df["close"].rolling(window=20).mean()
df["MA60"] = df["close"].rolling(window=60).mean()
这段代码能直接跑,不是因为 TickDB 帮你做了复权计算——而是因为数据本身就已经处理好了。TickDB 的 K 线数据经过清洗和前复权对齐,开发者拿到手就是可以直接进指标引擎的干净数据。
后复权:计算真实收益率才用它
如果你要计算一笔投资的真实回报率(比如"2010 年买进 1000 股,持有到现在值多少钱"),就必须用后复权价格。因为前复权的当前价格是"调整过的",用它算出来的"收益率"会把拆股影响混进去。
# 用后复权价格计算真实总回报率
response_raw = requests.get(
"https://api.tickdb.ai/v1/market/kline",
headers=headers,
params={
"symbol": "AAPL.US",
"interval": "1d",
"limit": 1,
"adjustment": "none" # ⚠️ 关闭复权调整,获取原始价格
},
timeout=(3.05, 10)
)
# 注意:adjustment=none 适用于不需要复权的场景
# 实际使用时需根据策略需求选择合适的 adjustment 参数
程序员必须记住的一条规则:
用前复权 K 线计算技术指标,用原始 K 线(或后复权)计算真实收益率。两者混用是量化新人最常见的致命错误。
二、除息:股价"自动下跌"不是市场错了
除息的本质:钱从公司口袋里转到了股东口袋里
上市公司宣布分红 1 元/股。登记日持有股票的股东,将收到 1 元/股的分红。
**除息日(Ex-Dividend Date)**是分红生效的第一天。这天开盘时,股价会在前收盘价的基础上"自动"减去分红的金额。比如前收盘 100 元,分红 1 元,开盘价大约会是 99 元。
这不是市场在下跌,这是数学:公司把 1 元分给了你,公司账户上的现金少了 1 元,股票代表的那份资产也少了 1 元。除息后股价自然下调,叫除息缺口(Ex-Dividend Gap)。
为什么程序员要关心这个
如果你在回测中使用未复权的原始价格,分红事件会产生一个虚假的"跳空下跌",你的均值回归策略可能会误判这个缺口为"超跌信号",然后——
你以为捡到了便宜,实际上只是在买一个刚刚被分红"打折"的数字。
# 模拟除息对未复权价格的影响
# 场景:AAPL 宣布分红 0.25 美元/股,除息日 T+2
def simulate_ex_dividend(close_price: float, dividend: float) -> float:
"""
模拟除息后的开盘价(前复权价格保持不变)
"""
ex_div_price = close_price - dividend
return ex_div_open_price
# 未复权的 K 线中,除息日会产生一个 "缺口"
# 你的技术指标在这个缺口上会失真
# 所以回测系统必须使用复权数据,或者在除息日手动对齐
正确的处理方式:回测引擎在使用技术指标前,确保 K 线数据已经完成前复权。TickDB 的 K 线数据默认已完成复权处理,开发者不需要手动对齐除息缺口。
三、做空:欠着股票卖,和"买涨"完全不同的游戏
什么是做空
普通交易:买入股票 → 等涨 → 卖出获利。做空(Short Selling) 是这个过程的镜像:
- 借入股票(比如从券商借)
- 立刻卖出,拿走现金
- 等股价下跌
- 低价买回股票
- 归还券商,保留差价
做空盈利的核心逻辑:股价下跌 = 赚钱。
做空的三个程序员必须知道的细节
第一,损失空间是无限的。
买股票最多亏到 0(股价归零)。但做空不一样:你可以赚到的差价有上限(股价跌到 0),而你的损失没有上限——股价可以无限涨,你的亏损也可以无限大。
买入盈利(做多):最多赚 100% - 股价涨到无限
做空盈利:最多赚 100% - 股价跌到 0
做空亏损:亏到无限 - 股价涨到无限
第二,利息是隐性成本。
借股票不是免费的。券商收取融券利息,年化利率通常在 3%-10% 之间。如果你的空头仓位持有一年,这笔利息会持续侵蚀利润。对于高频统计套利策略,这笔成本很可能让一个理论上有正期望的策略变成负期望。
第三,保证金追加(Margin Call)。
做空需要保证金。如果股价持续上涨,你的保证金账户会不断缩水。跌到某个阈值,券商会强制平仓——不管你愿不愿意,系统自动买入股票归还。你不仅亏光了,还被券商追着要钱。
代码中的做空逻辑
# 简化的做空回测框架(概念演示,不构成投资建议)
class ShortPosition:
def __init__(self, entry_price: float, quantity: int, borrow_fee_rate: float):
self.entry_price = entry_price # 借入时卖出价格
self.quantity = quantity # 数量(负数表示空头)
self.borrow_fee_rate = borrow_fee_rate # 年化融券费率
self.entry_time = None
def calculate_pnl(self, exit_price: float, holding_days: int) -> dict:
"""计算做空盈亏(包含融券利息成本)"""
# 资本利得 = (入场价 - 出场价) × 数量
capital_gain = (self.entry_price - exit_price) * self.quantity
# 融券利息 = 入场价 × 数量 × 年化费率 × 持有天数 / 365
borrow_cost = self.entry_price * self.quantity * self.borrow_fee_rate * holding_days / 365
net_pnl = capital_gain - borrow_cost
return_pct = net_pnl / (self.entry_price * self.quantity)
return {
"capital_gain": capital_gain,
"borrow_cost": borrow_cost,
"net_pnl": net_pnl,
"return_pct": return_pct
}
def max_loss_if_price_doubles(self) -> float:
"""
⚠️ 工程预警:估算股价翻倍时的最大亏损
现实中股价翻倍会导致 margin call,实际亏损可能更大
"""
return (self.entry_price - self.entry_price * 2) * self.quantity
工程警告:上述代码仅用于理解做空盈亏的基本结构。实盘中必须处理保证金比例、margin call 触发机制、券商强平价格滑点等复杂因素。
四、期权:权利的游戏,不是股票的替代品
期权的本质:花钱买一张"未来的选择权"
期权(Options)给予持有者在未来某个时间,以约定价格买入或卖出资产的权利。注意这里是"权利"——你可以选择执行,也可以选择不执行。
| 类型 | 方向 | 你预期 |
|---|---|---|
| 看涨期权(Call) | 有权买 | 标的资产上涨 |
| 看跌期权(Put) | 有权卖 | 标的资产下跌 |
四个核心概念
行权价(Strike Price):约定的买卖价格。比如 AAPL Call 行权价 180 美元,意味着你有权在到期日前以 180 美元买入 AAPL。
权利金(Premium):买这张"选择权"要付的钱。这是做多期权最大的损失——最多亏光你付的权利金。
内在价值(Intrinsic Value):如果现在立刻行权能赚多少。AAPL 当前 190 美元,行权价 180 元的 Call 内在价值 = 10 美元。
时间价值(Time Value):不确定性带来的"机会成本"。距离到期越远,时间价值越高。时间是期权买方的敌人——随着到期日临近,时间价值会持续衰减,这个过程叫 Theta 衰减。
为什么程序员需要理解期权
一个量化策略可能不直接交易期权,但你需要理解期权市场的数据,因为:
- 期权隐含波动率(IV)是重要的市场情绪指标。VIX(恐慌指数)本质上就是 S&P 500 期权的隐含波动率。
- 期权链数据可以反推市场预期。机构通过期权布局来对冲风险,期权数据里藏着大量信息。
- 很多策略涉及期权作为对冲工具。比如备兑看涨(Covered Call)、保护看跌(Protective Put)。
# ⚠️ 概念演示:使用 TickDB 获取期权链数据
# 注意:TickDB 当前主要覆盖股票、数字货币等资产
# 完整期权数据功能请参考官方文档确认支持范围
import requests
import os
headers = {"X-API-Key": os.environ.get("TICKDB_API_KEY")}
# 获取某标的的可用期权合约列表
# response = requests.get(
# "https://api.tickdb.ai/v1/options/chain",
# headers=headers,
# params={
# "symbol": "AAPL.US",
# "expiry_date": "2026-05-16" # 到期日
# },
# timeout=(3.05, 10)
# )
# ⚠️ 生产环境提示:期权链数据量较大(同一标的可能有数百个合约)
# 建议按行权价范围筛选,避免一次性拉取全量数据造成性能问题
print("期权链数据需要按需筛选,过滤条件:")
print(" - 行权价区间(strike_range)")
print(" - 到期日范围(expiry_range)")
print(" - 期权类型(call/put)")
五、期货:杠杆、双向和交割,你需要知道的三件事
期货是什么
期货(Futures)是约定好未来某个时间、以某个价格交割某个资产的合约。买卖双方在今天约定好"三个月后我以 100 元/吨买进铁矿石",到期按 100 元交割——不管届时市场价格是 80 还是 120。
期货和期权最大的区别:期货是义务,期权是权利。
程序员需要懂的三件事
第一,杠杆(Leverage)。
期货通常只需要支付合约价值的一小部分作为保证金就能建仓。比如螺纹钢期货保证金比例 10%,意味着你用 10 万元可以控制 100 万元的螺纹钢——10 倍杠杆。
杠杆让盈利翻倍,也同样让亏损翻倍。10 倍杠杆下,螺纹钢价格下跌 10%,你的保证金就亏光了。
第二,双向交易。
和股票不同,期货可以先卖后买——不需要"先有股票才能卖"。这意味着无论市场涨跌,你都可以建仓。
第三,合约到期和移仓。
期货有到期日。临近到期时,你需要平仓或者移仓到下一个合约月。移仓有成本(远近合约价差),这个成本叫展期收益/损失(Roll Yield)。
如果你用期货数据做策略回测,一定要处理合约换月问题。主力合约不是永远不变的,你需要动态跟踪哪个合约是当前的主力。
# 期货主力合约连续数据处理(概念框架)
class FuturesRoll:
"""
⚠️ 重要:期货策略必须处理合约换月问题
主力合约切换时会产生价格跳变,直接拼接 K 线会产生虚假趋势/回撤
"""
def __init__(self, symbol: str):
self.symbol = symbol
self.current_contract = None # 当前主力合约
self.roll_date_threshold = 0.8 # 到期前 20% 天数切换
def should_roll(self, current_date: str, expiry_date: str) -> bool:
"""
判断是否需要切换合约
简单策略:到期前 20% 天数切换到次季合约
进阶策略:基于成交量/持仓量确认主力切换信号
"""
import datetime
curr = datetime.datetime.strptime(current_date, "%Y-%m-%d")
expiry = datetime.datetime.strptime(expiry_date, "%Y-%m-%d")
days_to_expiry = (expiry - curr).days
total_days = 90 # 假设季度合约,90 天周期
return days_to_expiry / total_days < self.roll_date_threshold
def get_roll_cost(self, front_contract_price: float, next_contract_price: float) -> float:
"""
计算展期成本
正数 = Contango(期货升水),多头需要支付展期成本
负数 = Backwardation(期货贴水),多头获得展期收益
"""
roll_cost = (next_contract_price - front_contract_price) / front_contract_price
return roll_cost
六、这 5 个概念怎么用在一块
说完了五个概念,你可能会问:实际写量化代码的时候,它们是怎么组合在一起的?
来看一个具体场景:你要回测一个基于技术指标的股票多空策略。
# 策略回测前的数据准备清单(TickDB + 五个概念)
class QuantDataPreprocessor:
"""
程序员转量化必读:数据准备的 5 个检查项
"""
def __init__(self, symbol: str):
self.symbol = symbol
def check_1_adjustment(self, df: pd.DataFrame) -> pd.DataFrame:
"""
✅ 复权检查:确认 K 线数据已前复权
TickDB /v1/market/kline 默认返回前复权数据
"""
# 验证数据是否连续(排除复权断层)
price_gap = (df["close"] - df["close"].shift(1)) / df["close"].shift(1)
suspicious_gaps = price_gap[abs(price_gap) > 0.5] # 单日涨跌超 50% 必是数据问题
if not suspicious_gaps.empty:
print(f"⚠️ 检测到异常价格跳变,可能存在未处理的除息/拆股:{suspicious_gaps.index.tolist()}")
return df
def check_2_dividend(self, df: pd.DataFrame, dividend_dates: list) -> None:
"""
✅ 除息检查:在已知除息日验证复权是否正确
"""
for date in dividend_dates:
if date in df["time"].values:
print(f"除息日 {date} 数据已正确处理(前复权)")
else:
print(f"⚠️ 除息日 {date} 不在数据范围内,建议延长回测窗口")
def check_3_short_availability(self) -> bool:
"""
✅ 做空可行性检查:不是所有标的都能做空
"""
# 港股/美股大盘股通常支持做空
# 小盘股、流动性差的标的可能无法借到券
# 实盘前必须确认券商的融券利率和可做空标的范围
return True
def check_4_options_skew(self, iv_data: dict) -> float:
"""
✅ 期权隐含波动率检查(可选,用于判断市场情绪)
"""
# Put/Call IV 价差 > 0.2 通常意味着市场存在下行担忧
if iv_data:
put_iv = iv_data.get("put_iv", 0)
call_iv = iv_data.get("call_iv", 0)
iv_skew = put_iv - call_iv
return iv_skew
return 0.0
def check_5_futures_roll(self, futures_df: pd.DataFrame) -> pd.DataFrame:
"""
✅ 期货展期检查:连续合约需要处理换月跳变
"""
# 检测主力合约切换时的价格跳变
price_change = futures_df["close"].pct_change()
roll_gaps = price_change[abs(price_change) > 0.05]
if not roll_gaps.empty:
print(f"⚠️ 检测到期现换月跳变点:{roll_gaps.index.tolist()}")
# 修复方法:使用收益率而非价格构建连续序列
return futures_df
这个检查清单的核心逻辑很简单:数据不对,策略白费。你花三周写的因子模型,如果用的数据有除息缺口没处理、期货合约没展期,结果和扔硬币差不多。
下一步行动
如果你刚刚开始转量化:
先把本文的 5 个概念用你自己的语言写一遍。能不能向一个不懂代码的人解释清楚"为什么前复权和后复权价格不一样"?能讲清楚,说明你真的懂了。
如果你已经在写策略但回测和实盘差距大:
回到你的数据源,确认你用的是前复权数据、期货做了展期处理、除息日做了对齐。这是量化回测最常见的"幽灵错误"。
如果你想直接跳过数据预处理这个坑:
TickDB 的 K 线数据已内置前复权处理,API 设计遵循 REST 规范,可以直接集成进你的回测框架。注册 tickdb.ai,免费获取 API Key,用干净的金融数据开始你的量化之旅。
风险提示:本文不构成任何投资建议。期货、期权等衍生品交易涉及杠杆风险,可能导致本金损失。市场有风险,投资需谨慎。