Files
AI-Trader/routes_market.py
T
guaiwoluo2020 51b2f30748 feat: 添加前端界面和市场分析模块
- 新增 Vue 3 + Vuetify 前端界面
- 新增市场分析模块 (market/)
- 更新主服务器和路由
- 更新 MT5 EA 文件
- 添加 .gitignore 排除临时文件
2026-03-10 17:38:13 +08:00

534 lines
17 KiB
Python

#!/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
import json
from market.store import MarketStore
from market.pivot_detector import PivotDetector
from market.monitor import PivotMonitor
from market.trend_analyzer import TrendAnalyzer
from market.pending_orders import PendingOrderManager
def create_market_routes(store: MarketStore, detector: PivotDetector,
monitor: PivotMonitor, trend_analyzer: TrendAnalyzer,
pending_orders: PendingOrderManager) -> APIRouter:
"""
创建行情相关路由
Args:
store: K线存储
detector: 转折点检测器
monitor: 转折点监控器
trend_analyzer: 趋势分析器
pending_orders: 待确认订单管理器
"""
router = APIRouter()
# ==================== EA端接口 ====================
@router.post("/ea/kline/{period}")
async def receive_kline(period: str, request: Request) -> Dict:
"""
EA推送K线数据
Args:
period: 周期 (H4/H1/M15/M5/M1)
请求体:
```json
{
"symbol": "GOLD",
"is_full": false, // 是否为全量数据
"klines": [
{
"timestamp": "2024-01-15 14:00:00",
"open": 2030.50,
"high": 2035.00,
"low": 2028.00,
"close": 2033.50,
"volume": 1234
}
]
}
```
返回:
- 成功: {"status": "ok", "count": N}
- 需要全量数据: {"status": "error", "code": 8888, "message": "需要全量数据"}
"""
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').upper()
is_full = data.get('is_full', False)
klines = data.get('klines', [])
if not klines:
return {"status": "ok", "count": 0, "message": "无数据"}
# 检查是否需要全量数据
if not is_full and not store.is_initialized(symbol, period):
print(f"[MarketAPI] {symbol} {period} 未初始化,需要全量数据")
return JSONResponse(
status_code=400,
content={
"status": "error",
"code": 8888,
"message": "需要全量数据"
}
)
# 保存K线数据
result = store.save_klines(symbol, period, klines, is_full)
if result['status'] == 'ok':
# 更新转折点
all_klines = store.get_all_klines(symbol, period)
if all_klines:
# 转换为KlineData对象
from market.store import KlineData
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
detector.update_pivots(symbol, period, kline_objs)
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线数据
请求体:
```json
{
"symbol": "GOLD",
"is_full": true,
"data": {
"H4": [{...}, {...}],
"H1": [{...}, {...}],
"M15": [{...}, {...}],
"M5": [{...}, {...}],
"M1": [{...}, {...}]
}
}
```
"""
try:
data = await request.json()
symbol = data.get('symbol', 'GOLD').upper()
is_full = data.get('is_full', False)
kline_data = data.get('data', {})
results = {}
for period, klines in kline_data.items():
period = period.upper()
if period not in ['H4', 'H1', 'M15', 'M5', 'M1']:
continue
result = store.save_klines(symbol, period, klines, is_full)
results[period] = result
# 更新转折点
if result['status'] == 'ok':
all_klines = store.get_all_klines(symbol, period)
if all_klines:
from market.store import KlineData
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
detector.update_pivots(symbol, period, kline_objs)
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线数据
"""
symbol = symbol.upper()
period = period.upper()
klines = store.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:
"""
获取转折点数据
"""
symbol = symbol.upper()
if period:
period = period.upper()
pivots = detector.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 = detector.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 = store.get_symbols()
return {
"status": "ok",
"symbols": symbols,
"count": len(symbols)
}
@router.get("/market/status")
async def get_market_status() -> Dict:
"""
获取行情存储状态
"""
store_status = store.get_status()
detector_status = detector.get_status()
monitor_status = monitor.get_status()
return {
"status": "ok",
"store": store_status,
"pivots": detector_status,
"monitor": monitor_status
}
@router.get("/market/thresholds")
async def get_thresholds() -> Dict:
"""
获取各周期的接近阈值
"""
thresholds = detector.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:
"""
获取单个品种的趋势分析
"""
from market.store import KlineData
# 分析每个周期的趋势
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
all_klines = store.get_all_klines(symbol, period)
if all_klines:
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
trend_analyzer.analyze_trend(symbol, period, kline_objs)
# 获取共振分析
resonance = trend_analyzer.analyze_resonance(symbol)
# 获取趋势转换历史
changes = trend_analyzer.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:
"""
基于趋势分析生成交易建议
"""
from market.store import KlineData
# 更新趋势分析
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
all_klines = store.get_all_klines(symbol, period)
if all_klines:
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
trend_analyzer.analyze_trend(symbol, period, kline_objs)
# 获取所有周期的转折点
all_pivots = []
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
pivot_list = detector.get_pivots(symbol, period, None, 20)
all_pivots.extend(pivot_list)
# 获取当前价格
current_price = store.get_latest_price(symbol)
if not current_price:
return {"status": "error", "message": "无法获取当前价格"}
# 生成交易建议
suggestion = trend_analyzer.generate_trade_suggestion(symbol, all_pivots, current_price)
if not suggestion:
return {
"status": "ok",
"message": "当前无交易建议",
"resonance": trend_analyzer.analyze_resonance(symbol)
}
# 添加到待确认订单
order_id = pending_orders.add_order(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_orders.get_pending_orders(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
# 更新订单参数
if update_data:
order = pending_orders.get_order_by_id(order_id)
if order:
if 'mount' in update_data:
order['mount'] = update_data['mount']
if 'sl' in update_data:
order['sl'] = update_data['sl']
if 'tp' in update_data:
order['tp'] = update_data['tp']
order = pending_orders.confirm_order(order_id)
if not order:
return {"status": "error", "message": "订单不存在"}
return {
"status": "ok",
"message": "订单已确认",
"order": order
}
@router.post("/pending_orders/{order_id}/reject")
async def reject_pending_order(order_id: str) -> Dict:
"""
拒绝待确认订单
"""
success = pending_orders.reject_order(order_id)
if not success:
return {"status": "error", "message": "订单不存在"}
return {
"status": "ok",
"message": "订单已拒绝"
}
# ==================== 交易配置接口 ====================
@router.get("/trade_config")
async def get_trade_config() -> Dict:
"""
获取交易配置
"""
from market.monitor import TradeConfig
config = TradeConfig.get_instance()
return {
"status": "ok",
"config": config.to_dict()
}
@router.post("/trade_config")
async def update_trade_config(request: Request) -> Dict:
"""
更新交易配置
"""
from market.monitor import TradeConfig
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)}
# ==================== WebSocket接口 ====================
@router.websocket("/ws/market")
async def websocket_market(websocket: WebSocket):
"""
WebSocket连接,用于实时推送转折点提醒
"""
await websocket.accept()
monitor.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:
monitor.remove_ws_client(websocket)
return router