- 新增 Vue 3 + Vuetify 前端界面 - 新增市场分析模块 (market/) - 更新主服务器和路由 - 更新 MT5 EA 文件 - 添加 .gitignore 排除临时文件
259 lines
8.6 KiB
Python
259 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
K线数据存储模块
|
||
按周期和Symbol存储K线数据
|
||
"""
|
||
|
||
from collections import defaultdict
|
||
from datetime import datetime
|
||
from typing import List, Dict, Optional
|
||
import threading
|
||
|
||
|
||
def normalize_symbol(symbol: str) -> str:
|
||
"""
|
||
标准化品种名称(保持原样)
|
||
"""
|
||
return symbol if symbol else ""
|
||
|
||
|
||
class KlineData:
|
||
"""K线数据结构"""
|
||
|
||
def __init__(self, symbol: str, period: str, timestamp, open_price: float,
|
||
high: float, low: float, close: float, volume: float = 0):
|
||
self.symbol = normalize_symbol(symbol)
|
||
self.period = period # H4, H1, M15, M5, M1
|
||
self.timestamp = timestamp
|
||
self.open = open_price
|
||
self.high = high
|
||
self.low = low
|
||
self.close = close
|
||
self.volume = volume
|
||
|
||
def to_dict(self) -> Dict:
|
||
"""转换为字典"""
|
||
ts = self.timestamp
|
||
if isinstance(ts, datetime):
|
||
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
|
||
else:
|
||
ts_str = str(ts)
|
||
|
||
return {
|
||
"symbol": self.symbol,
|
||
"period": self.period,
|
||
"timestamp": ts_str,
|
||
"open": self.open,
|
||
"high": self.high,
|
||
"low": self.low,
|
||
"close": self.close,
|
||
"volume": self.volume
|
||
}
|
||
|
||
|
||
class MarketStore:
|
||
"""K线数据存储"""
|
||
|
||
# 支持的周期
|
||
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
|
||
|
||
# 各周期最大存储条数
|
||
MAX_KLINES = {
|
||
'H4': 1500, # 4小时,6个月约1100根,留余量
|
||
'H1': 1000, # 1小时,1个月约720根
|
||
'M15': 500, # 15分钟,3天约288根
|
||
'M5': 400, # 5分钟,24小时288根
|
||
'M1': 100 # 1分钟,1小时60根
|
||
}
|
||
|
||
def __init__(self):
|
||
# 存储结构: {SYMBOL: {PERIOD: [KlineData, ...]}}
|
||
self._klines = defaultdict(lambda: defaultdict(list))
|
||
self._lock = threading.RLock()
|
||
|
||
# 标记每个symbol每个周期是否已收到全量数据
|
||
# 结构: {SYMBOL: {PERIOD: True/False}}
|
||
self._initialized = defaultdict(lambda: defaultdict(bool))
|
||
|
||
print("[MarketStore] K线存储已初始化")
|
||
|
||
def save_klines(self, symbol: str, period: str, klines: List[Dict],
|
||
is_full: bool = False) -> Dict:
|
||
"""
|
||
保存K线数据
|
||
|
||
Args:
|
||
symbol: 交易品种
|
||
period: 周期 (H4/H1/M15/M5/M1)
|
||
klines: K线数据列表
|
||
is_full: 是否为全量数据
|
||
|
||
Returns:
|
||
{"status": "ok", "count": N, "is_full": bool}
|
||
"""
|
||
symbol = normalize_symbol(symbol)
|
||
period = period.upper()
|
||
|
||
if period not in self.PERIODS:
|
||
return {"status": "error", "message": f"不支持的周期: {period}"}
|
||
|
||
with self._lock:
|
||
if is_full:
|
||
# 全量数据,直接覆盖
|
||
self._klines[symbol][period] = []
|
||
|
||
# 解析并存储K线数据
|
||
new_count = 0
|
||
for k in klines:
|
||
kline = KlineData(
|
||
symbol=symbol,
|
||
period=period,
|
||
timestamp=k.get('timestamp') or k.get('time'),
|
||
open_price=float(k.get('open', 0)),
|
||
high=float(k.get('high', 0)),
|
||
low=float(k.get('low', 0)),
|
||
close=float(k.get('close', 0)),
|
||
volume=float(k.get('volume', 0))
|
||
)
|
||
|
||
# 检查是否已存在相同时间戳的数据
|
||
existing = self._klines[symbol][period]
|
||
ts = kline.timestamp
|
||
|
||
# 查找是否已存在
|
||
found_idx = -1
|
||
for i, existing_kline in enumerate(existing):
|
||
if self._normalize_timestamp(existing_kline.timestamp) == self._normalize_timestamp(ts):
|
||
found_idx = i
|
||
break
|
||
|
||
if found_idx >= 0:
|
||
# 更新已有数据
|
||
existing[found_idx] = kline
|
||
else:
|
||
# 添加新数据
|
||
existing.append(kline)
|
||
new_count += 1
|
||
|
||
# 按时间排序
|
||
self._klines[symbol][period].sort(
|
||
key=lambda x: self._normalize_timestamp(x.timestamp)
|
||
)
|
||
|
||
# 限制最大条数,保留最新的
|
||
max_count = self.MAX_KLINES.get(period, 500)
|
||
if len(self._klines[symbol][period]) > max_count:
|
||
self._klines[symbol][period] = self._klines[symbol][period][-max_count:]
|
||
|
||
# 标记已初始化
|
||
self._initialized[symbol][period] = True
|
||
|
||
total = len(self._klines[symbol][period])
|
||
print(f"[MarketStore] {symbol} {period} 保存了 {new_count} 条新数据, 当前共 {total} 条")
|
||
|
||
return {
|
||
"status": "ok",
|
||
"count": new_count,
|
||
"total": total,
|
||
"is_full": is_full
|
||
}
|
||
|
||
def get_klines(self, symbol: str, period: str, count: int = 100) -> List[Dict]:
|
||
"""获取K线数据"""
|
||
symbol = normalize_symbol(symbol)
|
||
period = period.upper()
|
||
|
||
with self._lock:
|
||
klines = self._klines[symbol][period][-count:]
|
||
return [k.to_dict() for k in klines]
|
||
|
||
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
|
||
"""获取所有K线数据"""
|
||
symbol = normalize_symbol(symbol)
|
||
period = period.upper()
|
||
|
||
with self._lock:
|
||
return [k.to_dict() for k in self._klines[symbol][period]]
|
||
|
||
def get_latest_price(self, symbol: str) -> Optional[float]:
|
||
"""获取最新价格(从K线的最新close,优先M1,依次尝试其他周期)"""
|
||
symbol = normalize_symbol(symbol)
|
||
|
||
with self._lock:
|
||
# 尝试找到匹配的symbol(支持带#后缀的symbol)
|
||
actual_symbol = None
|
||
if symbol in self._klines:
|
||
actual_symbol = symbol
|
||
else:
|
||
# 尝试添加#后缀
|
||
for s in self._klines:
|
||
if s.upper().startswith(symbol.upper()):
|
||
actual_symbol = s
|
||
break
|
||
|
||
if not actual_symbol:
|
||
return None
|
||
|
||
# 按优先级尝试各周期(M1优先,然后更短周期)
|
||
for period in ['M1', 'M5', 'M15', 'H1', 'H4']:
|
||
klines = self._klines[actual_symbol][period]
|
||
if klines:
|
||
return klines[-1].close
|
||
return None
|
||
|
||
def is_initialized(self, symbol: str, period: str) -> bool:
|
||
"""检查某个周期的数据是否已初始化"""
|
||
symbol = normalize_symbol(symbol)
|
||
period = period.upper()
|
||
return self._initialized[symbol][period]
|
||
|
||
def check_all_initialized(self, symbol: str) -> bool:
|
||
"""检查所有周期是否都已初始化"""
|
||
symbol = normalize_symbol(symbol)
|
||
return all(self._initialized[symbol][p] for p in self.PERIODS)
|
||
|
||
def clear_symbol(self, symbol: str):
|
||
"""清除某个Symbol的数据"""
|
||
symbol = normalize_symbol(symbol)
|
||
with self._lock:
|
||
if symbol in self._klines:
|
||
del self._klines[symbol]
|
||
if symbol in self._initialized:
|
||
del self._initialized[symbol]
|
||
|
||
def get_status(self) -> Dict:
|
||
"""获取存储状态"""
|
||
with self._lock:
|
||
status = {}
|
||
for symbol in self._klines:
|
||
status[symbol] = {}
|
||
for period in self.PERIODS:
|
||
count = len(self._klines[symbol][period])
|
||
initialized = self._initialized[symbol][period]
|
||
status[symbol][period] = {
|
||
"count": count,
|
||
"initialized": initialized
|
||
}
|
||
return status
|
||
|
||
def get_symbols(self) -> List[str]:
|
||
"""获取所有有实际数据的symbol列表"""
|
||
with self._lock:
|
||
symbols = []
|
||
for symbol in self._klines:
|
||
# 检查是否有实际数据(任一周期有K线数据)
|
||
has_data = False
|
||
for period in self.PERIODS:
|
||
if len(self._klines[symbol][period]) > 0:
|
||
has_data = True
|
||
break
|
||
if has_data:
|
||
symbols.append(symbol)
|
||
return symbols
|
||
|
||
def _normalize_timestamp(self, ts) -> str:
|
||
"""标准化时间戳为字符串"""
|
||
if isinstance(ts, datetime):
|
||
return ts.strftime("%Y-%m-%d %H:%M:%S")
|
||
return str(ts) |