d
This commit is contained in:
parent
6813a4abe0
commit
54e33e7fff
@ -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
@ -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()
|
||||
|
||||
|
||||
169
web/api.py
169
web/api.py
@ -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
Loading…
Reference in New Issue
Block a user