tradusai/trading/paper_trading.py
2025-12-09 13:27:38 +08:00

868 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Paper Trading Module - 多周期独立仓位管理
支持三个独立周期的模拟交易:
- 短周期 (5m/15m/1h): short_term_5m_15m_1h / intraday
- 中周期 (4h/1d): medium_term_4h_1d / swing
- 长周期 (1d/1w): long_term_1d_1w
每个周期独立管理:
- 独立仓位
- 独立止盈止损
- 独立统计数据
- 独立权益曲线
"""
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List
from pathlib import Path
from dataclasses import dataclass, asdict, field
from enum import Enum
logger = logging.getLogger(__name__)
class TimeFrame(Enum):
"""交易周期"""
SHORT = "short" # 短周期 5m/15m/1h
MEDIUM = "medium" # 中周期 4h/1d
LONG = "long" # 长周期 1d/1w
TIMEFRAME_CONFIG = {
TimeFrame.SHORT: {
'name': '短周期',
'name_en': 'Short-term',
'signal_keys': ['short_term_5m_15m_1h', 'intraday'],
'leverage': 10,
'initial_balance': 10000.0, # 独立初始资金
'max_price_deviation': 0.001, # 0.1% - 短周期要求精准入场
},
TimeFrame.MEDIUM: {
'name': '中周期',
'name_en': 'Medium-term',
'signal_keys': ['medium_term_4h_1d', 'swing'],
'leverage': 10,
'initial_balance': 10000.0, # 独立初始资金
'max_price_deviation': 0.003, # 0.3% - 中周期适中容错
},
TimeFrame.LONG: {
'name': '长周期',
'name_en': 'Long-term',
'signal_keys': ['long_term_1d_1w'],
'leverage': 10,
'initial_balance': 10000.0, # 独立初始资金
'max_price_deviation': 0.005, # 0.5% - 长周期追求大趋势
},
}
# 金字塔加仓配置每次加仓的仓位比例总计100%
PYRAMID_LEVELS = [0.4, 0.3, 0.2, 0.1] # 首仓40%加仓30%、20%、10%
@dataclass
class PositionEntry:
"""单次入场记录"""
price: float
size: float # BTC 数量
margin: float # 本次占用保证金
timestamp: str
level: int # 金字塔层级 0=首仓, 1=加仓1, ...
def to_dict(self) -> dict:
return asdict(self)
@classmethod
def from_dict(cls, data: dict) -> 'PositionEntry':
return cls(**data)
@dataclass
class Position:
"""持仓信息(支持金字塔加仓)"""
side: str # LONG, SHORT, FLAT
entries: List['PositionEntry'] = field(default_factory=list) # 入场记录
stop_loss: float = 0.0
take_profit: float = 0.0
created_at: str = ""
signal_reasoning: str = ""
@property
def entry_price(self) -> float:
"""加权平均入场价"""
if not self.entries:
return 0.0
total_value = sum(e.price * e.size for e in self.entries)
total_size = sum(e.size for e in self.entries)
return total_value / total_size if total_size > 0 else 0.0
@property
def size(self) -> float:
"""总持仓数量"""
return sum(e.size for e in self.entries)
@property
def margin(self) -> float:
"""总占用保证金"""
return sum(e.margin for e in self.entries)
@property
def pyramid_level(self) -> int:
"""当前金字塔层级"""
return len(self.entries)
def to_dict(self) -> dict:
return {
'side': self.side,
'entry_price': self.entry_price,
'size': self.size,
'margin': self.margin,
'pyramid_level': self.pyramid_level,
'entries': [e.to_dict() for e in self.entries],
'stop_loss': self.stop_loss,
'take_profit': self.take_profit,
'created_at': self.created_at,
'signal_reasoning': self.signal_reasoning,
}
@classmethod
def from_dict(cls, data: dict) -> 'Position':
entries = [PositionEntry.from_dict(e) for e in data.get('entries', [])]
# 兼容旧数据格式
if not entries and data.get('entry_price') and data.get('size'):
entries = [PositionEntry(
price=data['entry_price'],
size=data['size'],
margin=data.get('margin', 0),
timestamp=data.get('created_at', ''),
level=0,
)]
return cls(
side=data['side'],
entries=entries,
stop_loss=data.get('stop_loss', 0),
take_profit=data.get('take_profit', 0),
created_at=data.get('created_at', ''),
signal_reasoning=data.get('signal_reasoning', ''),
)
@dataclass
class Trade:
"""交易记录"""
id: str
timeframe: str
side: str
entry_price: float
entry_time: str
exit_price: float
exit_time: str
size: float
pnl: float
pnl_pct: float
exit_reason: str
def to_dict(self) -> dict:
return asdict(self)
@classmethod
def from_dict(cls, data: dict) -> 'Trade':
return cls(**data)
@dataclass
class TimeFrameAccount:
"""单个周期的账户
资金结构:
- initial_balance: 初始本金
- realized_pnl: 已实现盈亏(平仓后累计)
- position.margin: 当前持仓占用保证金
- unrealized_pnl: 未实现盈亏(需实时计算)
账户权益 = initial_balance + realized_pnl + unrealized_pnl
可用余额 = initial_balance + realized_pnl - position.margin
"""
timeframe: str
initial_balance: float
leverage: int
realized_pnl: float = 0.0 # 已实现盈亏
position: Optional[Position] = None
trades: List[Trade] = field(default_factory=list)
stats: Dict = field(default_factory=dict)
equity_curve: List[Dict] = field(default_factory=list)
def __post_init__(self):
if not self.stats:
self.stats = self._init_stats()
def _init_stats(self) -> dict:
return {
'total_trades': 0,
'winning_trades': 0,
'losing_trades': 0,
'total_pnl': 0.0,
'max_drawdown': 0.0,
'peak_balance': self.initial_balance,
'win_rate': 0.0,
'avg_win': 0.0,
'avg_loss': 0.0,
'profit_factor': 0.0,
}
def get_used_margin(self) -> float:
"""获取已占用保证金"""
if self.position and self.position.side != 'FLAT':
return self.position.margin
return 0.0
def get_available_balance(self) -> float:
"""获取可用余额(可用于开新仓)"""
return self.initial_balance + self.realized_pnl - self.get_used_margin()
def get_equity(self, unrealized_pnl: float = 0.0) -> float:
"""获取账户权益(包含未实现盈亏)"""
return self.initial_balance + self.realized_pnl + unrealized_pnl
def to_dict(self) -> dict:
return {
'timeframe': self.timeframe,
'initial_balance': self.initial_balance,
'realized_pnl': self.realized_pnl,
'leverage': self.leverage,
'position': self.position.to_dict() if self.position else None,
'trades': [t.to_dict() for t in self.trades[-100:]],
'stats': self.stats,
'equity_curve': self.equity_curve[-500:],
}
@classmethod
def from_dict(cls, data: dict) -> 'TimeFrameAccount':
# 兼容旧数据格式
realized_pnl = data.get('realized_pnl', 0.0)
# 如果是旧格式,从 balance 推算 realized_pnl
if 'realized_pnl' not in data and 'balance' in data:
realized_pnl = data['balance'] - data['initial_balance']
account = cls(
timeframe=data['timeframe'],
initial_balance=data['initial_balance'],
leverage=data['leverage'],
realized_pnl=realized_pnl,
stats=data.get('stats', {}),
equity_curve=data.get('equity_curve', []),
)
if data.get('position'):
account.position = Position.from_dict(data['position'])
account.trades = [Trade.from_dict(t) for t in data.get('trades', [])]
return account
class MultiTimeframePaperTrader:
"""多周期模拟盘交易器"""
def __init__(
self,
initial_balance: float = 10000.0,
state_file: str = None
):
self.initial_balance = initial_balance
# 状态文件
if state_file:
self.state_file = Path(state_file)
else:
self.state_file = Path(__file__).parent.parent / 'output' / 'paper_trading_state.json'
# 初始化三个周期账户
self.accounts: Dict[TimeFrame, TimeFrameAccount] = {}
# 加载或初始化状态
self._load_state()
logger.info(f"Multi-timeframe Paper Trader initialized: total_balance=${initial_balance:.2f}")
def _load_state(self):
"""加载持久化状态"""
if self.state_file.exists():
try:
with open(self.state_file, 'r') as f:
state = json.load(f)
# 加载各周期账户
for tf in TimeFrame:
tf_data = state.get('accounts', {}).get(tf.value)
if tf_data:
self.accounts[tf] = TimeFrameAccount.from_dict(tf_data)
else:
self._init_account(tf)
logger.info(f"Loaded state from {self.state_file}")
except Exception as e:
logger.error(f"Failed to load state: {e}")
self._init_all_accounts()
else:
self._init_all_accounts()
def _init_all_accounts(self):
"""初始化所有账户"""
for tf in TimeFrame:
self._init_account(tf)
def _init_account(self, tf: TimeFrame):
"""初始化单个周期账户"""
config = TIMEFRAME_CONFIG[tf]
# 每个周期独立初始资金 10000 USD10倍杠杆最大仓位价值 100000 USD
self.accounts[tf] = TimeFrameAccount(
timeframe=tf.value,
initial_balance=config['initial_balance'],
leverage=config['leverage'],
realized_pnl=0.0,
)
def _save_state(self):
"""保存状态到文件"""
self.state_file.parent.mkdir(parents=True, exist_ok=True)
state = {
'accounts': {tf.value: acc.to_dict() for tf, acc in self.accounts.items()},
'last_updated': datetime.now().isoformat(),
}
with open(self.state_file, 'w') as f:
json.dump(state, f, indent=2, ensure_ascii=False)
def process_signal(self, signal: Dict[str, Any], current_price: float) -> Dict[str, Any]:
"""处理交易信号 - 检查所有周期"""
results = {
'timestamp': datetime.now().isoformat(),
'current_price': current_price,
'timeframes': {},
}
for tf in TimeFrame:
result = self._process_timeframe_signal(tf, signal, current_price)
results['timeframes'][tf.value] = result
self._save_state()
return results
def _process_timeframe_signal(
self, tf: TimeFrame, signal: Dict[str, Any], current_price: float
) -> Dict[str, Any]:
"""处理单个周期的信号"""
account = self.accounts[tf]
config = TIMEFRAME_CONFIG[tf]
result = {
'action': 'NONE',
'details': None,
}
# 更新权益曲线
self._update_equity_curve(tf, current_price)
# 1. 检查止盈止损
if account.position and account.position.side != 'FLAT':
close_result = self._check_close_position(tf, current_price)
if close_result:
result['action'] = 'CLOSE'
result['details'] = close_result
return result
# 2. 提取该周期的信号
tf_signal = self._extract_timeframe_signal(signal, config['signal_keys'])
if not tf_signal or not tf_signal.get('exists'):
result['action'] = 'NO_SIGNAL'
return result
direction = tf_signal.get('direction')
if not direction:
result['action'] = 'NO_SIGNAL'
return result
signal_stop_loss = tf_signal.get('stop_loss', 0)
signal_take_profit = tf_signal.get('take_profit', 0)
signal_entry_price = tf_signal.get('entry_price', 0)
# 验证止盈止损
if signal_stop_loss <= 0 or signal_take_profit <= 0:
result['action'] = 'NO_SIGNAL'
result['details'] = {'reason': '缺少有效止盈止损'}
return result
# 检查价格偏差:当前价格与建议入场价偏差超过阈值则不开仓
max_deviation = config.get('max_price_deviation', 0.002)
if signal_entry_price > 0:
price_deviation = abs(current_price - signal_entry_price) / signal_entry_price
if price_deviation > max_deviation:
result['action'] = 'PRICE_DEVIATION'
result['details'] = {
'reason': f'价格偏差过大: {price_deviation*100:.2f}% > {max_deviation*100:.1f}%',
'signal_entry': signal_entry_price,
'current_price': current_price,
'deviation_pct': price_deviation * 100,
'max_deviation_pct': max_deviation * 100,
}
logger.info(
f"[{config['name']}] 跳过开仓: 价格偏差 {price_deviation*100:.2f}% > {max_deviation*100:.1f}% "
f"(信号价: ${signal_entry_price:.2f}, 当前价: ${current_price:.2f})"
)
return result
# 3. 如果有持仓
if account.position and account.position.side != 'FLAT':
# 反向信号:只平仓不开反向仓
if (account.position.side == 'LONG' and direction == 'SHORT') or \
(account.position.side == 'SHORT' and direction == 'LONG'):
close_result = self._close_position(tf, current_price, 'SIGNAL_REVERSE')
result['action'] = 'CLOSE'
result['details'] = close_result
logger.info(
f"[{config['name']}] 反向信号平仓,等待下一周期新信号"
)
return result
else:
# 同方向信号:尝试金字塔加仓
add_result = self._add_position(
tf, current_price,
signal_stop_loss, signal_take_profit,
tf_signal.get('reasoning', '')[:100]
)
if add_result:
result['action'] = 'ADD'
result['details'] = add_result
else:
# 已达到最大仓位,保持持仓
result['action'] = 'HOLD'
result['details'] = {
'position': account.position.to_dict(),
'unrealized_pnl': self._calc_unrealized_pnl(tf, current_price),
'reason': '已达最大仓位层级',
}
return result
# 4. 无持仓,开新仓(首仓)
open_result = self._open_position(
tf, direction, current_price,
signal_stop_loss, signal_take_profit,
tf_signal.get('reasoning', '')[:100]
)
if open_result:
result['action'] = 'OPEN'
result['details'] = open_result
else:
result['action'] = 'WAIT'
return result
def _extract_timeframe_signal(
self, signal: Dict[str, Any], signal_keys: List[str]
) -> Optional[Dict[str, Any]]:
"""提取特定周期的信号"""
try:
# 从 llm_signal.opportunities 中提取
llm_signal = signal.get('llm_signal') or signal.get('aggregated_signal', {}).get('llm_signal')
if llm_signal and isinstance(llm_signal, dict):
opportunities = llm_signal.get('opportunities', {})
for key in signal_keys:
if key in opportunities and opportunities[key]:
return opportunities[key]
# 备选路径
agg = signal.get('aggregated_signal', {})
if agg:
llm = agg.get('llm_signal', {})
if llm:
opps = llm.get('opportunities', {})
for key in signal_keys:
if key in opps and opps[key]:
return opps[key]
return None
except Exception as e:
logger.error(f"Error extracting signal: {e}")
return None
def _get_max_position_value(self, tf: TimeFrame) -> float:
"""获取最大仓位价值(本金 × 杠杆)"""
account = self.accounts[tf]
return account.initial_balance * account.leverage
def _get_current_position_value(self, tf: TimeFrame, current_price: float) -> float:
"""获取当前仓位价值"""
account = self.accounts[tf]
if not account.position or account.position.side == 'FLAT':
return 0.0
return account.position.size * current_price
def _open_position(
self, tf: TimeFrame, direction: str, price: float,
stop_loss: float, take_profit: float, reasoning: str
) -> Optional[Dict]:
"""开首仓(金字塔第一层)"""
account = self.accounts[tf]
config = TIMEFRAME_CONFIG[tf]
# 计算首仓仓位:最大仓位 × 首仓比例
max_position_value = self._get_max_position_value(tf)
first_level_ratio = PYRAMID_LEVELS[0] # 40%
position_value = max_position_value * first_level_ratio
margin = position_value / account.leverage
size = position_value / price
# 检查可用余额是否足够
available_balance = account.get_available_balance()
if available_balance < margin:
logger.warning(f"[{config['name']}] 可用余额不足: ${available_balance:.2f} < ${margin:.2f}")
return None
if size <= 0:
return None
# 创建首仓入场记录
entry = PositionEntry(
price=price,
size=size,
margin=margin,
timestamp=datetime.now().isoformat(),
level=0,
)
account.position = Position(
side=direction,
entries=[entry],
stop_loss=stop_loss,
take_profit=take_profit,
created_at=datetime.now().isoformat(),
signal_reasoning=reasoning,
)
logger.info(
f"[{config['name']}] OPEN {direction} [L1/{len(PYRAMID_LEVELS)}]: price=${price:.2f}, "
f"size={size:.6f} BTC, margin=${margin:.2f}, value=${position_value:.2f}, "
f"SL=${stop_loss:.2f}, TP=${take_profit:.2f}"
)
return {
'timeframe': tf.value,
'side': direction,
'entry_price': price,
'size': size,
'margin': margin,
'position_value': position_value,
'pyramid_level': 1,
'max_levels': len(PYRAMID_LEVELS),
'stop_loss': stop_loss,
'take_profit': take_profit,
}
def _add_position(
self, tf: TimeFrame, price: float,
stop_loss: float, take_profit: float, reasoning: str
) -> Optional[Dict]:
"""金字塔加仓"""
account = self.accounts[tf]
config = TIMEFRAME_CONFIG[tf]
pos = account.position
if not pos or pos.side == 'FLAT':
return None
# 检查是否已达最大层级
current_level = pos.pyramid_level
if current_level >= len(PYRAMID_LEVELS):
logger.info(f"[{config['name']}] 已达最大仓位层级 {current_level}/{len(PYRAMID_LEVELS)}")
return None
# 计算加仓仓位
max_position_value = self._get_max_position_value(tf)
level_ratio = PYRAMID_LEVELS[current_level]
add_position_value = max_position_value * level_ratio
add_margin = add_position_value / account.leverage
add_size = add_position_value / price
# 检查可用余额
available_balance = account.get_available_balance()
if available_balance < add_margin:
logger.warning(
f"[{config['name']}] 加仓余额不足: ${available_balance:.2f} < ${add_margin:.2f}"
)
return None
# 添加入场记录
entry = PositionEntry(
price=price,
size=add_size,
margin=add_margin,
timestamp=datetime.now().isoformat(),
level=current_level,
)
pos.entries.append(entry)
# 更新止盈止损
pos.stop_loss = stop_loss
pos.take_profit = take_profit
new_level = pos.pyramid_level
logger.info(
f"[{config['name']}] ADD {pos.side} [L{new_level}/{len(PYRAMID_LEVELS)}]: price=${price:.2f}, "
f"add_size={add_size:.6f} BTC, add_margin=${add_margin:.2f}, "
f"total_size={pos.size:.6f} BTC, total_margin=${pos.margin:.2f}, "
f"avg_price=${pos.entry_price:.2f}"
)
return {
'timeframe': tf.value,
'side': pos.side,
'add_price': price,
'add_size': add_size,
'add_margin': add_margin,
'add_position_value': add_position_value,
'total_size': pos.size,
'total_margin': pos.margin,
'avg_entry_price': pos.entry_price,
'pyramid_level': new_level,
'max_levels': len(PYRAMID_LEVELS),
'stop_loss': stop_loss,
'take_profit': take_profit,
}
def _check_close_position(self, tf: TimeFrame, current_price: float) -> Optional[Dict]:
"""检查是否触发止盈止损"""
account = self.accounts[tf]
pos = account.position
if not pos or pos.side == 'FLAT':
return None
if pos.side == 'LONG':
if current_price >= pos.take_profit:
return self._close_position(tf, current_price, 'TAKE_PROFIT')
elif current_price <= pos.stop_loss:
return self._close_position(tf, current_price, 'STOP_LOSS')
else: # SHORT
if current_price <= pos.take_profit:
return self._close_position(tf, current_price, 'TAKE_PROFIT')
elif current_price >= pos.stop_loss:
return self._close_position(tf, current_price, 'STOP_LOSS')
return None
def _close_position(self, tf: TimeFrame, price: float, reason: str) -> Dict:
"""平仓"""
account = self.accounts[tf]
config = TIMEFRAME_CONFIG[tf]
pos = account.position
if not pos or pos.side == 'FLAT':
return {'error': 'No position'}
# 计算盈亏(基于保证金的收益率)
if pos.side == 'LONG':
# 做多:(卖出价 - 买入价) * 数量
pnl = (price - pos.entry_price) * pos.size
else:
# 做空:(买入价 - 卖出价) * 数量
pnl = (pos.entry_price - price) * pos.size
# 收益率 = 盈亏 / 保证金 * 100
pnl_pct = (pnl / pos.margin * 100) if pos.margin > 0 else 0
# 更新已实现盈亏(保证金释放 + 盈亏结算)
account.realized_pnl += pnl
# 记录交易
trade = Trade(
id=f"{tf.value[0].upper()}{len(account.trades)+1:04d}",
timeframe=tf.value,
side=pos.side,
entry_price=pos.entry_price,
entry_time=pos.created_at,
exit_price=price,
exit_time=datetime.now().isoformat(),
size=pos.size,
pnl=pnl,
pnl_pct=pnl_pct,
exit_reason=reason,
)
account.trades.append(trade)
self._update_stats(tf, trade)
# 计算新的账户权益
new_equity = account.get_equity()
result = {
'timeframe': tf.value,
'side': pos.side,
'entry_price': pos.entry_price,
'exit_price': price,
'size': pos.size,
'margin': pos.margin,
'pnl': pnl,
'pnl_pct': pnl_pct,
'reason': reason,
'new_equity': new_equity,
'realized_pnl': account.realized_pnl,
}
logger.info(
f"[{config['name']}] CLOSE {pos.side}: entry=${pos.entry_price:.2f}, "
f"exit=${price:.2f}, PnL=${pnl:.2f} ({pnl_pct:.2f}%), reason={reason}, "
f"equity=${new_equity:.2f}"
)
account.position = None
return result
def _calc_unrealized_pnl(self, tf: TimeFrame, current_price: float) -> Dict[str, float]:
"""计算未实现盈亏"""
account = self.accounts[tf]
pos = account.position
if not pos or pos.side == 'FLAT':
return {'pnl': 0, 'pnl_pct': 0}
# 计算未实现盈亏
if pos.side == 'LONG':
pnl = (current_price - pos.entry_price) * pos.size
else:
pnl = (pos.entry_price - current_price) * pos.size
# 收益率 = 盈亏 / 保证金 * 100
pnl_pct = (pnl / pos.margin * 100) if pos.margin > 0 else 0
return {'pnl': pnl, 'pnl_pct': pnl_pct}
def _update_equity_curve(self, tf: TimeFrame, current_price: float):
"""更新权益曲线"""
account = self.accounts[tf]
unrealized = self._calc_unrealized_pnl(tf, current_price)
equity = account.get_equity(unrealized['pnl'])
account.equity_curve.append({
'timestamp': datetime.now().isoformat(),
'equity': equity,
'initial_balance': account.initial_balance,
'realized_pnl': account.realized_pnl,
'unrealized_pnl': unrealized['pnl'],
'price': current_price,
})
def _update_stats(self, tf: TimeFrame, trade: Trade):
"""更新统计数据"""
account = self.accounts[tf]
stats = account.stats
stats['total_trades'] += 1
stats['total_pnl'] += trade.pnl
if trade.pnl > 0:
stats['winning_trades'] += 1
else:
stats['losing_trades'] += 1
if stats['total_trades'] > 0:
stats['win_rate'] = stats['winning_trades'] / stats['total_trades'] * 100
wins = [t for t in account.trades if t.pnl > 0]
losses = [t for t in account.trades if t.pnl <= 0]
if wins:
stats['avg_win'] = sum(t.pnl for t in wins) / len(wins)
if losses:
stats['avg_loss'] = sum(t.pnl for t in losses) / len(losses)
if stats['avg_loss'] != 0:
stats['profit_factor'] = abs(stats['avg_win'] / stats['avg_loss'])
# 更新峰值和回撤(基于账户权益)
equity = account.get_equity()
if equity > stats['peak_balance']:
stats['peak_balance'] = equity
drawdown = (stats['peak_balance'] - equity) / stats['peak_balance'] * 100
if drawdown > stats['max_drawdown']:
stats['max_drawdown'] = drawdown
def get_status(self, current_price: float = None) -> Dict[str, Any]:
"""获取所有周期状态"""
total_equity = 0
total_initial = 0
total_realized_pnl = 0
total_unrealized_pnl = 0
# 先计算各周期数据
timeframes_data = {}
for tf in TimeFrame:
account = self.accounts[tf]
config = TIMEFRAME_CONFIG[tf]
# 计算未实现盈亏
unrealized = self._calc_unrealized_pnl(tf, current_price) if current_price else {'pnl': 0, 'pnl_pct': 0}
equity = account.get_equity(unrealized['pnl'])
total_initial += account.initial_balance
total_realized_pnl += account.realized_pnl
total_unrealized_pnl += unrealized['pnl']
total_equity += equity
# 收益率 = (权益 - 初始本金) / 初始本金
return_pct = (equity - account.initial_balance) / account.initial_balance * 100 if account.initial_balance > 0 else 0
tf_status = {
'name': config['name'],
'name_en': config['name_en'],
'initial_balance': account.initial_balance,
'realized_pnl': account.realized_pnl,
'unrealized_pnl': unrealized['pnl'],
'equity': equity,
'available_balance': account.get_available_balance(),
'used_margin': account.get_used_margin(),
'return_pct': return_pct,
'leverage': account.leverage,
'position': None,
'stats': account.stats,
'recent_trades': [t.to_dict() for t in account.trades[-10:]],
'equity_curve': account.equity_curve[-100:],
}
if account.position and account.position.side != 'FLAT':
pos_dict = account.position.to_dict()
if current_price:
pos_dict['current_price'] = current_price
pos_dict['unrealized_pnl'] = unrealized['pnl']
pos_dict['unrealized_pnl_pct'] = unrealized['pnl_pct']
tf_status['position'] = pos_dict
timeframes_data[tf.value] = tf_status
# 总收益率
total_return = (total_equity - total_initial) / total_initial * 100 if total_initial > 0 else 0
status = {
'timestamp': datetime.now().isoformat(),
'total_initial_balance': total_initial,
'total_realized_pnl': total_realized_pnl,
'total_unrealized_pnl': total_unrealized_pnl,
'total_equity': total_equity,
'total_return': total_return,
'timeframes': timeframes_data,
}
return status
def reset(self):
"""重置所有账户"""
self._init_all_accounts()
self._save_state()
logger.info("All accounts reset")
# 兼容旧的 PaperTrader 接口
PaperTrader = MultiTimeframePaperTrader