Files
AI-Trader/routes_market.py

696 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
行情相关的接口路由
包括K线数据接收、查询、WebSocket推送等
"""
from fastapi import APIRouter, Query, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse
from typing import Optional, List, Dict
from datetime import datetime, timedelta
import json
import random
from market.models import KlineData
from market.store import KlineStore
from market.services import KlineService, PivotService, TechService, PendingOrderService
from market.trade_config import TradeConfig
from market.system_log import get_system_log
def create_market_routes(
kline_store: KlineStore,
kline_service: KlineService,
pivot_service: PivotService,
tech_service: TechService,
pending_order_service: PendingOrderService,
trading_server = None
) -> APIRouter:
"""
创建行情相关路由
Args:
kline_store: K线存储
kline_service: K线服务
pivot_service: 转折点服务
tech_service: 技术分析服务
pending_order_service: 待确认订单服务
trading_server: TradingServer 实例
"""
router = APIRouter()
# 增量K线日志打印概率 (5%)
KLINE_LOG_PROBABILITY = 0.05
# ==================== EA端接口 ====================
@router.post("/ea/kline/{period}")
async def receive_kline(period: str, request: Request) -> Dict:
"""
EA推送K线数据
"""
period = period.upper()
if period not in ['H4', 'H1', 'M15', 'M5', 'M1']:
return JSONResponse(
status_code=400,
content={"status": "error", "message": f"不支持的周期: {period}"}
)
try:
data = await request.json()
symbol = data.get('symbol', 'GOLD')
is_full = data.get('is_full', False)
klines = data.get('klines', [])
if not klines:
return {"status": "ok", "count": 0, "message": "无数据"}
# 全量数据时检查K线时效性
if is_full:
staleness = kline_service.check_staleness(symbol, period, klines)
if staleness.get('latest_kline_time'):
trade_config = TradeConfig.get_instance()
timezone_offset_hours = trade_config.mt5_timezone_offset
staleness = kline_service.check_staleness(
symbol, period, klines, timezone_offset_hours
)
if staleness.get('is_stale'):
system_log = get_system_log()
system_log.add_log(
"ea_kline_stale",
{
"period": period,
"latest_kline_time": staleness.get('latest_kline_time').isoformat() if staleness.get('latest_kline_time') else None,
"kline_time_local": staleness.get('kline_time_local').isoformat() if staleness.get('kline_time_local') else None,
"time_diff_seconds": staleness.get('time_diff_seconds'),
"period_interval": staleness.get('period_interval')
},
symbol=symbol,
message=f"K线数据过期,最新K线距当前 {staleness.get('time_diff_seconds')}秒,可能休市"
)
print(f"[MarketAPI] {symbol} {period} 全量K线数据过期")
return {
"status": "ok",
"count": 0,
"message": "K线数据过期,可能休市",
"stale": True,
"latest_kline_time": staleness.get('latest_kline_time').isoformat() if staleness.get('latest_kline_time') else None,
"time_diff_seconds": staleness.get('time_diff_seconds')
}
# 检查是否需要全量数据
if not is_full and not kline_service.is_initialized(symbol, period):
print(f"[MarketAPI] {symbol} {period} 未初始化,需要全量数据")
return JSONResponse(
status_code=400,
content={
"status": "error",
"code": 8888,
"message": "需要全量数据"
}
)
# 增量数据时检查连续性
if not is_full and kline_service.is_initialized(symbol, period):
continuity = kline_service.check_continuity(symbol, period, klines)
if not continuity["is_continuous"]:
print(f"[MarketAPI] {symbol} {period} 数据不连续,缺失 {continuity['gap_count']} 个周期")
return JSONResponse(
status_code=400,
content={
"status": "error",
"code": 8888,
"message": f"数据不连续,缺失 {continuity['gap_count']} 个周期,需要全量数据"
}
)
# 保存K线数据
result = kline_service.process_kline_data(symbol, period, klines, is_full)
# 记录日志
if is_full or random.random() < KLINE_LOG_PROBABILITY:
system_log = get_system_log()
event_type = "ea_kline_full" if is_full else "ea_kline_incremental"
system_log.add_log(
event_type,
{"period": period, "count": len(klines), "is_full": is_full},
symbol=symbol,
message=f"{'全量' if is_full else '增量'} {period} {len(klines)}条"
)
if result['status'] == 'ok':
# 更新转折点
all_klines = kline_service.get_all_kline_objects(symbol, period)
if all_klines:
pivot_service.update_pivots(symbol, period, all_klines)
return result
except Exception as e:
print(f"[MarketAPI] 接收K线数据异常: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
@router.post("/ea/kline_batch")
async def receive_kline_batch(request: Request) -> Dict:
"""EA批量推送多个周期的K线数据"""
try:
data = await request.json()
symbol = data.get('symbol', 'GOLD')
is_full = data.get('is_full', False)
kline_data = data.get('data', {})
results = {}
system_log = get_system_log()
for period, klines in kline_data.items():
period = period.upper()
if period not in ['H4', 'H1', 'M15', 'M5', 'M1']:
continue
result = kline_service.process_kline_data(symbol, period, klines, is_full)
results[period] = result
if is_full or random.random() < KLINE_LOG_PROBABILITY:
event_type = "ea_kline_full" if is_full else "ea_kline_incremental"
system_log.add_log(
event_type,
{"period": period, "count": len(klines), "is_full": is_full},
symbol=symbol,
message=f"{'全量' if is_full else '增量'} {period} {len(klines)}条"
)
if result['status'] == 'ok':
all_klines = kline_service.get_all_kline_objects(symbol, period)
if all_klines:
pivot_service.update_pivots(symbol, period, all_klines)
return {
"status": "ok",
"symbol": symbol,
"results": results
}
except Exception as e:
print(f"[MarketAPI] 批量接收K线数据异常: {e}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
# ==================== 查询接口 ====================
@router.get("/market/kline/{symbol}")
async def get_kline(
symbol: str,
period: str = Query("M5", description="周期: H4/H1/M15/M5/M1"),
count: int = Query(100, description="返回条数")
) -> Dict:
"""获取K线数据"""
period = period.upper()
klines = kline_service.get_klines(symbol, period, count)
return {
"status": "ok",
"symbol": symbol,
"period": period,
"count": len(klines),
"data": klines
}
@router.get("/market/pivots/{symbol}")
async def get_pivots(
symbol: str,
period: str = Query(None, description="周期,不指定则返回全部"),
direction: str = Query(None, description="方向: high/low"),
count: int = Query(50, description="返回条数")
) -> Dict:
"""获取转折点数据"""
if period:
period = period.upper()
pivots = pivot_service.get_pivots(symbol, period, direction, count)
return {
"status": "ok",
"symbol": symbol,
"period": period,
"count": len(pivots),
"data": pivots
}
else:
result = {}
for p in ['H4', 'H1', 'M15', 'M5', 'M1']:
pivots = pivot_service.get_pivots(symbol, p, direction, count)
if pivots:
result[p] = pivots
return {
"status": "ok",
"symbol": symbol,
"data": result
}
@router.get("/market/symbols")
async def get_symbols() -> Dict:
"""获取所有已存储数据的symbol列表"""
symbols = kline_service.get_symbols()
return {
"status": "ok",
"symbols": symbols,
"count": len(symbols)
}
@router.get("/market/configured_symbols")
async def get_configured_symbols() -> Dict:
"""获取配置的品种列表及其数据状态"""
config = TradeConfig.get_instance()
configured_symbols = list(config.symbol_config.keys())
symbols_status = []
for symbol in configured_symbols:
m1_status = kline_service.check_m1_updated_within(symbol, 180)
latest_m1_time = kline_store.get_latest_kline_time(symbol, 'M1')
period_counts = {}
with kline_store._lock:
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
period_counts[period] = len(kline_store._klines[symbol][period])
symbols_status.append({
"symbol": symbol,
"has_data": m1_status["has_data"],
"m1_count": period_counts.get('M1', 0),
"latest_m1_time": latest_m1_time.isoformat() if latest_m1_time else None,
"m1_update_time": m1_status.get("update_time").isoformat() if m1_status.get("update_time") else None,
"seconds_ago": m1_status.get("seconds_ago"),
"market_status": m1_status.get("market_status", "closed"),
"period_counts": period_counts,
"config": config.symbol_config.get(symbol, {})
})
return {
"status": "ok",
"symbols": symbols_status,
"count": len(symbols_status)
}
@router.get("/market/status")
async def get_market_status() -> Dict:
"""获取行情存储状态"""
store_status = kline_service.get_status()
pivot_status = pivot_service.get_status()
# 使用 trading_server 获取状态
server_status = trading_server.get_status() if trading_server else {}
return {
"status": "ok",
"store": store_status,
"pivots": pivot_status,
"server": server_status
}
@router.get("/market/thresholds")
async def get_thresholds() -> Dict:
"""获取各周期的接近阈值"""
thresholds = pivot_service.THRESHOLDS
return {
"status": "ok",
"thresholds": {
period: {
"value": threshold,
"percent": f"{threshold * 100:.4f}%",
"description": f"千分之{threshold * 1000}"
}
for period, threshold in thresholds.items()
}
}
# ==================== 趋势分析接口 ====================
@router.get("/trend/{symbol}")
async def get_trend(symbol: str) -> Dict:
"""获取单个品种的趋势分析"""
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
tech_service.analyze_trend(symbol, period)
resonance = tech_service.analyze_resonance(symbol)
changes = tech_service.get_trend_changes(symbol, 10)
return {
"status": "ok",
"symbol": symbol,
"resonance": resonance,
"trend_changes": changes
}
@router.post("/trend/generate_order/{symbol}")
async def generate_trade_order(symbol: str) -> Dict:
"""基于趋势分析生成交易建议"""
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
tech_service.analyze_trend(symbol, period)
current_price = kline_service.get_latest_price(symbol)
if not current_price:
return {"status": "error", "message": "无法获取当前价格"}
suggestion = tech_service.generate_trade_suggestion(symbol, current_price)
if not suggestion:
return {
"status": "ok",
"message": "当前无交易建议",
"resonance": tech_service.analyze_resonance(symbol)
}
order_id = pending_order_service.create_order_from_dict(suggestion)
return {
"status": "ok",
"message": "交易建议已生成",
"order_id": order_id,
"suggestion": suggestion
}
# ==================== 待确认订单接口 ====================
@router.get("/pending_orders")
async def get_pending_orders(symbol: Optional[str] = None) -> Dict:
"""获取待确认订单列表"""
orders = pending_order_service.get_orders_dict(symbol)
return {
"status": "ok",
"count": len(orders),
"orders": orders
}
@router.post("/pending_orders/{order_id}/confirm")
async def confirm_pending_order(order_id: str, request: Request = None) -> Dict:
"""确认待确认订单"""
update_data = {}
if request:
try:
update_data = await request.json()
except:
pass
# 获取订单并确认
order = pending_order_service.confirm_order(order_id, update_data)
if not order:
return {"status": "error", "message": "订单不存在"}
system_log = get_system_log()
action_text = '买入' if order.action == 'b' else '卖出'
symbol = order.symbol
mount = order.mount
price = order.price
sl = order.sl
tp = order.tp
system_log.add_log(
"order_confirmed",
{
"order_id": order_id,
"action": order.action,
"price": price,
"mount": mount,
"sl": sl,
"tp": tp
},
symbol=symbol,
message=f"{action_text} @ {price}, 手数={mount}, SL={sl}, TP={tp}"
)
print(f"[订单确认] {symbol} | {action_text} | 价格={price} | 手数={mount} | SL={sl} | TP={tp}")
return {
"status": "ok",
"message": "订单已确认",
"order": order.to_dict()
}
@router.post("/pending_orders/{order_id}/reject")
async def reject_pending_order(order_id: str) -> Dict:
"""拒绝待确认订单"""
order = pending_order_service.reject_order(order_id)
if not order:
return {"status": "error", "message": "订单不存在"}
system_log = get_system_log()
system_log.add_log(
"order_rejected",
{"order_id": order_id, "action": order.action, "price": order.price},
symbol=order.symbol,
message=f"订单已拒绝"
)
return {
"status": "ok",
"message": "订单已拒绝"
}
# ==================== 交易配置接口 ====================
@router.get("/trade_config")
async def get_trade_config() -> Dict:
"""获取交易配置"""
config = TradeConfig.get_instance()
return {
"status": "ok",
"config": config.to_dict()
}
@router.post("/trade_config")
async def update_trade_config(request: Request) -> Dict:
"""更新交易配置"""
config = TradeConfig.get_instance()
try:
data = await request.json()
config.update(data)
return {
"status": "ok",
"message": "配置已更新",
"config": config.to_dict()
}
except Exception as e:
return {"status": "error", "message": str(e)}
# ==================== 策略决策接口 ====================
@router.get("/strategy")
async def get_all_strategies() -> Dict:
"""获取所有策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
strategies = trading_server.strategy_service.get_all_strategies()
return {
"status": "ok",
"count": len(strategies),
"strategies": [s.to_dict() for s in strategies]
}
@router.get("/strategy/decisions")
async def get_decisions(symbol: Optional[str] = None, count: int = 20) -> Dict:
"""获取决策历史"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
decisions = trading_server.get_decision_history(symbol, count)
return {
"status": "ok",
"count": len(decisions),
"decisions": decisions
}
@router.get("/strategy/{symbol}")
async def get_strategy(symbol: str) -> Dict:
"""获取品种策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
strategy = trading_server.strategy_service.get_strategy(symbol)
return {
"status": "ok",
"strategy": strategy.to_dict()
}
@router.post("/strategy/{symbol}")
async def update_strategy(symbol: str, request: Request) -> Dict:
"""更新品种策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
try:
data = await request.json()
strategy = trading_server.strategy_service.update_strategy(symbol, data)
return {
"status": "ok",
"message": "策略配置已更新",
"strategy": strategy.to_dict()
}
except Exception as e:
return {"status": "error", "message": str(e)}
@router.delete("/strategy/{symbol}")
async def delete_strategy(symbol: str) -> Dict:
"""删除品种策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
success = trading_server.strategy_service.strategy_store.delete_strategy(symbol)
if success:
return {"status": "ok", "message": "策略配置已删除"}
return {"status": "error", "message": "策略配置不存在"}
@router.post("/strategy/trigger/{symbol}")
async def trigger_strategy_decision(symbol: str) -> Dict:
"""手动触发策略决策"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
current_price = kline_service.get_latest_price(symbol)
if not current_price:
return {"status": "error", "message": "无法获取当前价格"}
result = trading_server.process_price(symbol, current_price)
return {
"status": "ok",
"result": result
}
# ==================== 系统日志接口 ====================
@router.get("/system/logs")
async def get_system_logs(count: int = 50, event_type: str = None,
symbol: str = None) -> Dict:
"""获取系统运行日志"""
system_log = get_system_log()
event_types = None
if event_type:
event_types = [et.strip() for et in event_type.split(',') if et.strip()]
logs = system_log.get_logs(count, event_types, symbol)
return {
"status": "ok",
"count": len(logs),
"logs": logs
}
@router.delete("/system/logs")
async def clear_system_logs() -> Dict:
"""清空系统日志"""
system_log = get_system_log()
system_log.clear_logs()
return {"status": "ok", "message": "日志已清空"}
# ==================== WebSocket接口 ====================
@router.websocket("/ws/market")
async def websocket_market(websocket: WebSocket):
"""WebSocket连接"""
await websocket.accept()
# 注册到 TradingServer(内部会自动注册到 llm_analyzer 和 system_log
if trading_server:
trading_server.add_ws_client(websocket)
system_log = get_system_log()
system_log.add_ws_client(websocket)
try:
await websocket.send_text(json.dumps({
"type": "connected",
"message": "已连接到行情监控服务"
}))
while True:
try:
data = await websocket.receive_text()
msg = json.loads(data)
if msg.get('type') == 'ping':
await websocket.send_text(json.dumps({"type": "pong"}))
except WebSocketDisconnect:
break
except Exception as e:
print(f"[WebSocket] 连接异常: {e}")
finally:
if trading_server:
trading_server.remove_ws_client(websocket)
system_log.remove_ws_client(websocket)
# ==================== 大模型分析接口 ====================
@router.get("/llm/analysis")
async def get_llm_analysis(symbol: Optional[str] = None) -> Dict:
"""获取大模型分析结果"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
result = trading_server.get_llm_analysis(symbol)
return {
"status": "ok",
"data": result
}
@router.get("/llm/status")
async def get_llm_status() -> Dict:
"""获取大模型分析器状态"""
if not trading_server:
return {"status": "ok", "data": {"enabled": False, "message": "TradingServer 未初始化"}}
return {
"status": "ok",
"data": trading_server.get_llm_status()
}
@router.get("/llm/config")
async def get_llm_config() -> Dict:
"""获取大模型配置"""
if not trading_server:
return {"status": "ok", "config": {"enabled": False, "message": "TradingServer 未初始化"}}
return {
"status": "ok",
"config": trading_server.get_llm_config()
}
@router.post("/llm/trigger")
async def trigger_llm_analysis() -> Dict:
"""手动触发大模型分析"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
return trading_server.trigger_llm_analysis()
@router.post("/llm/configure")
async def configure_llm(request: Request) -> Dict:
"""配置大模型参数"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
try:
data = await request.json()
result = trading_server.configure_llm(
api_key=data.get("api_key"),
api_base=data.get("api_base"),
model=data.get("model")
)
return {"status": "ok", "data": result}
except Exception as e:
return {"status": "error", "message": str(e)}
return router