This commit is contained in:
aaron 2025-12-09 12:57:31 +08:00
parent 6813a4abe0
commit 54e33e7fff
5 changed files with 937 additions and 1419 deletions

View File

@ -1,18 +0,0 @@
{
"balance": 10000.0,
"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
},
"last_updated": "2025-12-09T12:02:05.263984"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,41 @@
"""
Realtime Paper Trading - 基于 WebSocket 实时数据的模拟盘
Realtime Trading - 基于 WebSocket 实时数据的多周期交易
使用 Binance WebSocket 获取实时价格结合信号进行模拟交易
支持仓位管理金字塔加仓最大持仓限制部分止盈
使用 Binance WebSocket 获取实时价格结合信号进行多周期独立交易
- 短周期 (5m/15m/1h)
- 中周期 (4h/1d)
- 长周期 (1d/1w)
"""
import asyncio
import json
import logging
import signal
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, Optional, Callable
import websockets
from .paper_trading import PaperTrader
from .paper_trading import MultiTimeframePaperTrader, TimeFrame, TIMEFRAME_CONFIG
logger = logging.getLogger(__name__)
class RealtimeTrader:
"""实时模拟盘交易器"""
"""实时多周期交易器"""
def __init__(
self,
symbol: str = "btcusdt",
initial_balance: float = 10000.0,
leverage: int = 5,
max_position_pct: float = 0.5,
base_position_pct: float = 0.1,
signal_check_interval: int = 60, # 每60秒检查一次信号
signal_check_interval: int = 60,
):
"""
初始化实时交易器
Args:
symbol: 交易对 (小写)
initial_balance: 初始资金
leverage: 杠杆倍数
max_position_pct: 最大持仓比例 (占资金百分比)
base_position_pct: 基础仓位比例 (每次入场)
initial_balance: 初始资金 (分配给三个周期)
signal_check_interval: 信号检查间隔()
"""
self.symbol = symbol.lower()
@ -49,13 +44,8 @@ class RealtimeTrader:
# WebSocket URL
self.ws_url = f"wss://fstream.binance.com/ws/{self.symbol}@aggTrade"
# 模拟盘 - 使用新的仓位管理参数
self.trader = PaperTrader(
initial_balance=initial_balance,
leverage=leverage,
max_position_pct=max_position_pct,
base_position_pct=base_position_pct,
)
# 多周期交易器
self.trader = MultiTimeframePaperTrader(initial_balance=initial_balance)
# 状态
self.current_price = 0.0
@ -73,14 +63,15 @@ class RealtimeTrader:
async def start(self):
"""启动实时交易"""
self.is_running = True
logger.info(f"Starting realtime trader for {self.symbol.upper()}")
logger.info(f"Starting multi-timeframe realtime trader for {self.symbol.upper()}")
logger.info(f"WebSocket URL: {self.ws_url}")
logger.info(f"Initial balance: ${self.trader.balance:.2f}")
logger.info(f"Leverage: {self.trader.leverage}x")
logger.info(f"Max position: {self.trader.position_manager.max_position_pct * 100}%")
logger.info(f"Base position: {self.trader.position_manager.base_position_pct * 100}%")
logger.info(f"Signal check interval: {self.signal_check_interval}s")
for tf in TimeFrame:
config = TIMEFRAME_CONFIG[tf]
account = self.trader.accounts[tf]
logger.info(f" [{config['name_en']}] Balance: ${account.balance:.2f}, Leverage: {account.leverage}x")
while self.is_running:
try:
await self._connect_and_trade()
@ -96,7 +87,6 @@ class RealtimeTrader:
self.ws = ws
logger.info("WebSocket connected")
# 打印初始状态
self._print_status()
async for message in ws:
@ -113,21 +103,21 @@ class RealtimeTrader:
async def _process_tick(self, data: Dict[str, Any]):
"""处理每个 tick 数据"""
# 提取价格
self.current_price = float(data.get('p', 0))
if self.current_price <= 0:
return
# 调用价格回调
if self.on_price_callback:
self.on_price_callback(self.current_price)
# 检查止盈止损
if self.trader.position:
close_result = self.trader._check_close_position(self.current_price)
if close_result:
self._on_position_closed(close_result)
# 检查各周期止盈止损
for tf in TimeFrame:
account = self.trader.accounts[tf]
if account.position and account.position.side != 'FLAT':
close_result = self.trader._check_close_position(tf, self.current_price)
if close_result:
self._on_position_closed(tf, close_result)
# 定期检查信号
now = asyncio.get_event_loop().time()
@ -137,16 +127,20 @@ class RealtimeTrader:
async def _check_and_execute_signal(self):
"""检查信号并执行交易"""
signal = self._load_latest_signal()
signal_data = self._load_latest_signal()
if not signal:
if not signal_data:
return
result = self.trader.process_signal(signal, self.current_price)
results = self.trader.process_signal(signal_data, self.current_price)
if result['action'] in ['OPEN', 'CLOSE', 'REVERSE', 'ADD', 'PARTIAL_CLOSE']:
self._on_trade_executed(result)
self._print_status()
# 处理各周期结果
for tf_value, result in results.get('timeframes', {}).items():
if result['action'] in ['OPEN', 'CLOSE', 'REVERSE']:
tf = TimeFrame(tf_value)
self._on_trade_executed(tf, result)
self._print_status()
def _load_latest_signal(self) -> Optional[Dict[str, Any]]:
"""加载最新信号"""
@ -160,90 +154,64 @@ class RealtimeTrader:
logger.error(f"Error loading signal: {e}")
return None
def _on_trade_executed(self, result: Dict[str, Any]):
def _on_trade_executed(self, tf: TimeFrame, result: Dict[str, Any]):
"""交易执行回调"""
config = TIMEFRAME_CONFIG[tf]
action = result['action']
details = result['details']
details = result.get('details', {})
if action == 'OPEN':
logger.info("=" * 60)
logger.info(f"🟢 OPEN {details['side']}")
logger.info(f"🟢 [{config['name_en']}] OPEN {details['side']}")
logger.info(f" Entry: ${details['entry_price']:.2f}")
logger.info(f" Size: {details['size']:.6f} BTC")
logger.info(f" Total Size: {details['total_size']:.6f} BTC")
logger.info(f" Stop Loss: ${details['stop_loss']:.2f}")
logger.info(f" Take Profit: ${details['take_profit']:.2f}")
logger.info("=" * 60)
elif action == 'ADD':
logger.info("=" * 60)
logger.info(f" ADD POSITION {details['side']}")
logger.info(f" Add Price: ${details['add_price']:.2f}")
logger.info(f" Add Size: {details['add_size']:.6f} BTC")
logger.info(f" Total Size: {details['total_size']:.6f} BTC")
logger.info(f" Avg Entry: ${details['avg_entry_price']:.2f}")
logger.info(f" Entries: {details['num_entries']}")
logger.info("=" * 60)
elif action == 'CLOSE':
pnl = details['pnl']
pnl = details.get('pnl', 0)
pnl_icon = "🟢" if pnl > 0 else "🔴"
logger.info("=" * 60)
logger.info(f"{pnl_icon} CLOSE {details['side']}")
logger.info(f"{pnl_icon} [{config['name_en']}] CLOSE {details['side']}")
logger.info(f" Entry: ${details['entry_price']:.2f}")
logger.info(f" Exit: ${details['exit_price']:.2f}")
logger.info(f" Size: {details['size']:.6f} BTC")
logger.info(f" Entries: {details.get('num_entries', 1)}")
logger.info(f" PnL: ${pnl:.2f} ({details['pnl_pct']:.2f}%)")
logger.info(f" Reason: {details['reason']}")
logger.info(f" New Balance: ${details['new_balance']:.2f}")
logger.info("=" * 60)
elif action == 'PARTIAL_CLOSE':
pnl = details['pnl']
pnl_icon = "🟢" if pnl > 0 else "🔴"
logger.info("=" * 60)
logger.info(f"📉 PARTIAL CLOSE {details['side']}")
logger.info(f" Closed Size: {details['closed_size']:.6f} BTC")
logger.info(f" Exit: ${details['exit_price']:.2f}")
logger.info(f" {pnl_icon} PnL: ${pnl:.2f} ({details['pnl_pct']:.2f}%)")
logger.info(f" Remaining: {details['remaining_size']:.6f} BTC")
logger.info(f" New Balance: ${details['new_balance']:.2f}")
logger.info("=" * 60)
elif action == 'REVERSE':
logger.info("=" * 60)
logger.info("🔄 REVERSE POSITION")
logger.info(f"🔄 [{config['name_en']}] REVERSE POSITION")
if 'close' in details:
logger.info(f" Closed: PnL ${details['close']['pnl']:.2f}")
logger.info(f" Closed: PnL ${details['close'].get('pnl', 0):.2f}")
if 'open' in details:
logger.info(f" Opened: {details['open']['side']} @ ${details['open']['entry_price']:.2f}")
logger.info("=" * 60)
# 调用外部回调
if self.on_trade_callback:
self.on_trade_callback(result)
self.on_trade_callback({'timeframe': tf.value, 'action': action, 'details': details})
def _on_position_closed(self, close_result: Dict[str, Any]):
def _on_position_closed(self, tf: TimeFrame, close_result: Dict[str, Any]):
"""持仓被平仓回调(止盈止损)"""
pnl = close_result['pnl']
config = TIMEFRAME_CONFIG[tf]
pnl = close_result.get('pnl', 0)
pnl_icon = "🟢" if pnl > 0 else "🔴"
reason_icon = "🎯" if close_result['reason'] == 'TAKE_PROFIT' else "🛑"
reason = close_result.get('reason', '')
reason_icon = "🎯" if reason == 'TAKE_PROFIT' else "🛑"
logger.info("=" * 60)
logger.info(f"{reason_icon} {close_result['reason']}")
logger.info(f" {pnl_icon} PnL: ${pnl:.2f} ({close_result['pnl_pct']:.2f}%)")
logger.info(f" Entry: ${close_result['entry_price']:.2f}")
logger.info(f" Exit: ${close_result['exit_price']:.2f}")
logger.info(f" Size: {close_result['size']:.6f} BTC")
logger.info(f" Entries: {close_result.get('num_entries', 1)}")
logger.info(f" New Balance: ${close_result['new_balance']:.2f}")
logger.info(f"{reason_icon} [{config['name_en']}] {reason}")
logger.info(f" {pnl_icon} PnL: ${pnl:.2f} ({close_result.get('pnl_pct', 0):.2f}%)")
logger.info(f" Entry: ${close_result.get('entry_price', 0):.2f}")
logger.info(f" Exit: ${close_result.get('exit_price', 0):.2f}")
logger.info(f" New Balance: ${close_result.get('new_balance', 0):.2f}")
logger.info("=" * 60)
self._print_status()
if self.on_trade_callback:
self.on_trade_callback({
'timeframe': tf.value,
'action': 'CLOSE',
'details': close_result,
})
@ -252,44 +220,39 @@ class RealtimeTrader:
"""打印当前状态"""
status = self.trader.get_status(self.current_price)
print("\n" + "=" * 70)
print(f"📊 PAPER TRADING STATUS - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 70)
print(f"💰 Balance: ${status['balance']:.2f} (Initial: ${status['initial_balance']:.2f})")
print(f"📈 Total Return: {status['total_return']:.2f}%")
print("\n" + "=" * 80)
print(f"📊 MULTI-TIMEFRAME TRADING STATUS - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 80)
print(f"💵 Current Price: ${self.current_price:.2f}")
print(f"💰 Total Balance: ${status['total_balance']:.2f} (Initial: ${status['total_initial_balance']:.2f})")
print(f"📈 Total Return: {status['total_return']:.2f}%")
print("-" * 80)
if status['position']:
pos = status['position']
unrealized = pos.get('unrealized_pnl', 0)
unrealized_pct = pos.get('unrealized_pnl_pct', 0)
pnl_icon = "🟢" if unrealized > 0 else "🔴" if unrealized < 0 else ""
print(f"\n📍 Position: {pos['side']} ({len(pos.get('entries', []))} entries)")
print(f" Avg Entry: ${pos['avg_entry_price']:.2f}")
print(f" Size: {pos['total_size']:.6f} BTC")
print(f" Stop Loss: ${pos['stop_loss']:.2f}")
print(f" Take Profit: ${pos['take_profit']:.2f}")
print(f" {pnl_icon} Unrealized PnL: ${unrealized:.2f} ({unrealized_pct:.2f}%)")
else:
print("\n📍 Position: FLAT (No position)")
for tf_value, tf_status in status['timeframes'].items():
name = tf_status['name_en']
balance = tf_status['balance']
return_pct = tf_status['return_pct']
leverage = tf_status['leverage']
stats = tf_status['stats']
stats = status['stats']
print(f"\n📊 Statistics:")
print(f" Total Trades: {stats['total_trades']}")
print(f" Win Rate: {stats['win_rate']:.1f}%")
print(f" Total PnL: ${stats['total_pnl']:.2f}")
print(f" Profit Factor: {stats['profit_factor']:.2f}")
print(f" Max Drawdown: {stats['max_drawdown']:.2f}%")
print(f" Max Consecutive Wins: {stats.get('max_consecutive_wins', 0)}")
print(f" Max Consecutive Losses: {stats.get('max_consecutive_losses', 0)}")
return_icon = "🟢" if return_pct > 0 else "🔴" if return_pct < 0 else ""
if status['recent_trades']:
print(f"\n📝 Recent Trades:")
for trade in status['recent_trades'][-5:]:
pnl_icon = "🟢" if trade['pnl'] > 0 else "🔴"
print(f" {pnl_icon} {trade['side']} | PnL: ${trade['pnl']:.2f} ({trade['pnl_pct']:.1f}%) | {trade['exit_reason']}")
print(f"\n📊 {name} ({leverage}x)")
print(f" Balance: ${balance:.2f} | Return: {return_icon} {return_pct:+.2f}%")
print(f" Trades: {stats['total_trades']} | Win Rate: {stats['win_rate']:.1f}% | PnL: ${stats['total_pnl']:.2f}")
print("=" * 70 + "\n")
pos = tf_status.get('position')
if pos:
unrealized = pos.get('unrealized_pnl', 0)
unrealized_pct = pos.get('unrealized_pnl_pct', 0)
pnl_icon = "🟢" if unrealized > 0 else "🔴" if unrealized < 0 else ""
print(f" Position: {pos['side']} @ ${pos['entry_price']:.2f}")
print(f" SL: ${pos['stop_loss']:.2f} | TP: ${pos['take_profit']:.2f}")
print(f" {pnl_icon} Unrealized: ${unrealized:.2f} ({unrealized_pct:+.2f}%)")
else:
print(f" Position: FLAT")
print("=" * 80 + "\n")
def stop(self):
"""停止交易"""
@ -303,29 +266,21 @@ class RealtimeTrader:
async def main():
"""主函数"""
import os
from dotenv import load_dotenv
# 加载环境变量
load_dotenv(Path(__file__).parent.parent / '.env')
# 设置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# 创建交易器
trader = RealtimeTrader(
symbol='btcusdt',
initial_balance=10000.0,
leverage=5,
max_position_pct=0.5, # 最大持仓50%资金
base_position_pct=0.1, # 每次入场10%资金
signal_check_interval=30, # 每30秒检查一次信号
signal_check_interval=30,
)
# 设置信号处理
def signal_handler(sig, frame):
logger.info("Received shutdown signal")
trader.stop()
@ -333,19 +288,16 @@ async def main():
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 启动
print("\n" + "=" * 70)
print("🚀 REALTIME PAPER TRADING")
print("=" * 70)
print("Position Management:")
print(" - Max position: 50% of balance")
print(" - Base entry: 10% of balance")
print(" - Max entries: 5 (pyramid)")
print(" - Pyramid factor: 0.8x per entry")
print(" - Signal cooldown: 5 minutes")
print("=" * 70)
print("\n" + "=" * 80)
print("🚀 MULTI-TIMEFRAME REALTIME TRADING")
print("=" * 80)
print("Timeframes:")
print(" 📈 Short-term (5m/15m/1h) - 5x leverage")
print(" 📊 Medium-term (4h/1d) - 3x leverage")
print(" 📉 Long-term (1d/1w) - 2x leverage")
print("=" * 80)
print("Press Ctrl+C to stop")
print("=" * 70 + "\n")
print("=" * 80 + "\n")
await trader.start()

View File

@ -1,24 +1,23 @@
"""
FastAPI Web Service - 模拟盘状态展示 API
FastAPI Web Service - 多周期交易状态展示 API
"""
import json
import asyncio
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, Optional, List
from typing import Dict, Any, List
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse
from pydantic import BaseModel
from fastapi.responses import FileResponse
# 状态文件路径
STATE_FILE = Path(__file__).parent.parent / 'output' / 'paper_trading_state.json'
SIGNAL_FILE = Path(__file__).parent.parent / 'output' / 'latest_signal.json'
app = FastAPI(title="Paper Trading Dashboard", version="1.0.0")
app = FastAPI(title="Trading Dashboard", version="2.0.0")
# WebSocket 连接管理
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
@ -28,7 +27,8 @@ class ConnectionManager:
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
if websocket in self.active_connections:
self.active_connections.remove(websocket)
async def broadcast(self, message: dict):
for connection in self.active_connections:
@ -37,6 +37,7 @@ class ConnectionManager:
except:
pass
manager = ConnectionManager()
@ -49,8 +50,23 @@ def load_trading_state() -> Dict[str, Any]:
except Exception as e:
print(f"Error loading state: {e}")
# 返回默认状态
return {
'balance': 10000.0,
'accounts': {
'short': _default_account('short', 10000),
'medium': _default_account('medium', 10000),
'long': _default_account('long', 10000),
},
'last_updated': None,
}
def _default_account(timeframe: str, balance: float) -> Dict:
return {
'timeframe': timeframe,
'balance': balance,
'initial_balance': balance,
'leverage': 10, # 所有周期统一 10 倍杠杆
'position': None,
'trades': [],
'stats': {
@ -59,7 +75,7 @@ def load_trading_state() -> Dict[str, Any]:
'losing_trades': 0,
'total_pnl': 0.0,
'max_drawdown': 0.0,
'peak_balance': 10000.0,
'peak_balance': balance,
'win_rate': 0.0,
},
'equity_curve': [],
@ -83,49 +99,85 @@ async def root():
html_file = Path(__file__).parent / 'static' / 'index.html'
if html_file.exists():
return FileResponse(html_file)
return HTMLResponse("<h1>Paper Trading Dashboard</h1><p>Static files not found</p>")
return {"error": "Static files not found"}
@app.get("/api/status")
async def get_status():
"""获取模拟盘状态"""
"""获取多周期交易状态"""
state = load_trading_state()
signal = load_latest_signal()
accounts = state.get('accounts', {})
# 计算总收益率
initial_balance = 10000.0
total_return = (state.get('balance', initial_balance) - initial_balance) / initial_balance * 100
# 计算总余额
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
# 构建各周期状态
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
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,
'return_pct': return_pct,
'leverage': acc.get('leverage', 1),
'position': acc.get('position'),
'stats': acc.get('stats', {}),
}
return {
'timestamp': datetime.now().isoformat(),
'balance': state.get('balance', initial_balance),
'initial_balance': initial_balance,
'total_balance': total_balance,
'total_initial_balance': total_initial,
'total_return': total_return,
'position': state.get('position'),
'stats': state.get('stats', {}),
'timeframes': timeframes,
'last_updated': state.get('last_updated'),
}
@app.get("/api/trades")
async def get_trades(limit: int = 50):
async def get_trades(timeframe: str = None, limit: int = 50):
"""获取交易记录"""
state = load_trading_state()
trades = state.get('trades', [])
accounts = state.get('accounts', {})
all_trades = []
for tf_key, acc in accounts.items():
if timeframe and tf_key != timeframe:
continue
trades = acc.get('trades', [])
all_trades.extend(trades)
# 按时间排序
all_trades.sort(key=lambda x: x.get('exit_time', ''), reverse=True)
return {
'total': len(trades),
'trades': trades[-limit:] if limit > 0 else trades,
'total': len(all_trades),
'trades': all_trades[:limit] if limit > 0 else all_trades,
}
@app.get("/api/equity")
async def get_equity_curve(limit: int = 500):
async def get_equity_curve(timeframe: str = None, limit: int = 500):
"""获取权益曲线"""
state = load_trading_state()
equity_curve = state.get('equity_curve', [])
accounts = state.get('accounts', {})
result = {}
for tf_key, acc in accounts.items():
if timeframe and tf_key != timeframe:
continue
equity_curve = acc.get('equity_curve', [])
result[tf_key] = equity_curve[-limit:] if limit > 0 else equity_curve
return {
'total': len(equity_curve),
'data': equity_curve[-limit:] if limit > 0 else equity_curve,
'data': result,
}
@ -134,51 +186,51 @@ async def get_signal():
"""获取最新信号"""
signal = load_latest_signal()
# 提取关键信息
agg = signal.get('aggregated_signal', {})
llm = agg.get('llm_signal', {})
quant = agg.get('quantitative_signal', {})
market = signal.get('market_analysis', {})
# 提取各周期机会
opportunities = llm.get('opportunities', {})
return {
'timestamp': agg.get('timestamp'),
'final_signal': agg.get('final_signal'),
'final_confidence': agg.get('final_confidence'),
'consensus': agg.get('consensus'),
'current_price': agg.get('levels', {}).get('current_price'),
'llm': {
'signal': llm.get('signal_type'),
'confidence': llm.get('confidence'),
'reasoning': llm.get('reasoning'),
'opportunities': llm.get('opportunities', {}),
'recommendations': llm.get('recommendations_by_timeframe', {}),
},
'quantitative': {
'signal': quant.get('signal_type'),
'confidence': quant.get('confidence'),
'composite_score': quant.get('composite_score'),
'scores': quant.get('scores', {}),
},
'market': {
'price': market.get('price'),
'trend': market.get('trend', {}),
'momentum': market.get('momentum', {}),
'current_price': agg.get('levels', {}).get('current_price') or market.get('price'),
'opportunities': {
'short': opportunities.get('short_term_5m_15m_1h') or opportunities.get('intraday'),
'medium': opportunities.get('medium_term_4h_1d') or opportunities.get('swing'),
'long': opportunities.get('long_term_1d_1w'),
},
'reasoning': llm.get('reasoning'),
'recommendations': llm.get('recommendations_by_timeframe', {}),
}
@app.get("/api/position")
async def get_position():
"""获取当前持仓详情"""
@app.get("/api/timeframe/{timeframe}")
async def get_timeframe_detail(timeframe: str):
"""获取单个周期详情"""
state = load_trading_state()
position = state.get('position')
accounts = state.get('accounts', {})
if not position:
return {'has_position': False, 'position': None}
if timeframe not in accounts:
return {"error": f"Timeframe '{timeframe}' not found"}
acc = accounts[timeframe]
initial = acc.get('initial_balance', 0)
balance = acc.get('balance', 0)
return {
'has_position': position.get('side') != 'FLAT' and position.get('total_size', 0) > 0,
'position': position,
'timeframe': timeframe,
'balance': balance,
'initial_balance': initial,
'return_pct': (balance - initial) / initial * 100 if initial > 0 else 0,
'leverage': acc.get('leverage', 1),
'position': acc.get('position'),
'stats': acc.get('stats', {}),
'recent_trades': acc.get('trades', [])[-20:],
'equity_curve': acc.get('equity_curve', [])[-200:],
}
@ -202,9 +254,8 @@ async def websocket_endpoint(websocket: WebSocket):
last_signal_mtime = SIGNAL_FILE.stat().st_mtime if SIGNAL_FILE.exists() else 0
while True:
await asyncio.sleep(1) # 每秒检查一次
await asyncio.sleep(1)
# 检查状态文件更新
current_state_mtime = STATE_FILE.stat().st_mtime if STATE_FILE.exists() else 0
current_signal_mtime = SIGNAL_FILE.stat().st_mtime if SIGNAL_FILE.exists() else 0
@ -239,4 +290,4 @@ if static_dir.exists():
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)
uvicorn.run(app, host="0.0.0.0", port=8000)

File diff suppressed because it is too large Load Diff