This commit is contained in:
薇薇安 2026-01-13 20:08:11 +08:00
parent dee252fadb
commit 186b2f2424
6 changed files with 154 additions and 78 deletions

View File

@ -3,7 +3,7 @@ FastAPI应用主入口
""" """
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from api.routes import config, trades, stats, dashboard from api.routes import config, trades, stats, dashboard, account
import os import os
from pathlib import Path from pathlib import Path
@ -45,6 +45,7 @@ app.include_router(config.router, prefix="/api/config", tags=["配置管理"])
app.include_router(trades.router, prefix="/api/trades", tags=["交易记录"]) app.include_router(trades.router, prefix="/api/trades", tags=["交易记录"])
app.include_router(stats.router, prefix="/api/stats", tags=["统计分析"]) app.include_router(stats.router, prefix="/api/stats", tags=["统计分析"])
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"]) app.include_router(dashboard.router, prefix="/api/dashboard", tags=["仪表板"])
app.include_router(account.router, prefix="/api/account", tags=["账户数据"])
@app.get("/") @app.get("/")

View File

@ -40,12 +40,29 @@ async def get_performance_stats(days: int = Query(7, ge=1, le=365)):
async def get_dashboard_data(): async def get_dashboard_data():
"""获取仪表板数据""" """获取仪表板数据"""
try: try:
# 最近的账户快照 account_data = None
snapshots = AccountSnapshot.get_recent(1)
latest_snapshot = snapshots[0] if snapshots else None
# 最近的交易 # 优先尝试获取实时账户数据
recent_trades = Trade.get_all(status='open')[:10] try:
from api.routes.account import get_realtime_account_data
account_data = await get_realtime_account_data()
except Exception as e:
logger.warning(f"获取实时账户数据失败,使用数据库快照: {e}")
# 回退到数据库快照
snapshots = AccountSnapshot.get_recent(1)
account_data = snapshots[0] if snapshots else None
# 获取持仓数据(优先实时,回退到数据库)
open_trades = []
try:
from api.routes.account import get_realtime_positions
positions = await get_realtime_positions()
# 转换为前端需要的格式
open_trades = positions
except Exception as e:
logger.warning(f"获取实时持仓失败,使用数据库记录: {e}")
# 回退到数据库记录
open_trades = Trade.get_all(status='open')[:10]
# 最近的扫描记录 # 最近的扫描记录
recent_scans = MarketScan.get_recent(10) recent_scans = MarketScan.get_recent(10)
@ -54,10 +71,11 @@ async def get_dashboard_data():
recent_signals = TradingSignal.get_recent(20) recent_signals = TradingSignal.get_recent(20)
return { return {
"account": latest_snapshot, "account": account_data,
"open_trades": recent_trades, "open_trades": open_trades,
"recent_scans": recent_scans, "recent_scans": recent_scans,
"recent_signals": recent_signals "recent_signals": recent_signals
} }
except Exception as e: except Exception as e:
logger.error(f"获取仪表板数据失败: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View File

@ -75,6 +75,16 @@
padding: 0.75rem; padding: 0.75rem;
background: white; background: white;
border-radius: 4px; border-radius: 4px;
gap: 1rem;
}
.trade-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.85rem;
color: #666;
flex: 1;
} }
.trade-symbol { .trade-symbol {

View File

@ -70,14 +70,27 @@ const StatsDashboard = () => {
<h3>当前持仓</h3> <h3>当前持仓</h3>
{openTrades.length > 0 ? ( {openTrades.length > 0 ? (
<div className="trades-list"> <div className="trades-list">
{openTrades.map(trade => ( {openTrades.map((trade, index) => (
<div key={trade.id} className="trade-item"> <div key={trade.id || trade.symbol || index} className="trade-item">
<div className="trade-symbol">{trade.symbol}</div> <div className="trade-symbol">{trade.symbol}</div>
<div className={`trade-side ${trade.side === 'BUY' ? 'buy' : 'sell'}`}> <div className={`trade-side ${trade.side === 'BUY' ? 'buy' : 'sell'}`}>
{trade.side} {trade.side}
</div> </div>
<div className="trade-pnl"> <div className="trade-info">
{parseFloat(trade.pnl).toFixed(2)} USDT <div>数量: {parseFloat(trade.quantity || 0).toFixed(4)}</div>
<div>入场价: {parseFloat(trade.entry_price || 0).toFixed(4)}</div>
{trade.mark_price && (
<div>标记价: {parseFloat(trade.mark_price).toFixed(4)}</div>
)}
{trade.leverage && (
<div>杠杆: {trade.leverage}x</div>
)}
</div>
<div className={`trade-pnl ${parseFloat(trade.pnl || 0) >= 0 ? 'positive' : 'negative'}`}>
{parseFloat(trade.pnl || 0).toFixed(2)} USDT
{trade.pnl_percent !== undefined && (
<span> ({parseFloat(trade.pnl_percent).toFixed(2)}%)</span>
)}
</div> </div>
</div> </div>
))} ))}

View File

@ -1,13 +1,22 @@
""" """
配置文件 - API密钥和交易参数配置 配置文件 - API密钥和交易参数配置
支持从数据库读取配置优先回退到环境变量和默认值 支持从数据库读取配置优先回退到环境变量和默认值
支持动态重载配置
""" """
import os import os
from typing import Optional from typing import Optional
# 尝试从数据库加载配置 # 尝试从数据库加载配置
USE_DB_CONFIG = False USE_DB_CONFIG = False
try: _config_manager = None
def _init_config_manager():
"""初始化配置管理器"""
global USE_DB_CONFIG, _config_manager
if _config_manager is not None:
return _config_manager
try:
import sys import sys
from pathlib import Path from pathlib import Path
# 从trading_system目录向上两级到项目根目录然后找backend # 从trading_system目录向上两级到项目根目录然后找backend
@ -16,71 +25,87 @@ try:
if backend_path.exists(): if backend_path.exists():
sys.path.insert(0, str(backend_path)) sys.path.insert(0, str(backend_path))
from config_manager import config_manager from config_manager import config_manager
_config_manager = config_manager
USE_DB_CONFIG = True USE_DB_CONFIG = True
return config_manager
else: else:
USE_DB_CONFIG = False USE_DB_CONFIG = False
except Exception: return None
except Exception as e:
USE_DB_CONFIG = False USE_DB_CONFIG = False
return None
# 币安API配置优先从数据库回退到环境变量和默认值 # 初始化配置管理器
if USE_DB_CONFIG: _init_config_manager()
BINANCE_API_KEY: Optional[str] = config_manager.get('BINANCE_API_KEY') or os.getenv('BINANCE_API_KEY', 'pMEXSgISMgpUIpGjyhikMXWQ7K7cCs1FFATyIvNIwWrUIQegoipVBskPUoUuvaVN')
BINANCE_API_SECRET: Optional[str] = config_manager.get('BINANCE_API_SECRET') or os.getenv('BINANCE_API_SECRET', 'RklItVtBCjGV40mIquoSj78xlTGkdUxz0AFyTnsnuzSBfx776VG0S2Vw5BRLRRg2')
USE_TESTNET: bool = config_manager.get('USE_TESTNET', False) if config_manager.get('USE_TESTNET') is not None else os.getenv('USE_TESTNET', 'False').lower() == 'true'
else:
BINANCE_API_KEY: Optional[str] = os.getenv('BINANCE_API_KEY', 'pMEXSgISMgpUIpGjyhikMXWQ7K7cCs1FFATyIvNIwWrUIQegoipVBskPUoUuvaVN')
BINANCE_API_SECRET: Optional[str] = os.getenv('BINANCE_API_SECRET', 'RklItVtBCjGV40mIquoSj78xlTGkdUxz0AFyTnsnuzSBfx776VG0S2Vw5BRLRRg2')
USE_TESTNET: bool = os.getenv('USE_TESTNET', 'False').lower() == 'true'
# 交易参数配置(优先从数据库读取) def _get_config_value(key, default=None):
if USE_DB_CONFIG: """获取配置值(支持动态重载)"""
TRADING_CONFIG = config_manager.get_trading_config() if _config_manager:
else: _config_manager.reload() # 每次获取配置时重新加载
TRADING_CONFIG = { return _config_manager.get(key, default) or os.getenv(key, default)
# 仓位控制 return os.getenv(key, default)
'MAX_POSITION_PERCENT': 0.05, # 单笔最大仓位账户余额的5%
'MAX_TOTAL_POSITION_PERCENT': 0.30, # 总仓位上限账户余额的30%
'MIN_POSITION_PERCENT': 0.01, # 单笔最小仓位账户余额的1%
# 涨跌幅阈值 def _get_trading_config():
'MIN_CHANGE_PERCENT': 2.0, # 最小涨跌幅阈值2% """获取交易配置(支持动态重载)"""
'TOP_N_SYMBOLS': 10, # 选择前N个货币对 if _config_manager:
_config_manager.reload() # 每次获取配置时重新加载
# 风险控制 return _config_manager.get_trading_config()
'STOP_LOSS_PERCENT': 0.03, # 止损3% # 回退到默认配置
'TAKE_PROFIT_PERCENT': 0.05, # 止盈5% return {
'MAX_POSITION_PERCENT': 0.05,
# 市场扫描1小时主周期 'MAX_TOTAL_POSITION_PERCENT': 0.30,
'SCAN_INTERVAL': 3600, # 扫描间隔1小时 'MIN_POSITION_PERCENT': 0.01,
'KLINE_INTERVAL': '1h', # K线周期1小时 'MIN_CHANGE_PERCENT': 2.0,
'PRIMARY_INTERVAL': '1h', # 主周期1小时 'TOP_N_SYMBOLS': 10,
'CONFIRM_INTERVAL': '4h', # 确认周期4小时 'STOP_LOSS_PERCENT': 0.03,
'ENTRY_INTERVAL': '15m', # 入场周期15分钟 'TAKE_PROFIT_PERCENT': 0.05,
# 过滤条件
'MIN_VOLUME_24H': 10000000, # 最小24小时成交量1000万USDT
'MIN_VOLATILITY': 0.02, # 最小波动率2%
# 高胜率策略参数
'MIN_SIGNAL_STRENGTH': 5, # 最小信号强度0-10越高越严格胜率越高
'LEVERAGE': 10, # 杠杆倍数
'USE_TRAILING_STOP': True, # 是否使用移动止损
'TRAILING_STOP_ACTIVATION': 0.01, # 移动止损激活阈值盈利1%后激活)
'TRAILING_STOP_PROTECT': 0.01, # 移动止损保护利润保护1%利润)
# Unicorn WebSocket配置
'USE_UNICORN_WEBSOCKET': True, # 是否使用Unicorn WebSocket高性能实时数据流
}
# 如果使用数据库配置,确保包含所有必要的默认值
if USE_DB_CONFIG:
defaults = {
'SCAN_INTERVAL': 3600, 'SCAN_INTERVAL': 3600,
'KLINE_INTERVAL': '1h', 'KLINE_INTERVAL': '1h',
'PRIMARY_INTERVAL': '1h', 'PRIMARY_INTERVAL': '1h',
'CONFIRM_INTERVAL': '4h', 'CONFIRM_INTERVAL': '4h',
'ENTRY_INTERVAL': '15m', 'ENTRY_INTERVAL': '15m',
'MIN_VOLUME_24H': 10000000,
'MIN_VOLATILITY': 0.02,
'MIN_SIGNAL_STRENGTH': 5,
'LEVERAGE': 10,
'USE_TRAILING_STOP': True,
'TRAILING_STOP_ACTIVATION': 0.01,
'TRAILING_STOP_PROTECT': 0.01,
'USE_UNICORN_WEBSOCKET': True,
} }
# 币安API配置优先从数据库回退到环境变量和默认值
BINANCE_API_KEY: Optional[str] = _get_config_value('BINANCE_API_KEY', 'pMEXSgISMgpUIpGjyhikMXWQ7K7cCs1FFATyIvNIwWrUIQegoipVBskPUoUuvaVN')
BINANCE_API_SECRET: Optional[str] = _get_config_value('BINANCE_API_SECRET', 'RklItVtBCjGV40mIquoSj78xlTGkdUxz0AFyTnsnuzSBfx776VG0S2Vw5BRLRRg2')
USE_TESTNET: bool = _get_config_value('USE_TESTNET', False) if _get_config_value('USE_TESTNET') is not None else os.getenv('USE_TESTNET', 'False').lower() == 'true'
# 交易参数配置(优先从数据库读取,支持动态重载)
TRADING_CONFIG = _get_trading_config()
# 确保包含所有必要的默认值
defaults = {
'SCAN_INTERVAL': 3600,
'KLINE_INTERVAL': '1h',
'PRIMARY_INTERVAL': '1h',
'CONFIRM_INTERVAL': '4h',
'ENTRY_INTERVAL': '15m',
}
for key, value in defaults.items():
if key not in TRADING_CONFIG:
TRADING_CONFIG[key] = value
# 提供一个函数来重新加载配置
def reload_config():
"""重新加载配置(供外部调用)"""
global TRADING_CONFIG, BINANCE_API_KEY, BINANCE_API_SECRET, USE_TESTNET, _config_manager
_init_config_manager() # 重新初始化配置管理器
if _config_manager:
_config_manager.reload()
BINANCE_API_KEY = _get_config_value('BINANCE_API_KEY', BINANCE_API_KEY)
BINANCE_API_SECRET = _get_config_value('BINANCE_API_SECRET', BINANCE_API_SECRET)
USE_TESTNET = _get_config_value('USE_TESTNET', False) if _get_config_value('USE_TESTNET') is not None else os.getenv('USE_TESTNET', 'False').lower() == 'true'
TRADING_CONFIG = _get_trading_config()
# 确保默认值
for key, value in defaults.items(): for key, value in defaults.items():
if key not in TRADING_CONFIG: if key not in TRADING_CONFIG:
TRADING_CONFIG[key] = value TRADING_CONFIG[key] = value

View File

@ -175,9 +175,18 @@ class TradingStrategy:
except Exception as e: except Exception as e:
logger.debug(f"记录账户快照失败: {e}") logger.debug(f"记录账户快照失败: {e}")
# 重新加载配置(确保使用最新配置)
try:
if hasattr(config, 'reload_config'):
config.reload_config()
logger.info("配置已重新加载,将使用最新配置进行下次扫描")
except Exception as e:
logger.debug(f"重新加载配置失败: {e}")
# 等待下次扫描 # 等待下次扫描
logger.info(f"等待 {config.TRADING_CONFIG['SCAN_INTERVAL']} 秒后进行下次扫描...") scan_interval = config.TRADING_CONFIG.get('SCAN_INTERVAL', 3600)
await asyncio.sleep(config.TRADING_CONFIG['SCAN_INTERVAL']) logger.info(f"等待 {scan_interval} 秒后进行下次扫描...")
await asyncio.sleep(scan_interval)
except Exception as e: except Exception as e:
logger.error(f"策略执行出错: {e}", exc_info=True) logger.error(f"策略执行出错: {e}", exc_info=True)