259 lines
8.2 KiB
Python
259 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
K线服务模块
|
|
处理K线相关的业务逻辑:时效性检查、连续性检查、格式转换等
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Dict, Optional
|
|
|
|
from ..store import KlineStore
|
|
from ..models import KlineData
|
|
|
|
|
|
class KlineService:
|
|
"""K线服务(处理业务逻辑)"""
|
|
|
|
def __init__(self, store: KlineStore):
|
|
self.store = store
|
|
|
|
def process_kline_data(self, symbol: str, period: str, klines: List[Dict],
|
|
is_full: bool = False) -> Dict:
|
|
"""
|
|
处理K线数据(包含业务逻辑)
|
|
|
|
Args:
|
|
symbol: 交易品种
|
|
period: 周期
|
|
klines: K线数据列表
|
|
is_full: 是否为全量数据
|
|
|
|
Returns:
|
|
处理结果
|
|
"""
|
|
period = period.upper()
|
|
|
|
# 保存数据
|
|
result = self.store.save_klines(symbol, period, klines, is_full)
|
|
|
|
return result
|
|
|
|
def check_staleness(self, symbol: str, period: str, klines: List[Dict],
|
|
timezone_offset_hours: float = 0) -> Dict:
|
|
"""
|
|
检查K线时效性
|
|
|
|
Args:
|
|
symbol: 交易品种
|
|
period: 周期
|
|
klines: K线数据
|
|
timezone_offset_hours: 时区偏移
|
|
|
|
Returns:
|
|
{
|
|
"is_stale": bool,
|
|
"latest_kline_time": datetime,
|
|
"kline_time_local": datetime,
|
|
"time_diff_seconds": int
|
|
}
|
|
"""
|
|
if not klines:
|
|
return {"is_stale": False, "message": "无数据"}
|
|
|
|
period_interval = self.store.get_period_interval(period)
|
|
latest_kline = klines[-1] if klines else None
|
|
|
|
if not latest_kline:
|
|
return {"is_stale": False}
|
|
|
|
ts = latest_kline.get('timestamp') or latest_kline.get('time')
|
|
latest_kline_time = self._parse_timestamp(ts)
|
|
|
|
if not latest_kline_time:
|
|
return {"is_stale": False}
|
|
|
|
now_local = datetime.now()
|
|
kline_time_local = latest_kline_time - timedelta(hours=timezone_offset_hours)
|
|
time_diff = (now_local - kline_time_local).total_seconds()
|
|
|
|
return {
|
|
"is_stale": time_diff > period_interval,
|
|
"latest_kline_time": latest_kline_time,
|
|
"kline_time_local": kline_time_local,
|
|
"time_diff_seconds": int(time_diff),
|
|
"period_interval": period_interval
|
|
}
|
|
|
|
def check_continuity(self, symbol: str, period: str, new_klines: List[Dict]) -> Dict:
|
|
"""
|
|
检查增量K线数据是否连续
|
|
|
|
Args:
|
|
symbol: 品种名称
|
|
period: 周期
|
|
new_klines: 新推送的K线数据列表
|
|
|
|
Returns:
|
|
{
|
|
"is_continuous": bool,
|
|
"gap_count": int,
|
|
"last_existing_time": datetime,
|
|
"first_new_time": datetime
|
|
}
|
|
"""
|
|
period = period.upper()
|
|
|
|
if not new_klines:
|
|
return {"is_continuous": True, "gap_count": 0}
|
|
|
|
interval = self.store.get_period_interval(period)
|
|
|
|
existing = self.store.get_all_klines(symbol, period)
|
|
if not existing:
|
|
return {"is_continuous": True, "gap_count": 0}
|
|
|
|
# 获取现有数据最后时间
|
|
last_existing_time = self._parse_timestamp(
|
|
existing[-1].get('timestamp') or existing[-1].get('time')
|
|
)
|
|
if last_existing_time is None:
|
|
return {"is_continuous": True, "gap_count": 0}
|
|
|
|
# 获取新数据最早时间
|
|
first_new_time = None
|
|
for k in new_klines:
|
|
ts = self._parse_timestamp(k.get('timestamp') or k.get('time'))
|
|
if ts:
|
|
if first_new_time is None or ts < first_new_time:
|
|
first_new_time = ts
|
|
|
|
if first_new_time is None:
|
|
return {"is_continuous": True, "gap_count": 0}
|
|
|
|
# 计算时间差
|
|
time_diff = (first_new_time - last_existing_time).total_seconds()
|
|
|
|
if time_diff <= 0:
|
|
return {
|
|
"is_continuous": True,
|
|
"gap_count": 0,
|
|
"last_existing_time": last_existing_time,
|
|
"first_new_time": first_new_time
|
|
}
|
|
|
|
gap_periods = int(time_diff / interval)
|
|
|
|
return {
|
|
"is_continuous": gap_periods <= 1,
|
|
"gap_count": max(0, gap_periods - 1),
|
|
"last_existing_time": last_existing_time,
|
|
"first_new_time": first_new_time,
|
|
"expected_gap": gap_periods
|
|
}
|
|
|
|
def convert_to_kline_objects(self, klines: List[Dict], symbol: str, period: str) -> List[KlineData]:
|
|
"""
|
|
将K线字典列表转换为KlineData对象列表
|
|
|
|
Args:
|
|
klines: K线字典列表
|
|
symbol: 品种
|
|
period: 周期
|
|
|
|
Returns:
|
|
KlineData对象列表
|
|
"""
|
|
return [
|
|
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))
|
|
)
|
|
for k in klines
|
|
]
|
|
|
|
def get_klines(self, symbol: str, period: str, count: int = 100) -> List[Dict]:
|
|
"""获取K线数据"""
|
|
return self.store.get_klines(symbol, period, count)
|
|
|
|
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
|
|
"""获取所有K线数据"""
|
|
return self.store.get_all_klines(symbol, period)
|
|
|
|
def get_all_kline_objects(self, symbol: str, period: str) -> List[KlineData]:
|
|
"""获取所有K线数据并转换为KlineData对象"""
|
|
klines = self.store.get_all_klines(symbol, period)
|
|
return self.convert_to_kline_objects(klines, symbol, period)
|
|
|
|
def get_latest_price(self, symbol: str) -> Optional[float]:
|
|
"""获取最新价格"""
|
|
return self.store.get_latest_price(symbol)
|
|
|
|
def is_initialized(self, symbol: str, period: str) -> bool:
|
|
"""检查是否已初始化"""
|
|
return self.store.is_initialized(symbol, period)
|
|
|
|
def get_symbols(self) -> List[str]:
|
|
"""获取所有品种"""
|
|
return self.store.get_symbols()
|
|
|
|
def get_status(self) -> Dict:
|
|
"""获取状态"""
|
|
return self.store.get_status()
|
|
|
|
def check_m1_updated_within(self, symbol: str, seconds: int = 180) -> Dict:
|
|
"""检查M1数据更新情况"""
|
|
return self.store.check_m1_updated_within(symbol, seconds)
|
|
|
|
def get_period_interval(self, period: str) -> int:
|
|
"""获取周期时间间隔"""
|
|
return self.store.get_period_interval(period)
|
|
|
|
def check_symbols_status(self, symbols: List[str], stale_threshold: int = 180) -> Dict[str, List[str]]:
|
|
"""
|
|
检查多个品种的数据更新状态
|
|
|
|
Args:
|
|
symbols: 品种列表
|
|
stale_threshold: 过期阈值(秒),默认180秒(3分钟)
|
|
|
|
Returns:
|
|
{"active": [...], "stale": [...], "closed": [...]}
|
|
- active: 指定秒数内有数据更新
|
|
- stale: 超过指定秒数未更新
|
|
- closed: 无数据
|
|
"""
|
|
result = {"active": [], "stale": [], "closed": []}
|
|
|
|
for symbol in symbols:
|
|
m1_status = self.store.check_m1_updated_within(symbol, stale_threshold)
|
|
market_status = m1_status.get("market_status", "closed")
|
|
|
|
if market_status == "active":
|
|
result["active"].append(symbol)
|
|
elif market_status == "stale":
|
|
result["stale"].append(symbol)
|
|
else:
|
|
result["closed"].append(symbol)
|
|
|
|
return result
|
|
|
|
def _parse_timestamp(self, ts) -> Optional[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 |