This commit is contained in:
aaron 2026-02-07 01:14:08 +08:00
parent 17995c0a0b
commit 161f9feda0
4 changed files with 215 additions and 5 deletions

View File

@ -8,6 +8,7 @@ from pydantic import BaseModel
from app.services.paper_trading_service import get_paper_trading_service from app.services.paper_trading_service import get_paper_trading_service
from app.services.price_monitor_service import get_price_monitor_service from app.services.price_monitor_service import get_price_monitor_service
from app.services.binance_service import binance_service
from app.utils.logger import logger from app.utils.logger import logger
@ -195,16 +196,55 @@ async def get_statistics_by_symbol():
@router.get("/monitor/status") @router.get("/monitor/status")
async def get_monitor_status(): async def get_monitor_status():
"""获取价格监控状态""" """获取价格监控状态和实时价格"""
try: try:
monitor = get_price_monitor_service() monitor = get_price_monitor_service()
paper_trading = get_paper_trading_service()
# 获取活跃订单的交易对
active_orders = paper_trading.get_active_orders()
symbols_needed = set(order.get('symbol') for order in active_orders if order.get('symbol'))
# 获取价格 - 优先使用监控服务的缓存价格
latest_prices = dict(monitor.latest_prices)
# 如果监控服务没有价格数据,直接从 Binance 获取
for symbol in symbols_needed:
if symbol not in latest_prices or latest_prices[symbol] is None:
price = binance_service.get_current_price(symbol)
if price:
latest_prices[symbol] = price
# 注意:止盈止损检查由后台任务 (main.py price_monitor_loop) 处理
# 这里只返回价格数据供前端显示
return { return {
"success": True, "success": True,
"running": monitor.is_running(), "running": True, # 后台任务始终运行
"subscribed_symbols": monitor.get_subscribed_symbols(), "subscribed_symbols": list(symbols_needed),
"latest_prices": monitor.latest_prices "latest_prices": latest_prices
} }
except Exception as e: except Exception as e:
logger.error(f"获取监控状态失败: {e}") logger.error(f"获取监控状态失败: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@router.post("/reset")
async def reset_paper_trading():
"""
重置所有模拟交易数据
警告此操作将删除所有订单记录不可恢复
"""
try:
service = get_paper_trading_service()
result = service.reset_all_data()
return {
"success": True,
"message": f"模拟交易数据已重置,删除 {result['deleted_count']} 条订单",
"result": result
}
except Exception as e:
logger.error(f"重置模拟交易数据失败: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@ -1,6 +1,7 @@
""" """
FastAPI主应用 FastAPI主应用
""" """
import asyncio
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@ -12,13 +13,112 @@ from app.api import chat, stock, skills, llm, auth, admin, paper_trading
import os import os
# 后台价格监控任务
_price_monitor_task = None
async def price_monitor_loop():
"""后台价格监控循环 - 检查止盈止损"""
from app.services.paper_trading_service import get_paper_trading_service
from app.services.binance_service import binance_service
from app.services.feishu_service import get_feishu_service
from app.services.telegram_service import get_telegram_service
logger.info("后台价格监控任务已启动")
while True:
try:
paper_trading = get_paper_trading_service()
feishu = get_feishu_service()
telegram = get_telegram_service()
# 获取活跃订单
active_orders = paper_trading.get_active_orders()
if not active_orders:
await asyncio.sleep(10) # 没有活跃订单时10秒检查一次
continue
# 获取所有需要的交易对
symbols = set(order.get('symbol') for order in active_orders if order.get('symbol'))
# 获取价格并检查止盈止损
for symbol in symbols:
try:
price = binance_service.get_current_price(symbol)
if not price:
continue
# 检查止盈止损
triggered = paper_trading.check_price_triggers(symbol, price)
# 发送通知
for result in triggered:
status = result.get('status', '')
is_win = result.get('is_win', False)
if status == 'closed_tp':
emoji = "🎯"
status_text = "止盈平仓"
elif status == 'closed_sl':
emoji = "🛑"
status_text = "止损平仓"
else:
emoji = "📤"
status_text = "平仓"
win_text = "盈利" if is_win else "亏损"
side_text = "做多" if result.get('side') == 'long' else "做空"
message = f"""{emoji} 订单{status_text}
交易对: {result.get('symbol')}
方向: {side_text}
入场: ${result.get('entry_price', 0):,.2f}
出场: ${result.get('exit_price', 0):,.2f}
{win_text}: {result.get('pnl_percent', 0):+.2f}% (${result.get('pnl_amount', 0):+.2f})
持仓时间: {result.get('hold_duration', 'N/A')}"""
# 发送通知
await feishu.send_text(message)
await telegram.send_message(message)
logger.info(f"后台监控触发平仓: {result.get('order_id')} | {symbol}")
except Exception as e:
logger.error(f"检查 {symbol} 价格失败: {e}")
# 每 3 秒检查一次
await asyncio.sleep(3)
except Exception as e:
logger.error(f"价格监控循环出错: {e}")
await asyncio.sleep(5)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
"""应用生命周期管理""" """应用生命周期管理"""
global _price_monitor_task
# 启动时执行 # 启动时执行
logger.info("应用启动") logger.info("应用启动")
# 启动后台价格监控任务
settings = get_settings()
if getattr(settings, 'paper_trading_enabled', True):
_price_monitor_task = asyncio.create_task(price_monitor_loop())
logger.info("后台价格监控任务已创建")
yield yield
# 关闭时执行 # 关闭时执行
if _price_monitor_task:
_price_monitor_task.cancel()
try:
await _price_monitor_task
except asyncio.CancelledError:
pass
logger.info("后台价格监控任务已停止")
logger.info("应用关闭") logger.info("应用关闭")

View File

@ -442,6 +442,39 @@ class PaperTradingService:
} }
return result return result
def reset_all_data(self) -> Dict[str, Any]:
"""
重置所有模拟交易数据
Returns:
重置结果包含删除的订单数量
"""
db = db_service.get_session()
try:
# 统计删除前的数量
total_count = db.query(PaperOrder).count()
active_count = len(self.active_orders)
# 删除所有订单
db.query(PaperOrder).delete()
db.commit()
# 清空内存缓存
self.active_orders.clear()
logger.info(f"模拟交易数据已重置,删除 {total_count} 条订单")
return {
'deleted_count': total_count,
'active_orders_cleared': active_count
}
except Exception as e:
db.rollback()
logger.error(f"重置模拟交易数据失败: {e}")
raise
finally:
db.close()
# 全局单例 # 全局单例
_paper_trading_service: Optional[PaperTradingService] = None _paper_trading_service: Optional[PaperTradingService] = None

View File

@ -344,6 +344,21 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
/* 重置按钮 */
.reset-btn {
padding: 8px 16px;
background: transparent;
border: 1px solid #ff4444;
color: #ff4444;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.reset-btn:hover {
background: rgba(255, 68, 68, 0.1);
}
/* 操作按钮 */ /* 操作按钮 */
.action-btn { .action-btn {
padding: 4px 8px; padding: 4px 8px;
@ -441,12 +456,13 @@
<!-- 头部 --> <!-- 头部 -->
<div class="trading-header"> <div class="trading-header">
<h1 class="trading-title">模拟交易 <span>Paper Trading</span></h1> <h1 class="trading-title">模拟交易 <span>Paper Trading</span></h1>
<div style="display: flex; align-items: center; gap: 20px;"> <div style="display: flex; align-items: center; gap: 12px;">
<div class="monitor-status"> <div class="monitor-status">
<div class="monitor-dot" :class="{ running: monitorRunning }"></div> <div class="monitor-dot" :class="{ running: monitorRunning }"></div>
<span>{{ monitorRunning ? '监控中' : '未启动' }}</span> <span>{{ monitorRunning ? '监控中' : '未启动' }}</span>
</div> </div>
<button class="refresh-btn" @click="refreshData">刷新数据</button> <button class="refresh-btn" @click="refreshData">刷新数据</button>
<button class="reset-btn" @click="resetData">重置数据</button>
</div> </div>
</div> </div>
@ -747,6 +763,27 @@
} }
}, },
async resetData() {
if (!confirm('确定要重置所有模拟交易数据吗?\n\n此操作将删除所有订单记录不可恢复')) {
return;
}
try {
const response = await fetch('/api/paper-trading/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
alert(data.message);
this.refreshData();
} else {
alert('重置失败: ' + (data.detail || '未知错误'));
}
} catch (e) {
alert('重置失败: ' + e.message);
}
},
async fetchActiveOrders() { async fetchActiveOrders() {
const response = await fetch('/api/paper-trading/orders/active'); const response = await fetch('/api/paper-trading/orders/active');
const data = await response.json(); const data = await response.json();