This commit is contained in:
aaron 2025-12-09 13:27:38 +08:00
parent 54e33e7fff
commit 143628e793
4 changed files with 507 additions and 114 deletions

View File

@ -0,0 +1,68 @@
{
"accounts": {
"short": {
"timeframe": "short",
"initial_balance": 10000.0,
"realized_pnl": 0.0,
"leverage": 10,
"position": null,
"trades": [],
"stats": {
"total_trades": 0,
"winning_trades": 0,
"losing_trades": 0,
"total_pnl": 0.0,
"max_drawdown": 0.0,
"peak_balance": 10000.0,
"win_rate": 0.0,
"avg_win": 0.0,
"avg_loss": 0.0,
"profit_factor": 0.0
},
"equity_curve": []
},
"medium": {
"timeframe": "medium",
"initial_balance": 10000.0,
"realized_pnl": 0.0,
"leverage": 10,
"position": null,
"trades": [],
"stats": {
"total_trades": 0,
"winning_trades": 0,
"losing_trades": 0,
"total_pnl": 0.0,
"max_drawdown": 0.0,
"peak_balance": 10000.0,
"win_rate": 0.0,
"avg_win": 0.0,
"avg_loss": 0.0,
"profit_factor": 0.0
},
"equity_curve": []
},
"long": {
"timeframe": "long",
"initial_balance": 10000.0,
"realized_pnl": 0.0,
"leverage": 10,
"position": null,
"trades": [],
"stats": {
"total_trades": 0,
"winning_trades": 0,
"losing_trades": 0,
"total_pnl": 0.0,
"max_drawdown": 0.0,
"peak_balance": 10000.0,
"win_rate": 0.0,
"avg_win": 0.0,
"avg_loss": 0.0,
"profit_factor": 0.0
},
"equity_curve": []
}
},
"last_updated": "2025-12-09T13:24:30.616776"
}

View File

@ -37,6 +37,7 @@ TIMEFRAME_CONFIG = {
'signal_keys': ['short_term_5m_15m_1h', 'intraday'],
'leverage': 10,
'initial_balance': 10000.0, # 独立初始资金
'max_price_deviation': 0.001, # 0.1% - 短周期要求精准入场
},
TimeFrame.MEDIUM: {
'name': '中周期',
@ -44,6 +45,7 @@ TIMEFRAME_CONFIG = {
'signal_keys': ['medium_term_4h_1d', 'swing'],
'leverage': 10,
'initial_balance': 10000.0, # 独立初始资金
'max_price_deviation': 0.003, # 0.3% - 中周期适中容错
},
TimeFrame.LONG: {
'name': '长周期',
@ -51,29 +53,101 @@ TIMEFRAME_CONFIG = {
'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 Position:
"""持仓信息"""
side: str # LONG, SHORT, FLAT
entry_price: float = 0.0
size: float = 0.0 # BTC 数量
stop_loss: float = 0.0
take_profit: float = 0.0
created_at: str = ""
signal_reasoning: str = ""
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) -> 'Position':
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:
"""交易记录"""
@ -99,11 +173,21 @@ class Trade:
@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
balance: float
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)
@ -127,11 +211,25 @@ class TimeFrameAccount:
'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,
'balance': self.balance,
'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:]],
@ -141,11 +239,17 @@ class TimeFrameAccount:
@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'],
balance=data['balance'],
initial_balance=data['initial_balance'],
leverage=data['leverage'],
realized_pnl=realized_pnl,
stats=data.get('stats', {}),
equity_curve=data.get('equity_curve', []),
)
@ -210,12 +314,11 @@ class MultiTimeframePaperTrader:
"""初始化单个周期账户"""
config = TIMEFRAME_CONFIG[tf]
# 每个周期独立初始资金 10000 USD10倍杠杆最大仓位价值 100000 USD
account_balance = config['initial_balance']
self.accounts[tf] = TimeFrameAccount(
timeframe=tf.value,
balance=account_balance,
initial_balance=account_balance,
initial_balance=config['initial_balance'],
leverage=config['leverage'],
realized_pnl=0.0,
)
def _save_state(self):
@ -282,6 +385,7 @@ class MultiTimeframePaperTrader:
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:
@ -289,33 +393,58 @@ class MultiTimeframePaperTrader:
result['details'] = {'reason': '缺少有效止盈止损'}
return result
# 3. 如果有反向持仓,先平仓
# 检查价格偏差:当前价格与建议入场价偏差超过阈值则不开仓
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'] = 'REVERSE'
result['details'] = {'close': close_result}
# 开反向仓
open_result = self._open_position(
tf, direction, current_price,
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 open_result:
result['details']['open'] = open_result
return result
else:
# 同方向,保持持仓
result['action'] = 'HOLD'
result['details'] = {
'position': account.position.to_dict(),
'unrealized_pnl': self._calc_unrealized_pnl(tf, current_price),
}
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. 无持仓,开新仓
# 4. 无持仓,开新仓(首仓)
open_result = self._open_position(
tf, direction, current_price,
signal_stop_loss, signal_take_profit,
@ -359,25 +488,54 @@ class MultiTimeframePaperTrader:
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]
# 计算仓位大小: 全部余额 * 杠杆倍数 (10000 * 10 = 100000 USD 仓位价值)
position_value = account.balance * account.leverage
# 计算首仓仓位:最大仓位 × 首仓比例
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,
entry_price=price,
size=size,
entries=[entry],
stop_loss=stop_loss,
take_profit=take_profit,
created_at=datetime.now().isoformat(),
@ -385,8 +543,9 @@ class MultiTimeframePaperTrader:
)
logger.info(
f"[{config['name']}] OPEN {direction}: price=${price:.2f}, "
f"size={size:.6f} BTC, SL=${stop_loss:.2f}, TP=${take_profit:.2f}"
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 {
@ -394,6 +553,81 @@ class MultiTimeframePaperTrader:
'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,
}
@ -428,16 +662,19 @@ class MultiTimeframePaperTrader:
if not pos or pos.side == 'FLAT':
return {'error': 'No position'}
# 计算盈亏
# 计算盈亏(基于保证金的收益率)
if pos.side == 'LONG':
pnl_pct = (price - pos.entry_price) / pos.entry_price * 100 * account.leverage
# 做多:(卖出价 - 买入价) * 数量
pnl = (price - pos.entry_price) * pos.size
else:
pnl_pct = (pos.entry_price - price) / pos.entry_price * 100 * account.leverage
# 做空:(买入价 - 卖出价) * 数量
pnl = (pos.entry_price - price) * pos.size
position_value = pos.size * pos.entry_price
pnl = position_value * (pnl_pct / 100)
# 收益率 = 盈亏 / 保证金 * 100
pnl_pct = (pnl / pos.margin * 100) if pos.margin > 0 else 0
account.balance += pnl
# 更新已实现盈亏(保证金释放 + 盈亏结算)
account.realized_pnl += pnl
# 记录交易
trade = Trade(
@ -456,21 +693,27 @@ class MultiTimeframePaperTrader:
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_balance': account.balance,
'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"exit=${price:.2f}, PnL=${pnl:.2f} ({pnl_pct:.2f}%), reason={reason}, "
f"equity=${new_equity:.2f}"
)
account.position = None
@ -484,29 +727,29 @@ class MultiTimeframePaperTrader:
if not pos or pos.side == 'FLAT':
return {'pnl': 0, 'pnl_pct': 0}
# 计算未实现盈亏
if pos.side == 'LONG':
pnl_pct = (current_price - pos.entry_price) / pos.entry_price * 100 * account.leverage
pnl = (current_price - pos.entry_price) * pos.size
else:
pnl_pct = (pos.entry_price - current_price) / pos.entry_price * 100 * account.leverage
pnl = (pos.entry_price - current_price) * pos.size
position_value = pos.size * pos.entry_price
pnl = position_value * (pnl_pct / 100)
# 收益率 = 盈亏 / 保证金 * 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]
equity = account.balance
if account.position and account.position.side != 'FLAT':
unrealized = self._calc_unrealized_pnl(tf, current_price)
equity += unrealized['pnl']
unrealized = self._calc_unrealized_pnl(tf, current_price)
equity = account.get_equity(unrealized['pnl'])
account.equity_curve.append({
'timestamp': datetime.now().isoformat(),
'equity': equity,
'balance': account.balance,
'initial_balance': account.initial_balance,
'realized_pnl': account.realized_pnl,
'unrealized_pnl': unrealized['pnl'],
'price': current_price,
})
@ -537,36 +780,50 @@ class MultiTimeframePaperTrader:
if stats['avg_loss'] != 0:
stats['profit_factor'] = abs(stats['avg_win'] / stats['avg_loss'])
if account.balance > stats['peak_balance']:
stats['peak_balance'] = account.balance
# 更新峰值和回撤(基于账户权益)
equity = account.get_equity()
if equity > stats['peak_balance']:
stats['peak_balance'] = equity
drawdown = (stats['peak_balance'] - account.balance) / stats['peak_balance'] * 100
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_balance = sum(acc.balance for acc in self.accounts.values())
total_initial = sum(acc.initial_balance for acc in self.accounts.values())
status = {
'timestamp': datetime.now().isoformat(),
'total_balance': total_balance,
'total_initial_balance': total_initial,
'total_return': (total_balance - total_initial) / total_initial * 100,
'timeframes': {},
}
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'],
'balance': account.balance,
'initial_balance': account.initial_balance,
'return_pct': (account.balance - account.initial_balance) / account.initial_balance * 100,
'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,
@ -577,13 +834,25 @@ class MultiTimeframePaperTrader:
if account.position and account.position.side != 'FLAT':
pos_dict = account.position.to_dict()
if current_price:
unrealized = self._calc_unrealized_pnl(tf, 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
status['timeframes'][tf.value] = tf_status
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

View File

@ -61,11 +61,11 @@ def load_trading_state() -> Dict[str, Any]:
}
def _default_account(timeframe: str, balance: float) -> Dict:
def _default_account(timeframe: str, initial_balance: float) -> Dict:
return {
'timeframe': timeframe,
'balance': balance,
'initial_balance': balance,
'initial_balance': initial_balance,
'realized_pnl': 0.0,
'leverage': 10, # 所有周期统一 10 倍杠杆
'position': None,
'trades': [],
@ -75,7 +75,7 @@ def _default_account(timeframe: str, balance: float) -> Dict:
'losing_trades': 0,
'total_pnl': 0.0,
'max_drawdown': 0.0,
'peak_balance': balance,
'peak_balance': initial_balance,
'win_rate': 0.0,
},
'equity_curve': [],
@ -108,33 +108,55 @@ async def get_status():
state = load_trading_state()
accounts = state.get('accounts', {})
# 计算总余额
total_balance = sum(acc.get('balance', 0) for acc in accounts.values())
total_initial = sum(acc.get('initial_balance', 0) for acc in accounts.values())
total_return = (total_balance - total_initial) / total_initial * 100 if total_initial > 0 else 0
total_initial = 0
total_realized_pnl = 0
total_equity = 0
# 构建各周期状态
timeframes = {}
for tf_key, acc in accounts.items():
initial = acc.get('initial_balance', 0)
balance = acc.get('balance', 0)
return_pct = (balance - initial) / initial * 100 if initial > 0 else 0
realized_pnl = acc.get('realized_pnl', 0)
# 兼容旧数据格式
if 'realized_pnl' not in acc and 'balance' in acc:
realized_pnl = acc['balance'] - initial
# 计算权益(不含未实现盈亏,因为 API 没有实时价格)
equity = initial + realized_pnl
# 检查持仓的保证金
position = acc.get('position')
used_margin = position.get('margin', 0) if position else 0
available_balance = equity - used_margin
total_initial += initial
total_realized_pnl += realized_pnl
total_equity += equity
return_pct = (equity - initial) / initial * 100 if initial > 0 else 0
timeframes[tf_key] = {
'name': '短周期' if tf_key == 'short' else '中周期' if tf_key == 'medium' else '长周期',
'name_en': 'Short-term' if tf_key == 'short' else 'Medium-term' if tf_key == 'medium' else 'Long-term',
'balance': balance,
'initial_balance': initial,
'realized_pnl': realized_pnl,
'equity': equity,
'available_balance': available_balance,
'used_margin': used_margin,
'return_pct': return_pct,
'leverage': acc.get('leverage', 1),
'position': acc.get('position'),
'leverage': acc.get('leverage', 10),
'position': position,
'stats': acc.get('stats', {}),
}
total_return = (total_equity - total_initial) / total_initial * 100 if total_initial > 0 else 0
return {
'timestamp': datetime.now().isoformat(),
'total_balance': total_balance,
'total_initial_balance': total_initial,
'total_realized_pnl': total_realized_pnl,
'total_equity': total_equity,
'total_return': total_return,
'timeframes': timeframes,
'last_updated': state.get('last_updated'),

View File

@ -288,16 +288,25 @@
if (!state || !state.accounts) return;
const accounts = state.accounts;
let totalBalance = 0, totalInitial = 0;
let totalInitial = 0, totalEquity = 0, totalRealizedPnl = 0, totalUnrealizedPnl = 0;
for (const [tf, acc] of Object.entries(accounts)) {
totalBalance += acc.balance || 0;
totalInitial += acc.initial_balance || 0;
const initial = acc.initial_balance || 0;
const realizedPnl = acc.realized_pnl || 0;
// 兼容旧数据
const equity = acc.equity || (acc.balance || initial + realizedPnl);
const unrealizedPnl = acc.position?.unrealized_pnl || 0;
totalInitial += initial;
totalRealizedPnl += realizedPnl;
totalUnrealizedPnl += unrealizedPnl;
totalEquity += equity + unrealizedPnl;
updateTimeframeCard(tf, acc);
}
const totalReturn = totalInitial > 0 ? (totalBalance - totalInitial) / totalInitial * 100 : 0;
document.getElementById('total-balance').textContent = `$${totalBalance.toLocaleString('en-US', {minimumFractionDigits: 2})}`;
const totalReturn = totalInitial > 0 ? (totalEquity - totalInitial) / totalInitial * 100 : 0;
document.getElementById('total-balance').textContent = `$${totalEquity.toLocaleString('en-US', {minimumFractionDigits: 2})}`;
const returnEl = document.getElementById('total-return');
returnEl.textContent = `${totalReturn >= 0 ? '+' : ''}${totalReturn.toFixed(2)}%`;
returnEl.className = `text-xl font-bold ${totalReturn > 0 ? 'text-success' : totalReturn < 0 ? 'text-danger' : 'text-slate-400'}`;
@ -315,13 +324,17 @@
const card = document.getElementById(`tf-${tf}`);
if (!card) return;
const balance = acc.balance || 0;
const initial = acc.initial_balance || 0;
const returnPct = initial > 0 ? (balance - initial) / initial * 100 : 0;
const stats = acc.stats || {};
const realizedPnl = acc.realized_pnl || 0;
const position = acc.position;
const unrealizedPnl = position?.unrealized_pnl || 0;
card.querySelector('.tf-balance').textContent = `$${balance.toLocaleString('en-US', {minimumFractionDigits: 2})}`;
// 权益 = 初始本金 + 已实现盈亏 + 未实现盈亏
const equity = initial + realizedPnl + unrealizedPnl;
const returnPct = initial > 0 ? (equity - initial) / initial * 100 : 0;
const stats = acc.stats || {};
card.querySelector('.tf-balance').textContent = `$${equity.toLocaleString('en-US', {minimumFractionDigits: 2})}`;
const returnEl = card.querySelector('.tf-return');
returnEl.textContent = `${returnPct >= 0 ? '+' : ''}${returnPct.toFixed(2)}%`;
@ -340,18 +353,23 @@
const unrealized = position.unrealized_pnl || 0;
const unrealizedPct = position.unrealized_pnl_pct || 0;
const pyramidLevel = position.pyramid_level || 1;
const pnlColor = unrealized >= 0 ? 'text-success' : 'text-danger';
posInfo.innerHTML = `
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">Entry</span>
<span class="text-slate-500">Entry <span class="text-slate-600">(L${pyramidLevel}/4)</span></span>
<span class="text-white font-mono">$${(position.entry_price || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
</div>
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">SL / TP</span>
<span class="font-mono"><span class="text-danger">$${(position.stop_loss || 0).toFixed(0)}</span> / <span class="text-success">$${(position.take_profit || 0).toFixed(0)}</span></span>
<span class="text-slate-500">Stop Loss</span>
<span class="text-danger font-mono">$${(position.stop_loss || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
</div>
<div class="flex justify-between text-xs">
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">Take Profit</span>
<span class="text-success font-mono">$${(position.take_profit || 0).toLocaleString('en-US', {minimumFractionDigits: 2})}</span>
</div>
<div class="flex justify-between text-xs pt-1 border-t border-slate-700/50">
<span class="text-slate-500">PnL</span>
<span class="${pnlColor} font-mono font-medium">${unrealized >= 0 ? '+' : ''}$${Math.abs(unrealized).toFixed(2)} (${unrealizedPct >= 0 ? '+' : ''}${unrealizedPct.toFixed(1)}%)</span>
</div>
@ -360,8 +378,15 @@
} else {
badge.className = 'badge badge-flat tf-position-badge';
badge.textContent = 'FLAT';
posInfo.innerHTML = '<div class="text-slate-500 text-sm text-center">No position</div>';
posInfo.className = 'tf-position-info bg-slate-800/30 rounded-lg p-3 text-center';
// 显示已实现盈亏
const realizedColor = realizedPnl >= 0 ? 'text-success' : 'text-danger';
posInfo.innerHTML = `
<div class="text-center">
<div class="text-slate-500 text-xs mb-1">Realized PnL</div>
<div class="${realizedColor} font-mono text-sm">${realizedPnl >= 0 ? '+' : ''}$${realizedPnl.toFixed(2)}</div>
</div>
`;
posInfo.className = 'tf-position-info bg-slate-800/30 rounded-lg p-3';
}
}
@ -389,23 +414,29 @@
if (opp && opp.exists && opp.direction) {
const isLong = opp.direction === 'LONG';
const confidence = opp.confidence || opp.confidence_score || 0;
const reasoning = opp.reasoning || '';
el.innerHTML = `
<div class="flex items-center justify-between mb-2">
<div class="flex items-center justify-between mb-3">
<span class="text-slate-400 text-xs uppercase">${tfName}</span>
<span class="badge ${isLong ? 'badge-long' : 'badge-short'}">${opp.direction}</span>
<div class="flex items-center gap-2">
${confidence ? `<span class="text-xs text-slate-500">Conf: ${(confidence * 100).toFixed(0)}%</span>` : ''}
<span class="badge ${isLong ? 'badge-long' : 'badge-short'}">${opp.direction}</span>
</div>
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<div><span class="text-slate-500">Entry</span><div class="text-white font-mono">$${(opp.entry_price || 0).toFixed(0)}</div></div>
<div><span class="text-slate-500">SL</span><div class="text-danger font-mono">$${(opp.stop_loss || 0).toFixed(0)}</div></div>
<div><span class="text-slate-500">TP</span><div class="text-success font-mono">$${(opp.take_profit || 0).toFixed(0)}</div></div>
<div class="grid grid-cols-3 gap-2 text-xs mb-3">
<div><span class="text-slate-500">Entry</span><div class="text-white font-mono">$${(opp.entry_price || 0).toLocaleString()}</div></div>
<div><span class="text-slate-500">SL</span><div class="text-danger font-mono">$${(opp.stop_loss || 0).toLocaleString()}</div></div>
<div><span class="text-slate-500">TP</span><div class="text-success font-mono">$${(opp.take_profit || 0).toLocaleString()}</div></div>
</div>
${reasoning ? `<div class="text-xs text-slate-400 border-t border-slate-700/50 pt-2 mt-2 leading-relaxed">${reasoning}</div>` : ''}
`;
el.className = `bg-slate-800/50 rounded-lg p-4 border ${isLong ? 'border-success/30' : 'border-danger/30'}`;
} else {
const reason = opp?.reasoning || 'No opportunity';
el.innerHTML = `
<div class="text-slate-400 text-xs uppercase mb-2">${tfName}</div>
<div class="text-slate-500 text-sm">${reason.length > 60 ? reason.substring(0, 60) + '...' : reason}</div>
<div class="text-slate-500 text-sm leading-relaxed">${reason}</div>
`;
el.className = 'bg-slate-800/50 rounded-lg p-4';
}
@ -463,8 +494,11 @@
const state = { accounts: {} };
for (const [tf, data] of Object.entries(status.timeframes)) {
state.accounts[tf] = {
balance: data.balance,
initial_balance: data.initial_balance,
realized_pnl: data.realized_pnl || 0,
equity: data.equity || data.initial_balance,
available_balance: data.available_balance,
used_margin: data.used_margin,
leverage: data.leverage,
position: data.position,
stats: data.stats,