Files
AI-Trader/market/store/kline_store.py
T

285 lines
9.9 KiB
Python
Raw 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线数据存储模块
按周期和Symbol存储K线数据
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
class KlineStore:
"""K线数据存储(只负责数据CRUD,不包含业务逻辑)"""
# 支持的周期
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根
}
# 各周期时间间隔(秒)
PERIOD_INTERVALS = {
'H4': 4 * 60 * 60, # 4小时
'H1': 1 * 60 * 60, # 1小时
'M15': 15 * 60, # 15分钟
'M5': 5 * 60, # 5分钟
'M1': 1 * 60 # 1分钟
}
def __init__(self):
# 存储结构: {SYMBOL: {PERIOD: [KlineData, ...]}}
# 这里存储的是字典格式,由 KlineService 转换
self._klines = defaultdict(lambda: defaultdict(list))
self._lock = threading.RLock()
# 标记每个symbol每个周期是否已收到全量数据
self._initialized = defaultdict(lambda: defaultdict(bool))
# 记录每个symbol的M1数据最后更新时间(本地时间)
self._m1_update_time = {}
print("[KlineStore] K线存储已初始化")
def save_klines(self, symbol: str, period: str, klines: List[Dict],
is_full: bool = False) -> Dict:
"""
保存K线数据(纯存储操作)
Args:
symbol: 交易品种
period: 周期
klines: K线字典列表
is_full: 是否为全量数据
Returns:
{"status": "ok", "count": N, "total": M, "is_full": bool}
"""
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] = []
print(f"[KlineStore] 收到 {symbol} {period} 全量数据,清空该周期历史数据")
new_count = 0
update_count = 0
for k in klines:
# 检查是否已存在相同时间戳的数据
existing = self._klines[symbol][period]
ts = self._normalize_timestamp(k.get('timestamp') or k.get('time'))
found_idx = -1
for i, existing_kline in enumerate(existing):
if self._normalize_timestamp(existing_kline.get('timestamp') or existing_kline.get('time')) == ts:
found_idx = i
break
if found_idx >= 0:
existing[found_idx] = k
update_count += 1
else:
existing.append(k)
new_count += 1
# 按时间排序
self._klines[symbol][period].sort(
key=lambda x: self._normalize_timestamp(x.get('timestamp') or x.get('time'))
)
# 限制最大条数
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
if period == 'M1' and (new_count > 0 or update_count > 0):
self._m1_update_time[symbol] = datetime.now()
total = len(self._klines[symbol][period])
print(f"[KlineStore] {symbol} {period} 保存了 {new_count} 条新数据, 更新 {update_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线数据"""
period = period.upper()
with self._lock:
klines = self._klines[symbol][period][-count:]
return list(klines)
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
"""获取所有K线数据"""
period = period.upper()
with self._lock:
return list(self._klines[symbol][period])
def get_latest_price(self, symbol: str) -> Optional[float]:
"""获取最新价格(从K线的最新close,优先M1"""
with self._lock:
actual_symbol = None
if symbol in self._klines:
actual_symbol = symbol
else:
symbol_base = symbol.replace('#', '')
for s in self._klines:
if s.replace('#', '') == symbol_base:
actual_symbol = s
break
if not actual_symbol:
return None
for period in ['M1', 'M5', 'M15', 'H1', 'H4']:
klines = self._klines[actual_symbol][period]
if klines:
return float(klines[-1].get('close', 0))
return None
def is_initialized(self, symbol: str, period: str) -> bool:
"""检查某个周期的数据是否已初始化"""
period = period.upper()
return self._initialized[symbol][period]
def check_all_initialized(self, symbol: str) -> bool:
"""检查所有周期是否都已初始化"""
return all(self._initialized[symbol][p] for p in self.PERIODS)
def clear_symbol(self, symbol: str):
"""清除某个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:
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 get_latest_kline_time(self, symbol: str, period: str = 'M1') -> Optional[datetime]:
"""获取指定品种和周期的最新K线时间戳"""
period = period.upper()
with self._lock:
klines = self._klines[symbol][period]
if not klines:
return None
latest_ts = klines[-1].get('timestamp') or klines[-1].get('time')
return self._parse_timestamp(latest_ts)
def check_m1_updated_within(self, symbol: str, seconds: int = 180) -> Dict:
"""检查M1 K线是否在指定秒数内更新"""
with self._lock:
has_m1_data = len(self._klines[symbol]['M1']) > 0
if not has_m1_data:
return {
"has_data": False,
"latest_time": None,
"update_time": None,
"seconds_ago": None,
"is_stale": True,
"market_status": "closed"
}
latest_time = self.get_latest_kline_time(symbol, 'M1')
update_time = self._m1_update_time.get(symbol)
if update_time is None:
return {
"has_data": True,
"latest_time": latest_time,
"update_time": None,
"seconds_ago": None,
"is_stale": True,
"market_status": "closed"
}
now = datetime.now()
seconds_ago = int((now - update_time).total_seconds())
if seconds_ago > seconds:
market_status = "stale"
else:
market_status = "active"
return {
"has_data": True,
"latest_time": latest_time,
"update_time": update_time,
"seconds_ago": seconds_ago,
"is_stale": seconds_ago > seconds,
"market_status": market_status
}
def get_period_interval(self, period: str) -> int:
"""获取周期时间间隔(秒)"""
return self.PERIOD_INTERVALS.get(period.upper(), 60)
def get_m1_update_time(self, symbol: str) -> Optional[datetime]:
"""获取M1数据最后更新时间"""
return self._m1_update_time.get(symbol)
def _normalize_timestamp(self, ts) -> str:
"""标准化时间戳为字符串"""
if isinstance(ts, datetime):
return ts.strftime("%Y-%m-%d %H:%M:%S")
return str(ts) if ts else ""
def _parse_timestamp(self, ts) -> Optional[datetime]:
"""解析时间戳为datetime对象"""
if ts is None:
return None
if isinstance(ts, datetime):
return ts
ts_str = str(ts)
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y.%m.%d %H:%M", "%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M"]:
try:
return datetime.strptime(ts_str, fmt)
except:
continue
return None