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

View File

@ -1,24 +1,23 @@
""" """
FastAPI Web Service - 模拟盘状态展示 API FastAPI Web Service - 多周期交易状态展示 API
""" """
import json import json
import asyncio import asyncio
from datetime import datetime from datetime import datetime
from pathlib import Path 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 import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel
# 状态文件路径 # 状态文件路径
STATE_FILE = Path(__file__).parent.parent / 'output' / 'paper_trading_state.json' STATE_FILE = Path(__file__).parent.parent / 'output' / 'paper_trading_state.json'
SIGNAL_FILE = Path(__file__).parent.parent / 'output' / 'latest_signal.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: class ConnectionManager:
def __init__(self): def __init__(self):
self.active_connections: List[WebSocket] = [] self.active_connections: List[WebSocket] = []
@ -28,6 +27,7 @@ class ConnectionManager:
self.active_connections.append(websocket) self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket): def disconnect(self, websocket: WebSocket):
if websocket in self.active_connections:
self.active_connections.remove(websocket) self.active_connections.remove(websocket)
async def broadcast(self, message: dict): async def broadcast(self, message: dict):
@ -37,6 +37,7 @@ class ConnectionManager:
except: except:
pass pass
manager = ConnectionManager() manager = ConnectionManager()
@ -49,8 +50,23 @@ def load_trading_state() -> Dict[str, Any]:
except Exception as e: except Exception as e:
print(f"Error loading state: {e}") print(f"Error loading state: {e}")
# 返回默认状态
return { 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, 'position': None,
'trades': [], 'trades': [],
'stats': { 'stats': {
@ -59,7 +75,7 @@ def load_trading_state() -> Dict[str, Any]:
'losing_trades': 0, 'losing_trades': 0,
'total_pnl': 0.0, 'total_pnl': 0.0,
'max_drawdown': 0.0, 'max_drawdown': 0.0,
'peak_balance': 10000.0, 'peak_balance': balance,
'win_rate': 0.0, 'win_rate': 0.0,
}, },
'equity_curve': [], 'equity_curve': [],
@ -83,49 +99,85 @@ async def root():
html_file = Path(__file__).parent / 'static' / 'index.html' html_file = Path(__file__).parent / 'static' / 'index.html'
if html_file.exists(): if html_file.exists():
return FileResponse(html_file) 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") @app.get("/api/status")
async def get_status(): async def get_status():
"""获取模拟盘状态""" """获取多周期交易状态"""
state = load_trading_state() state = load_trading_state()
signal = load_latest_signal() accounts = state.get('accounts', {})
# 计算总收益率 # 计算总余额
initial_balance = 10000.0 total_balance = sum(acc.get('balance', 0) for acc in accounts.values())
total_return = (state.get('balance', initial_balance) - initial_balance) / initial_balance * 100 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 { return {
'timestamp': datetime.now().isoformat(), 'timestamp': datetime.now().isoformat(),
'balance': state.get('balance', initial_balance), 'total_balance': total_balance,
'initial_balance': initial_balance, 'total_initial_balance': total_initial,
'total_return': total_return, 'total_return': total_return,
'position': state.get('position'), 'timeframes': timeframes,
'stats': state.get('stats', {}),
'last_updated': state.get('last_updated'), 'last_updated': state.get('last_updated'),
} }
@app.get("/api/trades") @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() 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 { return {
'total': len(trades), 'total': len(all_trades),
'trades': trades[-limit:] if limit > 0 else trades, 'trades': all_trades[:limit] if limit > 0 else all_trades,
} }
@app.get("/api/equity") @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() 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 { return {
'total': len(equity_curve), 'data': result,
'data': equity_curve[-limit:] if limit > 0 else equity_curve,
} }
@ -134,51 +186,51 @@ async def get_signal():
"""获取最新信号""" """获取最新信号"""
signal = load_latest_signal() signal = load_latest_signal()
# 提取关键信息
agg = signal.get('aggregated_signal', {}) agg = signal.get('aggregated_signal', {})
llm = agg.get('llm_signal', {}) llm = agg.get('llm_signal', {})
quant = agg.get('quantitative_signal', {})
market = signal.get('market_analysis', {}) market = signal.get('market_analysis', {})
# 提取各周期机会
opportunities = llm.get('opportunities', {})
return { return {
'timestamp': agg.get('timestamp'), 'timestamp': agg.get('timestamp'),
'final_signal': agg.get('final_signal'), 'final_signal': agg.get('final_signal'),
'final_confidence': agg.get('final_confidence'), 'final_confidence': agg.get('final_confidence'),
'consensus': agg.get('consensus'), 'current_price': agg.get('levels', {}).get('current_price') or market.get('price'),
'current_price': agg.get('levels', {}).get('current_price'), 'opportunities': {
'llm': { 'short': opportunities.get('short_term_5m_15m_1h') or opportunities.get('intraday'),
'signal': llm.get('signal_type'), 'medium': opportunities.get('medium_term_4h_1d') or opportunities.get('swing'),
'confidence': llm.get('confidence'), 'long': opportunities.get('long_term_1d_1w'),
},
'reasoning': llm.get('reasoning'), 'reasoning': llm.get('reasoning'),
'opportunities': llm.get('opportunities', {}),
'recommendations': llm.get('recommendations_by_timeframe', {}), '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', {}),
},
} }
@app.get("/api/position") @app.get("/api/timeframe/{timeframe}")
async def get_position(): async def get_timeframe_detail(timeframe: str):
"""获取当前持仓详情""" """获取单个周期详情"""
state = load_trading_state() state = load_trading_state()
position = state.get('position') accounts = state.get('accounts', {})
if not position: if timeframe not in accounts:
return {'has_position': False, 'position': None} return {"error": f"Timeframe '{timeframe}' not found"}
acc = accounts[timeframe]
initial = acc.get('initial_balance', 0)
balance = acc.get('balance', 0)
return { return {
'has_position': position.get('side') != 'FLAT' and position.get('total_size', 0) > 0, 'timeframe': timeframe,
'position': position, '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 last_signal_mtime = SIGNAL_FILE.stat().st_mtime if SIGNAL_FILE.exists() else 0
while True: 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_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 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__": if __name__ == "__main__":
import uvicorn 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