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 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 close_result:
|
if account.position and account.position.side != 'FLAT':
|
||||||
self._on_position_closed(close_result)
|
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()
|
now = asyncio.get_event_loop().time()
|
||||||
@ -137,16 +127,20 @@ 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)
|
||||||
|
|
||||||
if result['action'] in ['OPEN', 'CLOSE', 'REVERSE', 'ADD', 'PARTIAL_CLOSE']:
|
# 处理各周期结果
|
||||||
self._on_trade_executed(result)
|
for tf_value, result in results.get('timeframes', {}).items():
|
||||||
self._print_status()
|
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]]:
|
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']
|
||||||
unrealized = pos.get('unrealized_pnl', 0)
|
balance = tf_status['balance']
|
||||||
unrealized_pct = pos.get('unrealized_pnl_pct', 0)
|
return_pct = tf_status['return_pct']
|
||||||
pnl_icon = "🟢" if unrealized > 0 else "🔴" if unrealized < 0 else "⚪"
|
leverage = tf_status['leverage']
|
||||||
print(f"\n📍 Position: {pos['side']} ({len(pos.get('entries', []))} entries)")
|
stats = tf_status['stats']
|
||||||
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)")
|
|
||||||
|
|
||||||
stats = status['stats']
|
return_icon = "🟢" if return_pct > 0 else "🔴" if return_pct < 0 else "⚪"
|
||||||
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📊 {name} ({leverage}x)")
|
||||||
print(f"\n📝 Recent Trades:")
|
print(f" Balance: ${balance:.2f} | Return: {return_icon} {return_pct:+.2f}%")
|
||||||
for trade in status['recent_trades'][-5:]:
|
print(f" Trades: {stats['total_trades']} | Win Rate: {stats['win_rate']:.1f}% | PnL: ${stats['total_pnl']:.2f}")
|
||||||
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")
|
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):
|
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()
|
||||||
|
|
||||||
|
|||||||
169
web/api.py
169
web/api.py
@ -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,7 +27,8 @@ class ConnectionManager:
|
|||||||
self.active_connections.append(websocket)
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
def disconnect(self, websocket: 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):
|
async def broadcast(self, message: dict):
|
||||||
for connection in self.active_connections:
|
for connection in self.active_connections:
|
||||||
@ -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'),
|
|
||||||
'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', {}),
|
|
||||||
},
|
},
|
||||||
|
'reasoning': llm.get('reasoning'),
|
||||||
|
'recommendations': llm.get('recommendations_by_timeframe', {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@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
Loading…
Reference in New Issue
Block a user