update
This commit is contained in:
parent
1cc9b0be45
commit
ceb593a061
@ -248,3 +248,37 @@ async def reset_paper_trading():
|
||||
except Exception as e:
|
||||
logger.error(f"重置模拟交易数据失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/report")
|
||||
async def send_report(
|
||||
hours: int = Query(4, description="报告时间段(小时)"),
|
||||
send_telegram: bool = Query(True, description="是否发送到Telegram")
|
||||
):
|
||||
"""
|
||||
手动触发发送模拟交易报告
|
||||
|
||||
- hours: 报告时间段,默认4小时
|
||||
- send_telegram: 是否发送到Telegram,默认True
|
||||
"""
|
||||
try:
|
||||
from app.services.telegram_service import get_telegram_service
|
||||
|
||||
service = get_paper_trading_service()
|
||||
report = service.generate_report(hours=hours)
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"report": report,
|
||||
"telegram_sent": False
|
||||
}
|
||||
|
||||
if send_telegram:
|
||||
telegram = get_telegram_service()
|
||||
sent = await telegram.send_message(report, parse_mode="HTML")
|
||||
result["telegram_sent"] = sent
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"生成报告失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ -13,8 +13,9 @@ from app.api import chat, stock, skills, llm, auth, admin, paper_trading
|
||||
import os
|
||||
|
||||
|
||||
# 后台价格监控任务
|
||||
# 后台任务
|
||||
_price_monitor_task = None
|
||||
_report_task = None
|
||||
|
||||
|
||||
async def price_monitor_loop():
|
||||
@ -94,20 +95,72 @@ async def price_monitor_loop():
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
async def periodic_report_loop():
|
||||
"""定时报告循环 - 每4小时发送一次模拟交易报告"""
|
||||
from datetime import datetime
|
||||
from app.services.paper_trading_service import get_paper_trading_service
|
||||
from app.services.telegram_service import get_telegram_service
|
||||
|
||||
logger.info("定时报告任务已启动")
|
||||
|
||||
# 计算距离下一个整4小时的等待时间
|
||||
def get_seconds_until_next_4h():
|
||||
now = datetime.now()
|
||||
current_hour = now.hour
|
||||
# 下一个4小时整点: 0, 4, 8, 12, 16, 20
|
||||
next_4h = ((current_hour // 4) + 1) * 4
|
||||
if next_4h >= 24:
|
||||
next_4h = 0
|
||||
# 需要等到明天
|
||||
from datetime import timedelta
|
||||
next_time = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
||||
else:
|
||||
next_time = now.replace(hour=next_4h, minute=0, second=0, microsecond=0)
|
||||
|
||||
wait_seconds = (next_time - now).total_seconds()
|
||||
return int(wait_seconds), next_time
|
||||
|
||||
while True:
|
||||
try:
|
||||
# 计算等待时间
|
||||
wait_seconds, next_time = get_seconds_until_next_4h()
|
||||
logger.info(f"下次报告时间: {next_time.strftime('%Y-%m-%d %H:%M')},等待 {wait_seconds // 3600}小时{(wait_seconds % 3600) // 60}分钟")
|
||||
|
||||
# 等待到下一个4小时整点
|
||||
await asyncio.sleep(wait_seconds)
|
||||
|
||||
# 生成并发送报告
|
||||
paper_trading = get_paper_trading_service()
|
||||
telegram = get_telegram_service()
|
||||
|
||||
report = paper_trading.generate_report(hours=4)
|
||||
await telegram.send_message(report, parse_mode="HTML")
|
||||
logger.info("已发送4小时模拟交易报告")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"定时报告循环出错: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
await asyncio.sleep(60) # 出错后等待1分钟再重试
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
global _price_monitor_task
|
||||
global _price_monitor_task, _report_task
|
||||
|
||||
# 启动时执行
|
||||
logger.info("应用启动")
|
||||
|
||||
# 启动后台价格监控任务
|
||||
# 启动后台任务
|
||||
settings = get_settings()
|
||||
if getattr(settings, 'paper_trading_enabled', True):
|
||||
_price_monitor_task = asyncio.create_task(price_monitor_loop())
|
||||
logger.info("后台价格监控任务已创建")
|
||||
|
||||
_report_task = asyncio.create_task(periodic_report_loop())
|
||||
logger.info("定时报告任务已创建")
|
||||
|
||||
yield
|
||||
|
||||
# 关闭时执行
|
||||
@ -119,6 +172,14 @@ async def lifespan(app: FastAPI):
|
||||
pass
|
||||
logger.info("后台价格监控任务已停止")
|
||||
|
||||
if _report_task:
|
||||
_report_task.cancel()
|
||||
try:
|
||||
await _report_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
logger.info("定时报告任务已停止")
|
||||
|
||||
logger.info("应用关闭")
|
||||
|
||||
|
||||
|
||||
@ -554,6 +554,123 @@ class PaperTradingService:
|
||||
}
|
||||
return result
|
||||
|
||||
def get_period_statistics(self, hours: int = 4) -> Dict[str, Any]:
|
||||
"""
|
||||
获取指定时间段内的统计数据
|
||||
|
||||
Args:
|
||||
hours: 统计时间段(小时)
|
||||
|
||||
Returns:
|
||||
时间段内的统计数据
|
||||
"""
|
||||
db = db_service.get_session()
|
||||
try:
|
||||
cutoff_time = datetime.utcnow() - timedelta(hours=hours)
|
||||
|
||||
# 查询时间段内平仓的订单
|
||||
closed_orders = db.query(PaperOrder).filter(
|
||||
PaperOrder.status.in_([
|
||||
OrderStatus.CLOSED_TP,
|
||||
OrderStatus.CLOSED_SL,
|
||||
OrderStatus.CLOSED_MANUAL
|
||||
]),
|
||||
PaperOrder.closed_at >= cutoff_time
|
||||
).all()
|
||||
|
||||
# 查询时间段内新开仓的订单(包括当前活跃的)
|
||||
new_orders = db.query(PaperOrder).filter(
|
||||
PaperOrder.created_at >= cutoff_time
|
||||
).all()
|
||||
|
||||
# 计算时间段内的盈亏
|
||||
period_pnl = sum(o.pnl_amount for o in closed_orders)
|
||||
period_wins = len([o for o in closed_orders if o.pnl_amount > 0])
|
||||
period_losses = len([o for o in closed_orders if o.pnl_amount < 0])
|
||||
|
||||
return {
|
||||
'period_hours': hours,
|
||||
'new_orders': len(new_orders),
|
||||
'closed_orders': len(closed_orders),
|
||||
'period_pnl': round(period_pnl, 2),
|
||||
'period_wins': period_wins,
|
||||
'period_losses': period_losses,
|
||||
'period_win_rate': round(period_wins / len(closed_orders) * 100, 1) if closed_orders else 0
|
||||
}
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def generate_report(self, hours: int = 4) -> str:
|
||||
"""
|
||||
生成模拟交易报告
|
||||
|
||||
Args:
|
||||
hours: 报告时间段(小时)
|
||||
|
||||
Returns:
|
||||
格式化的报告文本
|
||||
"""
|
||||
# 获取总体统计
|
||||
total_stats = self.calculate_statistics()
|
||||
|
||||
# 获取时间段统计
|
||||
period_stats = self.get_period_statistics(hours)
|
||||
|
||||
# 获取当前活跃订单
|
||||
active_orders = self.get_active_orders()
|
||||
|
||||
# 构建报告
|
||||
lines = [
|
||||
f"📊 <b>模拟交易 {hours} 小时报告</b>",
|
||||
"",
|
||||
"━━━━━━ 总体情况 ━━━━━━",
|
||||
f"总交易数: {total_stats['total_trades']} | 胜率: {total_stats['win_rate']}%",
|
||||
f"总盈亏: <code>${total_stats['total_pnl']:+.2f}</code>",
|
||||
"",
|
||||
f"━━━━━━ 过去 {hours} 小时 ━━━━━━",
|
||||
f"新订单: {period_stats['new_orders']} | 已平仓: {period_stats['closed_orders']}",
|
||||
f"本期盈亏: <code>${period_stats['period_pnl']:+.2f}</code>",
|
||||
]
|
||||
|
||||
# 当前持仓
|
||||
open_orders = [o for o in active_orders if o.get('status') == 'open']
|
||||
pending_orders = [o for o in active_orders if o.get('status') == 'pending']
|
||||
|
||||
if open_orders or pending_orders:
|
||||
lines.append("")
|
||||
lines.append("━━━━━━ 当前订单 ━━━━━━")
|
||||
|
||||
for order in open_orders[:5]: # 最多显示5个
|
||||
side_text = "做多" if order.get('side') == 'long' else "做空"
|
||||
entry_price = order.get('filled_price') or order.get('entry_price', 0)
|
||||
lines.append(f"✅ {order.get('symbol')} {side_text} @ ${entry_price:,.0f}")
|
||||
|
||||
for order in pending_orders[:3]: # 最多显示3个挂单
|
||||
side_text = "做多" if order.get('side') == 'long' else "做空"
|
||||
lines.append(f"⏳ {order.get('symbol')} {side_text} 挂单 @ ${order.get('entry_price', 0):,.0f}")
|
||||
|
||||
if len(open_orders) > 5:
|
||||
lines.append(f"... 还有 {len(open_orders) - 5} 个持仓")
|
||||
if len(pending_orders) > 3:
|
||||
lines.append(f"... 还有 {len(pending_orders) - 3} 个挂单")
|
||||
|
||||
# 按等级统计
|
||||
by_grade = total_stats.get('by_grade', {})
|
||||
if by_grade:
|
||||
lines.append("")
|
||||
lines.append("━━━━━━ 按等级统计 ━━━━━━")
|
||||
for grade in ['A', 'B', 'C']:
|
||||
if grade in by_grade:
|
||||
g = by_grade[grade]
|
||||
pnl_sign = "+" if g['total_pnl'] >= 0 else ""
|
||||
lines.append(f"{grade}级: {g['count']}笔 | 胜率{g['win_rate']}% | {pnl_sign}${g['total_pnl']:.0f}")
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"<i>报告时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}</i>")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def reset_all_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
重置所有模拟交易数据
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
.trading-container {
|
||||
max-width: 1400px;
|
||||
min-width: 800px;
|
||||
min-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@ -62,6 +62,26 @@
|
||||
color: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* 发送报告按钮 */
|
||||
.report-btn {
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: 1px solid #1da1f2;
|
||||
color: #1da1f2;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.report-btn:hover {
|
||||
background: rgba(29, 161, 242, 0.1);
|
||||
}
|
||||
|
||||
.report-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
@ -478,6 +498,9 @@
|
||||
<div class="monitor-dot" :class="{ running: monitorRunning }"></div>
|
||||
<span>{{ monitorRunning ? '监控中' : '未启动' }}</span>
|
||||
</div>
|
||||
<button class="report-btn" @click="sendReport" :disabled="sendingReport">
|
||||
{{ sendingReport ? '发送中...' : '发送报告' }}
|
||||
</button>
|
||||
<button class="refresh-btn" @click="manualRefresh">刷新数据</button>
|
||||
<button class="reset-btn" @click="resetData">重置数据</button>
|
||||
</div>
|
||||
@ -763,7 +786,8 @@
|
||||
monitorRunning: false,
|
||||
latestPrices: {},
|
||||
refreshInterval: null,
|
||||
isFirstLoad: true
|
||||
isFirstLoad: true,
|
||||
sendingReport: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@ -827,6 +851,29 @@
|
||||
}
|
||||
},
|
||||
|
||||
async sendReport() {
|
||||
this.sendingReport = true;
|
||||
try {
|
||||
const response = await fetch('/api/paper-trading/report?hours=4&send_telegram=true', {
|
||||
method: 'POST'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
if (data.telegram_sent) {
|
||||
alert('报告已发送到 Telegram Channel');
|
||||
} else {
|
||||
alert('报告生成成功,但 Telegram 发送失败(可能未配置)');
|
||||
}
|
||||
} else {
|
||||
alert('发送失败: ' + (data.detail || '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('发送失败: ' + e.message);
|
||||
} finally {
|
||||
this.sendingReport = false;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchActiveOrders() {
|
||||
const response = await fetch('/api/paper-trading/orders/active');
|
||||
const data = await response.json();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user