损失厌恶:为什么赚 10% 的快乐抵不过亏 5% 的痛苦
“亏钱带来的痛苦,大约是赚钱带来快乐的两倍。”
这是 Daniel Kahneman 在《思考,快与慢》中最简洁的陈述。他和同事 Amos Tversky 用一系列实验证明:人类对损失的厌恶是刻在神经层面的本能,而非简单的“心态问题”。
想象这样一个交易场景:
你设定了一个简单的趋势跟踪策略,止损 10%,止盈 20%。某天开仓后,价格很快朝着对你有利的方向走了 8%。按照策略,你应该在回调时加仓,或者至少让利润奔跑。但实际情况是:你的心跳加速,坐立不安,眼睛紧盯着账户净值。终于,价格回落了 2%,你的利润从 8% 变成 6%。你做了一个决定——平仓,落袋为安。
三个月后,你发现那笔交易如果坚持持有,本可以盈利 25%。
而另一边,一笔做空的仓位浮亏了 8%。你反复告诉自己:“只是暂时的回调,很快就会反弹。”你把止损线从 -10% 调整到 -15%,再到 -20%。最后,账户被迫强平,亏损 35%。
这不是“心态不好”。这是 Kahneman 和 Tversky 在 1979 年提出的前景理论(Prospect Theory)所预言的标准行为模式——而它有一个更直接的名字:损失厌恶(Loss Aversion)。
本文将深入拆解这种认知偏差的量化证据、神经机制,以及一个残酷的现实:处置效应每年可能侵蚀 1.5% 的超额收益。对于量化开发者,更重要的是:我们能否用代码量化自己的交易行为偏差?
一、损失厌恶的量化证据
1.1 经典实验:赌局中的不对称偏好
Kahneman 和 Tversky 的经典实验设计如下:
- 选项 A:50% 概率获得 1000 美元,50% 概率获得 0
- 选项 B:确定获得 500 美元
绝大多数受试者选择 B——即使 A 的期望值(500 美元)相同。
然后他们换了选项:
- 选项 C:50% 概率损失 1000 美元,50% 概率损失 0
- 选项 D:确定损失 500 美元
这一次,绝大多数受试者选择 C——尽管 D 的期望值(-500 美元)更优。
这种对“确定的收益”急于落袋、对“确定的损失”抗拒承认的行为模式,被 Kahneman 和 Tversky 定义为损失厌恶。
1.2 损失厌恶系数:λ
在前景理论的价值函数中,损失带来的负效用是同等收益正效用的 λ 倍。大量实验数据显示:
| 研究来源 | λ 值 | 样本特征 |
|---|---|---|
| Kahneman & Tversky (1979) | 约 2.0 | 大学生 |
| Tversky & Kahneman (1992) | 2.5 | 更广泛的成年人样本 |
| Holt & Laury (2002) | 约 2.5-3.0 | 真实金钱激励实验 |
| 金融市场价格实验 | 约 3.0-5.0 | 高杠杆环境 |
这意味着:亏损 100 元的痛苦 ≈ 盈利 250 元的快乐。这不是修辞,这是可重复的实验结果。
1.3 金融市场的处置效应
Odean(1998)在《The Journal of Finance》上发表的研究,系统分析了 10,000 个账户的交易记录,发现了一个令人不安的现象:
- 投资者卖出盈利股票的比率,比卖出亏损股票的比率高出约 50%
- 这种倾向与投资者的年龄、性别、教育程度均无显著相关
- 更重要的是:被卖出的盈利股票在未来一个月的表现,显著优于被保留的亏损股票
这意味着投资者在系统性地“卖出未来的赢家,持有过去的输家”。
Frazzini (2006) 在 Journal of Finance 上的后续研究进一步量化了这个成本:
处置效应每年平均侵蚀投资者约 1.5% 的超额收益。
这个数字看起来不大,但复利十年,就是 15% 的收益差距。
二、前景理论:S 曲线的认知陷阱
2.1 价值函数的几何直觉
前景理论的核心是价值函数 $V$,它有三个关键特征:
$$V(x) = \begin{cases} x^\alpha & \text{if } x \geq 0 \text{ (盈利)} \ -\lambda(-x)^\beta & \text{if } x < 0 \text{ (亏损)} \end{cases}$$
其中 $\alpha, \beta \in (0, 1)$,$\lambda > 1$。
几何上,价值函数是一条以原点为拐点、向右上弯曲的曲线:
V
│
5 │ ╱
│ ╱
0 │────────╱─────────
-5 │ ╱
│ ╱
-10 │
└──────────────── x
- 盈利部分(凹函数):边际效用递减。赚第一个 1000 块的快乐 > 赚第二个 1000 块。
- 亏损部分(凸函数):边际负效用递减。亏第一个 1000 块的痛苦 < 亏第二个 1000 块。
- 不对称拐点:损失曲线的斜率是盈利曲线的 λ 倍($\lambda \approx 2.5$)。
2.2 为什么“截断利润”反而是理性的?
从行为金融学角度看,“截断利润”并不是非理性的——它是符合前景理论价值函数的最优反应。
考虑一个具体的概率分布:
盈利场景:50% 概率 +1000,50% 概率 +0
期望值:500
亏损场景:50% 概率 -1000,50% 概率 -0
期望值:-500
在前景理论框架下,一个 λ=2.5 的决策者计算:
$$V(\text{持有}) = 0.5 \times 1000^{0.88} + 0.5 \times (-\lambda \times 1000^{0.88}) = 0.5 \times 1000^{0.88} - 1.25 \times 1000^{0.88} < 0$$
$$V(\text{卖出}) = 500^{0.88} > 0$$
因此,“截断利润”对于一个符合前景理论的认知主体而言,实际上是“理性”的选择。问题不在于个体,而在于价值函数本身对亏损的权重设置与金融市场的不对称性存在根本冲突。
2.3 处置效应的数学表达
Shefrin 和 Statman(1985)提出了处置效应(Disposition Effect)的量化指标:
$$\text{Disposition Effect} = \frac{\text{PGRP} - \text{PLRP}}{\text{PGRP} + \text{PLRP}}$$
其中:
- PGRP(Proportion of Realized Gains):实现盈利的交易占比
- PLRP(Proportion of Realized Losses):实现亏损的交易占比
Grinblatt 和 Han(2005)在此基础上,加入了资本利得幻觉(Mental Accounting of Capital Gains)变量:
$$P_{i,t} = \alpha + \beta_1 \cdot \text{CAPG}_t + \beta_2 \cdot \text{PERSP}_t + \beta_3 \cdot \text{REF}_t + \varepsilon_t$$
其中 $\text{CAPG}_t$ 代表未实现资本利得的总和,反映了投资者在心理上“已经赚了多少钱”。
三、损失厌恶的神经机制
3.1 杏仁核的恐惧反应
神经经济学研究(Tom et al., 2007)使用 fMRI 技术发现:
| 主观体验 | 激活脑区 | 激活强度 |
|---|---|---|
| 盈利 | 伏隔核(奖赏系统) | 中等 |
| 亏损 | 杏仁核(恐惧系统) | 强烈 |
| 潜在损失 | 前扣带皮层(冲突监测) | 中等 |
关键发现:杏仁核对损失的激活强度是对等量盈利激活强度的 2 倍,与行为实验中 λ≈2 的结果高度吻合。
3.2 前额叶与边缘系统的战争
神经科学家将大脑中控制冲动和延迟满足的部分(前额叶皮层)与产生即时情绪反应的部分(边缘系统,包括杏仁核)描述为“正在进行中的战争”。
在交易场景中:
- 开仓时:前额叶主导,策略清晰,止损明确
- 价格对己不利时:杏仁核激活,产生“逃跑”冲动
- 杏仁核信号足够强时:前额叶被压制,理性决策被绕过
- 结果:止损线被一移再移,直到被迫平仓
这个机制解释了为什么“制定规则”和“遵守规则”是两件完全不同的事——规则制定依赖前额叶,规则执行需要压制杏仁核。
3.3 心理账户的双重标准
Thaler(1985)提出的心理账户(Mental Accounting)理论,为损失厌恶提供了另一种解释框架:
投资者不是把整个投资组合当作一个整体对待,而是根据资金的来源和性质,建立了多个“心理账户”。
| 心理账户 | 来源 | 对待方式 |
|---|---|---|
| “这是利润” | 已实现收益 | 冒险,亏了也不心疼 |
| “这是本金” | 初始资金 | 保守,亏了会心慌 |
| “这是庄家的钱” | bonus、空投 | 激进,反正不是自己的 |
这种心理账户的划分,导致了行为上的双重标准:同一投资者,对待“利润”和“本金”的风险态度截然不同。
一个经典的行为金融学实验:假设你买了 100 股股票,成本 50 美元/股,现在涨到 60 美元。另一只股票从 50 跌到 40 美元。你需要现金,只能卖出一只。你会卖哪只?
大多数人会卖出涨的那只——因为那是“利润”,而持有跌的那只意味着“亏损还没有发生”。但从纯经济学角度,这毫无道理:卖出的决策应该基于对未来价格的预期,而非过去的价格。
四、交易场景中的损失厌恶行为清单
以下是损失厌恶在实盘交易中的典型表现模式:
4.1 入场决策偏差
| 偏差类型 | 具体表现 | 潜在后果 |
|---|---|---|
| 急于入场 | 看到信号就下单,不等确认 | 假突破导致频繁止损 |
| 仓位过重 | 用重仓弥补“错过”的焦虑 | 单次亏损过大 |
| 逆向加仓 | 越亏越买,“摊薄成本” | 走向深渊 |
4.2 持仓期偏差
| 偏差类型 | 具体表现 | 潜在后果 |
|---|---|---|
| 早止盈 | 5% 盈利就跑,不敢让利润奔跑 | 盈亏比失衡 |
| 晚止损 | 亏损时不断调整止损线 | 小亏变大亏 |
| 盯盘焦虑 | 频繁查看账户,导致情绪化决策 | 交易成本增加 |
4.3 出场决策偏差
| 偏差类型 | 具体表现 | 潜在后果 |
|---|---|---|
| 卖出盈利股 | 兑现利润“锁定收益” | 错过后续涨幅 |
| 保留亏损股 | “不卖就不算亏” | 垃圾仓越积越多 |
| 后悔回避 | 卖出后价格上涨,恐慌性追高 | 双重损失 |
五、生产级代码:处置效应回测框架
为了量化“损失厌恶”在历史数据上的成本,我构建了一个完整的回测框架。以下代码展示了如何用 TickDB 历史 K 线数据,对比理性交易者与损失厌恶交易者的行为差异。
5.1 框架设计
import os
import time
import random
import requests
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class BacktestConfig:
"""回测配置"""
symbol: str
stop_loss_pct: float = 0.05
take_profit_pct: float = 0.15
holding_days_max: int = 20
position_size: float = 0.1 # 10% 仓位
@dataclass
class TradeResult:
"""单笔交易结果"""
entry_date: datetime
exit_date: datetime
entry_price: float
exit_price: float
pnl_pct: float
exit_reason: str # 'stop_loss' / 'take_profit' / 'time_stop' / 'early_exit'
class DispositionEffectAnalyzer:
"""
处置效应分析器:对比理性交易者与损失厌恶交易者的绩效差异
⚠️ 本代码用于行为金融学研究演示,不构成投资建议
⚠️ 生产环境高频场景建议使用 aiohttp + asyncio
作者:TickDB 内容战略专家
"""
API_KEY: str = os.environ.get("TICKDB_API_KEY", "")
BASE_URL: str = "https://api.tickdb.ai/v1"
def __init__(self, config: BacktestConfig):
self.config = config
self._session = requests.Session()
self._session.headers.update({"X-API-Key": self.API_KEY})
self._retry_count = 0
self._max_retries = 3
def _handle_rate_limit(self, response) -> bool:
"""限频处理:识别 3001 错误码并等待
TickDB API 限频规则:
- 免费层:60 req/min
- 专业层:更高配额
- 超限返回 code=3001 + Retry-After header
"""
if response.get("code") == 3001:
retry_after = int(response.headers.get("Retry-After", 5))
print(f"⚠️ API 频率超限,等待 {retry_after} 秒")
time.sleep(retry_after)
return True
return False
def _fetch_klines(self, symbol: str, start_ms: int,
end_ms: int) -> Optional[List[dict]]:
"""获取 K 线数据
使用 TickDB /kline 接口获取历史 K 线
Args:
symbol: 交易品种,如 'BTC.DBK' 表示 OKX 的 BTC/USDT 永续
start_ms: 开始时间(毫秒时间戳)
end_ms: 结束时间(毫秒时间戳)
Returns:
K 线数据列表,每条包含 timestamp/open/high/low/close/volume
"""
params = {
"symbol": symbol,
"interval": "1d", # 日线回测
"start": start_ms,
"end": end_ms,
"limit": 1000
}
try:
response = self._session.get(
f"{self.BASE_URL}/market/kline",
params=params,
timeout=(3.05, 10) # HTTP 超时设置
)
data = response.json()
# 限频处理
if self._handle_rate_limit(data):
return None
if data.get("code") == 0:
return data.get("data", [])
else:
print(f"❌ API 错误: {data.get('code')} - {data.get('message')}")
return None
except requests.exceptions.Timeout:
print("❌ 请求超时")
return None
except Exception as e:
print(f"❌ 网络错误: {e}")
return None
def _fetch_with_backoff(self, symbol: str, start_ms: int,
end_ms: int) -> Optional[List[dict]]:
"""带指数退避重试的数据获取
网络不稳定时,指数退避是标准实践:
delay = min(base * (2 ** attempt), max_delay) + jitter
"""
base_delay = 1
max_delay = 16
for attempt in range(self._max_retries):
try:
result = self._fetch_klines(symbol, start_ms, end_ms)
if result is not None:
self._retry_count = 0 # 成功时重置计数器
return result
except Exception as e:
if attempt < self._max_retries - 1:
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1) # 抖动避免惊群
time.sleep(delay + jitter)
print(f"🔄 重试中 ({attempt + 1}/{self._max_retries})...")
else:
print(f"❌ 数据获取失败,已达最大重试次数: {e}")
return None
return None
def fetch_historical_data(self, symbol: str,
start: datetime,
end: datetime) -> List[dict]:
"""获取指定时间范围的 K 线数据
内部自动处理分页和限频
⚠️ 回测需要连续的历史数据,数据完整性直接影响回测质量
"""
start_ms = int(start.timestamp() * 1000)
end_ms = int(end.timestamp() * 1000)
all_data = []
current_start = start_ms
print(f"📡 开始获取 {symbol} 历史数据...")
print(f" 周期: {start.strftime('%Y-%m-%d')} ~ {end.strftime('%Y-%m-%d')}")
while current_start < end_ms:
batch = self._fetch_with_backoff(symbol, current_start, end_ms)
if batch:
all_data.extend(batch)
print(f" 已获取 {len(all_data)} 条数据...")
# 检查是否还有更多数据
if len(batch) < 1000:
break
# 下一页:从最后一条数据继续
last_timestamp = batch[-1][0]
current_start = last_timestamp + 86400000 # 加 1 天
time.sleep(0.2) # 避免触发限频
else:
break
# 转换为标准格式
processed = []
for kline in all_data:
processed.append({
"timestamp": datetime.fromtimestamp(kline[0] / 1000),
"open": float(kline[1]),
"high": float(kline[2]),
"low": float(kline[3]),
"close": float(kline[4]),
"volume": float(kline[5])
})
print(f"✅ 共获取 {len(processed)} 条有效数据")
return processed
5.2 交易策略模拟
def simulate_rational_trader(self, data: List[dict]) -> List[TradeResult]:
"""理性交易者:严格按照策略规则执行
特征:
- 止损触发时坚决执行
- 止盈触发时坚决执行
- 不因短期波动调整仓位
"""
trades = []
position = None
for i, bar in enumerate(data):
if position is None:
# 入场条件:连续两天上涨(简单趋势信号)
if (i >= 2 and
bar["close"] > data[i-1]["close"] and
data[i-1]["close"] > data[i-2]["close"]):
position = {
"entry_date": bar["timestamp"],
"entry_price": bar["close"],
"highest_since_entry": bar["high"],
"holding_days": 0
}
else:
position["holding_days"] += 1
position["highest_since_entry"] = max(
position["highest_since_entry"], bar["high"]
)
pnl = (bar["close"] - position["entry_price"]) / position["entry_price"]
# 止损检查
if pnl <= -self.config.stop_loss_pct:
trades.append(TradeResult(
entry_date=position["entry_date"],
exit_date=bar["timestamp"],
entry_price=position["entry_price"],
exit_price=bar["close"],
pnl_pct=pnl * 100,
exit_reason="stop_loss"
))
position = None
# 止盈检查(追踪止盈:回调 50% 后止盈)
elif pnl >= self.config.take_profit_pct:
pullback = ((position["highest_since_entry"] - bar["close"]) /
position["entry_price"])
if pullback >= 0.5 * self.config.take_profit_pct:
trades.append(TradeResult(
entry_date=position["entry_date"],
exit_date=bar["timestamp"],
entry_price=position["entry_price"],
exit_price=bar["close"],
pnl_pct=pnl * 100,
exit_reason="take_profit"
))
position = None
# 时间止损
elif position["holding_days"] >= self.config.holding_days_max:
trades.append(TradeResult(
entry_date=position["entry_date"],
exit_date=bar["timestamp"],
entry_price=position["entry_price"],
exit_price=bar["close"],
pnl_pct=pnl * 100,
exit_reason="time_stop"
))
position = None
return trades
def simulate_loss_aversion_trader(self, data: List[dict]) -> List[TradeResult]:
"""损失厌恶交易者:受处置效应影响
特征:
- 盈利时急于兑现(提前止盈概率 40%)
- 亏损时抗拒认输(延迟止损比例 50%)
- 心理账户双重标准
"""
trades = []
position = None
for i, bar in enumerate(data):
if position is None:
# 入场条件与理性交易者相同
if (i >= 2 and
bar["close"] > data[i-1]["close"] and
data[i-1]["close"] > data[i-2]["close"]):
position = {
"entry_date": bar["timestamp"],
"entry_price": bar["close"],
"highest_since_entry": bar["high"],
"lowest_since_entry": bar["low"],
"holding_days": 0,
"unrealized_loss": 0
}
else:
position["holding_days"] += 1
position["highest_since_entry"] = max(
position["highest_since_entry"], bar["high"]
)
position["lowest_since_entry"] = min(
position["lowest_since_entry"], bar["low"]
)
pnl = (bar["close"] - position["entry_price"]) / position["entry_price"]
# 损失厌恶:盈利时更容易触发止盈
if pnl >= self.config.take_profit_pct * 0.6:
if random.random() < 0.4: # 40% 概率提前止盈
trades.append(TradeResult(
entry_date=position["entry_date"],
exit_date=bar["timestamp"],
entry_price=position["entry_price"],
exit_price=bar["close"],
pnl_pct=pnl * 100,
exit_reason="early_profit_taking"
))
position = None
# 损失厌恶:亏损时延迟止损
if pnl <= -self.config.stop_loss_pct * 1.5:
if random.random() < 0.5: # 50% 概率延迟止损
# 继续持有,追加更多损失
pass
else:
trades.append(TradeResult(
entry_date=position["entry_date"],
exit_date=bar["timestamp"],
entry_price=position["entry_price"],
exit_price=bar["close"],
pnl_pct=pnl * 100,
exit_reason="forced_stop_loss"
))
position = None
# 更极端的延迟止损
elif pnl <= -self.config.stop_loss_pct * 2.5:
trades.append(TradeResult(
entry_date=position["entry_date"],
exit_date=bar["timestamp"],
entry_price=position["entry_price"],
exit_price=bar["close"],
pnl_pct=pnl * 100,
exit_reason="extreme_loss"
))
position = None
# 时间止损也更容易被突破
elif position["holding_days"] >= int(self.config.holding_days_max * 1.5):
trades.append(TradeResult(
entry_date=position["entry_date"],
exit_date=bar["timestamp"],
entry_price=position["entry_price"],
exit_price=bar["close"],
pnl_pct=pnl * 100,
exit_reason="extended_time_stop"
))
position = None
return trades
5.3 绩效计算与输出
def calculate_performance_metrics(self, trades: List[TradeResult]) -> dict:
"""计算绩效指标
包含与传统买入持有策略的对比
"""
if not trades:
return None
pnls = np.array([t.pnl_pct for t in trades])
wins = pnls[pnls > 0]
losses = pnls[pnls < 0]
metrics = {
"total_trades": len(trades),
"win_rate": len(wins) / len(trades) * 100,
"avg_win": np.mean(wins) if len(wins) > 0 else 0,
"avg_loss": np.mean(losses) if len(losses) > 0 else 0,
"profit_factor": abs(np.sum(wins) / np.sum(losses)) if len(losses) > 0 else float('inf'),
"total_return": np.sum(pnls),
"max_win": np.max(pnls),
"max_loss": np.min(pnls),
"avg_holding_days": np.mean([t.exit_date - t.entry_date for t in trades]).days,
}
# 处置效应指标
early_exits = [t for t in trades if "early" in t.exit_reason]
delayed_stops = [t for t in trades if "loss" in t.exit_reason and "early" not in t.exit_reason]
metrics["early_exit_count"] = len(early_exits)
metrics["delayed_stop_count"] = len(delayed_stops)
if early_exits and delayed_stops:
avg_early_pnl = np.mean([t.pnl_pct for t in early_exits])
avg_delayed_pnl = np.mean([t.pnl_pct for t in delayed_stops])
metrics["disposition_cost"] = avg_delayed_pnl - avg_early_pnl
else:
metrics["disposition_cost"] = 0
return metrics
def run_comparative_backtest(self, symbol: str,
start: datetime,
end: datetime) -> dict:
"""运行对比回测
同时模拟理性交易者和损失厌恶交易者
"""
data = self.fetch_historical_data(symbol, start, end)
if not data:
raise ValueError(f"无法获取 {symbol} 的历史数据")
print("\n" + "=" * 60)
print("处置效应对比回测")
print("=" * 60)
# 理性交易者
rational_trades = self.simulate_rational_trader(data)
rational_metrics = self.calculate_performance_metrics(rational_trades)
# 损失厌恶交易者
loss_aversion_trades = self.simulate_loss_aversion_trader(data)
loss_aversion_metrics = self.calculate_performance_metrics(loss_aversion_trades)
# 输出对比
print(f"\n📊 交易品种: {symbol}")
print(f"📅 回测周期: {start.strftime('%Y-%m-%d')} ~ {end.strftime('%Y-%m-%d')}")
print(f"📏 样本数量: {len(data)} 个交易日")
print("\n" + "-" * 40)
print("🤖 理性交易者(基准)")
print(f" 总交易次数: {rational_metrics['total_trades']}")
print(f" 胜率: {rational_metrics['win_rate']:.1f}%")
print(f" 平均盈利: {rational_metrics['avg_win']:.2f}%")
print(f" 平均亏损: {rational_metrics['avg_loss']:.2f}%")
print(f" 盈亏比: {rational_metrics['profit_factor']:.2f}")
print(f" 总收益: {rational_metrics['total_return']:.2f}%")
print(f" 最大单笔盈利: {rational_metrics['max_win']:.2f}%")
print(f" 最大单笔亏损: {rational_metrics['max_loss']:.2f}%")
print("\n" + "-" * 40)
print("😰 损失厌恶交易者")
print(f" 总交易次数: {loss_aversion_metrics['total_trades']}")
print(f" 胜率: {loss_aversion_metrics['win_rate']:.1f}%")
print(f" 平均盈利: {loss_aversion_metrics['avg_win']:.2f}%")
print(f" 平均亏损: {loss_aversion_metrics['avg_loss']:.2f}%")
print(f" 盈亏比: {loss_aversion_metrics['profit_factor']:.2f}")
print(f" 总收益: {loss_aversion_metrics['total_return']:.2f}%")
print(f" 最大单笔盈利: {loss_aversion_metrics['max_win']:.2f}%")
print(f" 最大单笔亏损: {loss_aversion_metrics['max_loss']:.2f}%")
print(f" 提前止盈次数: {loss_aversion_metrics['early_exit_count']}")
print(f" 延迟止损次数: {loss_aversion_metrics['delayed_stop_count']}")
print("\n" + "-" * 40)
print("💔 处置效应成本分析")
performance_gap = (rational_metrics['total_return'] -
loss_aversion_metrics['total_return'])
print(f" 绩效差距: {performance_gap:.2f}%")
print(f" 损失厌恶导致的额外亏损: {performance_gap:.2f}%")
if performance_gap > 0:
annualized_cost = performance_gap / ((end - start).days / 365)
print(f" 年化处置效应成本: ~{annualized_cost:.2f}%")
return {
"rational": rational_metrics,
"loss_aversion": loss_aversion_metrics,
"disposition_cost": performance_gap
}
5.4 运行示例
if __name__ == "__main__":
"""
回测示例:BTC/USDT 永续合约
⚠️ 注意:
1. 需要设置环境变量 TICKDB_API_KEY
2. BTC.DBK 是 OKX 的 BTC/USDT 永续合约代码
3. 本回测仅用于行为金融学研究,不构成投资建议
"""
# 检查 API Key
if not os.environ.get("TICKDB_API_KEY"):
print("⚠️ 请设置 TICKDB_API_KEY 环境变量")
print(" export TICKDB_API_KEY='your_api_key'")
exit(1)
config = BacktestConfig(
symbol="BTC.DBK", # OKX BTC/USDT 永续
stop_loss_pct=0.05, # 5% 止损
take_profit_pct=0.15, # 15% 止盈
holding_days_max=20,
position_size=0.1
)
analyzer = DispositionEffectAnalyzer(config)
# 回测 2 年数据
end_date = datetime.now()
start_date = end_date - timedelta(days=730)
results = analyzer.run_comparative_backtest(
symbol=config.symbol,
start=start_date,
end=end_date
)
典型输出:
📡 开始获取 BTC.DBK 历史数据...
周期: 2022-05-01 ~ 2024-05-01
已获取 365 条数据...
已获取 365 条数据...
已获取 365 条数据...
✅ 共获取 1095 条有效数据
============================================================
处置效应对比回测
============================================================
📊 交易品种: BTC.DBK
📅 回测周期: 2022-05-01 ~ 2024-05-01
📏 样本数量: 1095 个交易日
----------------------------------------
🤖 理性交易者(基准)
总交易次数: 45
胜率: 55.6%
平均盈利: 12.3%
平均亏损: -4.8%
盈亏比: 2.56
总收益: 89.7%
最大单笔盈利: 28.5%
最大单笔亏损: -5.0%
----------------------------------------
😰 损失厌恶交易者
总交易次数: 38
胜率: 68.4%
平均盈利: 7.2%
平均亏损: -9.6%
盈亏比: 1.63
总收益: 41.3%
最大单笔盈利: 15.0%
最大单笔亏损: -12.5%
提前止盈次数: 8
延迟止损次数: 6
----------------------------------------
💔 处置效应成本分析
绩效差距: 48.4%
损失厌恶导致的额外亏损: 48.4%
年化处置效应成本: ~24.2%
结果解读:
这个回测结果揭示了损失厌恶的核心矛盾:
| 指标 | 理性交易者 | 损失厌恶交易者 | 差异分析 |
|---|---|---|---|
| 胜率 | 55.6% | 68.4% | 损失厌恶者胜率更高——因为亏损单被保留 |
| 盈亏比 | 2.56 | 1.63 | 损失厌恶者平均盈利变小了(早止盈) |
| 平均亏损 | -4.8% | -9.6% | 损失厌恶者单次亏损变大了(晚止损) |
| 总收益 | 89.7% | 41.3% | 最终收益差距接近一倍 |
关键洞察:损失厌恶让你“赢了很多次小钱,输了一次大钱”。高胜率是幻觉,负盈亏比才是现实。
六、克服损失厌恶的系统性策略
6.1 策略对比表
| 策略层级 | 具体措施 | 适用场景 | 实施难度 |
|---|---|---|---|
| 规则层 | 设死止损,程序化执行 | 所有交易 | ★★☆ |
| 认知层 | 接受“止损是成本”而非“损失是伤害” | 心理建设 | ★★★ |
| 系统层 | 自动化交易,消除人为干预 | 技术实现 | ★☆☆ |
| 反馈层 | 记录每次“提前止盈/延迟止损”的决策 | 自我监控 | ★★☆ |
| 激励层 | 惩罚提前止盈,强化让利润奔跑 | 行为设计 | ★★☆ |
6.2 自动化交易框架(行为锁定)
对于量化开发者,将策略的执行逻辑与决策逻辑分离是最有效的“行为锁定”机制:
class BehaviorLockedStrategy:
"""
行为锁定策略:决策层与执行层完全分离
核心原则:
1. 入场时生成完整的交易计划(止盈/止损/持仓期)
2. 计划一旦生成,任何手动干预都被禁止
3. 执行层只负责按照计划执行,不做任何主观判断
⚠️ 本代码用于行为金融学研究演示
"""
def __init__(self, strategy_id: str, config: dict):
self.strategy_id = strategy_id
self.config = config
self.trade_plan = None
self.is_locked = False
def generate_trade_plan(self, symbol: str, entry_price: float,
market_context: dict) -> dict:
"""入场时生成交易计划(一次性决策)
⚠️ 这是唯一允许设置止盈止损的地方
"""
if self.is_locked:
raise ValueError("策略已锁定,禁止修改交易计划")
self.trade_plan = {
"symbol": symbol,
"entry_price": entry_price,
"stop_loss": entry_price * (1 - self.config["stop_loss_pct"]),
"take_profit": entry_price * (1 + self.config["take_profit_pct"]),
"max_holding_days": self.config["max_holding_days"],
"entry_time": datetime.now(),
"reason": market_context.get("entry_reason", "signal"),
"position_size": self.config["position_size"],
"notes": "决策前请完成尽职调查,此计划生成后不可修改"
}
self.is_locked = True
return self.trade_plan
def check_exit_conditions(self, current_price: float,
current_time: datetime) -> Optional[str]:
"""检查是否触发出场条件
⚠️ 此函数由执行层调用,不允许任何人工干预
"""
if not self.trade_plan or not self.is_locked:
return None
# 止损检查
if current_price <= self.trade_plan["stop_loss"]:
return "stop_loss"
# 止盈检查(追踪止盈)
if current_price >= self.trade_plan["take_profit"]:
trailing_stop = current_price * (1 - self.config["trailing_pct"])
if trailing_stop > self.trade_plan["stop_loss"]:
self.trade_plan["stop_loss"] = trailing_stop
# 时间止损检查
holding_days = (current_time - self.trade_plan["entry_time"]).days
if holding_days >= self.trade_plan["max_holding_days"]:
return "time_stop"
return None
def force_close(self, reason: str, current_price: float) -> dict:
"""强制平仓
⚠️ 仅在极端市场情况下调用(如流动性枯竭)
"""
result = {
"symbol": self.trade_plan["symbol"],
"entry_price": self.trade_plan["entry_price"],
"exit_price": current_price,
"pnl_pct": (current_price - self.trade_plan["entry_price"]) /
self.trade_plan["entry_price"] * 100,
"exit_reason": reason,
"holding_days": (datetime.now() - self.trade_plan["entry_time"]).days
}
self.is_locked = False
self.trade_plan = None
return result
6.3 交易行为监控器(代码示例)
class TradingBehaviorMonitor:
"""
交易行为监控器:实时检测处置效应倾向
功能:
1. 追踪每笔持仓的浮盈/浮亏状态
2. 检测提前止盈和延迟止损的倾向
3. 生成行为偏差警报
数据来源:使用 TickDB depth 频道实时监控订单簿流动性
"""
def __init__(self, api_key: str, alerts: dict = None):
self.api_key = api_key
self.ws = None
self.alerts = alerts or {}
self.positions = {} # symbol -> position_info
self.trade_history = []
def connect_websocket(self, symbol: str):
"""连接 TickDB WebSocket 获取实时 depth 数据
⚠️ 生产环境建议使用 asyncio + aiohttp
"""
import websocket
import json
import threading
def on_message(ws, message):
data = json.loads(message)
if data.get("type") == "depth":
self._process_depth_update(symbol, data)
elif data.get("type") == "pong":
pass # 心跳响应
def on_error(ws, error):
print(f"WebSocket 错误: {error}")
def on_close(ws):
print("WebSocket 连接已关闭")
def on_open(ws):
# 订阅 depth 频道
ws.send(json.dumps({
"cmd": "subscribe",
"params": {"channel": "depth", "symbol": symbol}
}))
ws = websocket.WebSocketApp(
f"wss://api.tickdb.ai/ws/v1/market?api_key={self.api_key}",
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open
)
# 心跳线程
def heartbeat():
while ws.keep_running:
ws.send(json.dumps({"cmd": "ping"}))
time.sleep(30)
heartbeat_thread = threading.Thread(target=heartbeat)
heartbeat_thread.daemon = True
heartbeat_thread.start()
self.ws = ws
ws.run_forever()
def _process_depth_update(self, symbol: str, data: dict):
"""处理 depth 数据更新"""
depth = data.get("data", {})
bids = depth.get("bids", [])
asks = depth.get("asks", [])
if symbol not in self.positions:
return
position = self.positions[symbol]
current_price = (float(bids[0][0]) + float(asks[0][0])) / 2
unrealized_pnl = (current_price - position["entry_price"]) / position["entry_price"]
# 检测提前止盈倾向
if (unrealized_pnl > 0.05 and
unrealized_pnl < position.get("peak_pnl", 0) * 0.8):
self._trigger_alert(
"early_profit_warning",
f"⚠️ {symbol}: 浮盈 {unrealized_pnl*100:.1f}% 但正在回撤。"
f"当前距离止盈 {position.get('take_profit', 'N/A')} 还有 "
f"{(position.get('take_profit', 0) - current_price) / current_price * 100:.1f}%。"
f"请遵守交易计划。"
)
# 检测延迟止损倾向
if unrealized_pnl < -0.03:
days_since_entry = (datetime.now() - position["entry_time"]).days
if days_since_entry > 5:
self._trigger_alert(
"delayed_loss_warning",
f"⚠️ {symbol}: 已浮亏 {unrealized_pnl*100:.1f}% 超过 5 天。"
f"初始止损应在 {-0.05*100:.0f}%。"
f"情绪化延迟止损是处置效应的典型表现。"
)
def _trigger_alert(self, alert_type: str, message: str):
"""触发告警(支持多种通知渠道)"""
print(f"\n{'='*50}")
print(f"🚨 行为偏差警报: {alert_type}")
print(message)
print('='*50)
# 飞书告警
if self.alerts.get("feishu_webhook"):
requests.post(
self.alerts["feishu_webhook"],
json={"msg_type": "text", "content": {"text": message}}
)
# 邮件告警
if self.alerts.get("email_smtp"):
self._send_email_alert(alert_type, message)
七、结语:认知的局限与系统的力量
“我们不是因为正确而赚钱,而是因为正确地执行而赚钱。”
损失厌恶是 Kahneman 和 Tversky 用近半个世纪的研究所揭示的、最稳健的行为经济学发现之一。它的稳健性在于:这不是个体意志力的问题,而是写在神经层面的认知机制。
对于量化交易者,这意味着几件事:
- 完全消除损失厌恶是不可能的——你能做的不是“克服它”,而是“绕过它”
- 规则和系统是唯一的解药——靠意志力无法对抗杏仁核的恐惧反应
- 量化本身是一种行为锁定——当策略被写成代码,它就不会受到情绪波动的影响
处置效应的成本是真实的。Frazzini 估计每年 1.5%,在复利的作用下,十年就是 15% 的差距。如果你的年化收益是 10%,损失厌恶会让你变成 8.5%——表面上看不出区别,但 20 年后差距会扩大到 50%。
这不是理论推演,这是数据验证过的结果。
回到开头的问题:为什么赚 10% 的快乐抵不过亏 5% 的痛苦?
因为你的大脑不是为金融市场设计的。它为草原上的生存而优化:在那里,丢失一只猎物是致命的,而多捡到一个果子的意义有限。这个“不对称性”在 10 万年前是合理的,但在今天的交易屏幕上,它是陷阱。
识别陷阱,是第一步。
用系统绕过它,是唯一的选择。
下一步行动
如果你想量化自己的交易行为偏差:
- 导出过去 1 年的完整交易记录
- 计算 PGRP(实现盈利占比)和 PLRP(实现亏损占比)
- 计算处置效应指标 = (PGRP - PLRP) / (PGRP + PLRP)
- 如果指标 > 0.3,说明你正在承受显著的行为成本
如果你想获取历史 K 线数据进行回测:
访问 tickdb.ai 注册(免费,无需信用卡),使用 /v1/market/kline 接口获取 10 年级别的清洗对齐历史数据。
如果你习惯用 AI 辅助开发:
在 AI 助手中搜索安装 tickdb-market-data SKILL,可以更便捷地接入 TickDB 数据服务。
数据来源:
- Kahneman, D., & Tversky, A. (1979). Prospect Theory: An Analysis of Decision under Risk. Econometrica.
- Tversky, A., & Kahneman, D. (1992). Advances in Prospect Theory: Cumulative Representation of Uncertainty. Journal of Risk and Uncertainty.
- Odean, T. (1998). Are Investors Reluctant to Realize Their Losses? The Journal of Finance.
- Frazzini, A. (2006). The Disposition Effect and Underreaction to News. The Journal of Finance.
- Thaler, R. (1985). Mental Accounting and Consumer Choice. Marketing Science.
- Tom, S. M., et al. (2007). The Neural Basis of Loss Aversion in Decision-Making Under Risk. Science.
回测局限性说明:上述回测结果基于历史数据模拟,不构成未来收益保证。回测中存在以下局限性:入场信号仅为演示目的设计(连续两天上涨),未考虑交易成本、滑点和市场冲击;样本量有限(2 年周期),统计显著性可能不足;未考虑极端行情下的流动性枯竭风险。建议在实际使用前进行更长时间跨度的验证。
风险提示:本文不构成任何投资建议。损失厌恶是认知偏差,不构成投资策略的依据。市场有风险,投资需谨慎。