update
This commit is contained in:
parent
17995c0a0b
commit
161f9feda0
@ -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))
|
||||||
|
|||||||
@ -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("应用关闭")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user