You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

310 lines
10 KiB

"""
K 线数据服务 v2 - 缓存优先策略
"""
import logging
from datetime import datetime
from typing import List, Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.db.init_db import TimescaleSessionLocal
from app.services.cache_service import cache_service
from app.services.amazing_data_service import amazing_data_service
logger = logging.getLogger(__name__)
class KlineService:
"""K 线数据服务"""
@staticmethod
def get_kline_data(
symbol: str,
period: str,
start: datetime,
end: datetime,
page: int = 1,
page_size: int = 1000
) -> List[dict]:
"""
获取 K 线数据支持分页
Args:
symbol: 品种代码
period: 周期 (1m, 5m, 1h, 1d )
start: 开始时间
end: 结束时间
page: 页码 (默认 1)
page_size: 每页数量 (默认 1000)
Raises:
ValueError: start > end 时抛出异常
"""
# 边界条件验证:开始时间不能大于结束时间
if start > end:
raise ValueError("开始时间不能大于结束时间 (startTime > endTime)")
with TimescaleSessionLocal() as db:
offset = (page - 1) * page_size
query = text("""
SELECT time, open, high, low, close, volume, amount, open_interest
FROM kline_data
WHERE symbol = :symbol
AND period = :period
AND time >= :start
AND time <= :end
ORDER BY time ASC
LIMIT :limit OFFSET :offset
""")
result = db.execute(
query,
{
"symbol": symbol,
"period": period,
"start": start,
"end": end,
"limit": page_size,
"offset": offset
}
)
rows = result.fetchall()
return [
{
"time": row[0].isoformat(),
"open": float(row[1]),
"high": float(row[2]),
"low": float(row[3]),
"close": float(row[4]),
"volume": int(row[5]),
"amount": float(row[6]) if row[6] else None,
"open_interest": int(row[7]) if row[7] else None
}
for row in rows
]
@staticmethod
def get_latest_kline(symbol: str, period: str) -> Optional[dict]:
"""获取最新一条 K 线数据"""
with TimescaleSessionLocal() as db:
query = text("""
SELECT time, open, high, low, close, volume, amount, open_interest
FROM kline_data
WHERE symbol = :symbol AND period = :period
ORDER BY time DESC
LIMIT 1
""")
result = db.execute(query, {"symbol": symbol, "period": period})
row = result.fetchone()
if row:
return {
"time": row[0].isoformat(),
"open": float(row[1]),
"high": float(row[2]),
"low": float(row[3]),
"close": float(row[4]),
"volume": int(row[5]),
"amount": float(row[6]) if row[6] else None,
"open_interest": int(row[7]) if row[7] else None
}
return None
@staticmethod
def insert_kline_data(
symbol: str,
period: str,
kline_data: List[dict]
) -> int:
"""
批量插入 K 线数据
Returns:
插入的记录数
"""
with TimescaleSessionLocal() as db:
query = text("""
INSERT INTO kline_data
(time, symbol, period, open, high, low, close, volume, amount, open_interest)
VALUES
(:time, :symbol, :period, :open, :high, :low, :close, :volume, :amount, :open_interest)
""")
count = 0
for kline in kline_data:
db.execute(
query,
{
"time": kline["time"],
"symbol": symbol,
"period": period,
"open": kline["open"],
"high": kline["high"],
"low": kline["low"],
"close": kline["close"],
"volume": kline["volume"],
"amount": kline.get("amount", 0),
"open_interest": kline.get("open_interest", 0)
}
)
count += 1
db.commit()
return count
@staticmethod
def get_symbols() -> List[str]:
"""获取所有品种代码"""
with TimescaleSessionLocal() as db:
query = text("""
SELECT DISTINCT symbol FROM kline_data ORDER BY symbol
""")
result = db.execute(query)
return [row[0] for row in result.fetchall()]
@staticmethod
def get_periods() -> List[str]:
"""获取所有周期"""
with TimescaleSessionLocal() as db:
query = text("""
SELECT DISTINCT period FROM kline_data ORDER BY period
""")
result = db.execute(query)
return [row[0] for row in result.fetchall()]
# ==================== V2 缓存优先策略 ====================
@staticmethod
async def get_kline_data_v2(
symbol: str,
period: str,
start_date: datetime,
end_date: datetime,
page: int = 1,
page_size: int = 1000,
use_cache: bool = True
) -> List[dict]:
"""
获取 K 线数据 v2 - 缓存优先策略
核心逻辑
1. 先查询 Redis 缓存
2. 缓存命中直接返回
3. 缓存未命中则调用 amazingData 获取数据
4. 写入缓存并返回
Args:
symbol: 品种代码
period: 周期 (1m, 5m, 1h, 1d )
start_date: 开始时间
end_date: 结束时间
page: 页码
page_size: 每页数量
use_cache: 是否使用缓存
Returns:
K 线数据列表
"""
# 边界条件验证
if start_date > end_date:
raise ValueError("开始时间不能大于结束时间 (startTime > endTime)")
# 1. 尝试从缓存获取
if use_cache:
cached_data = await cache_service.get_kline(
symbol=symbol,
period=period,
start_date=start_date,
end_date=end_date
)
if cached_data:
logger.info(f"Cache hit for {symbol} {period} {start_date} to {end_date}")
# 分页处理
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
return cached_data[start_idx:end_idx]
logger.info(f"Cache miss for {symbol} {period}, fetching from amazingData")
# 2. 缓存未命中,调用 amazingData 获取数据
try:
# 确保连接
if not amazing_data_service.ensure_connected():
logger.warning("amazingData not connected, trying database fallback")
# 回退到数据库查询
return KlineService.get_kline_data(symbol, period, start_date, end_date, page, page_size)
# 从 amazingData 获取数据
kline_data = amazing_data_service.get_kline_data(
symbol=symbol,
period=period,
start_date=start_date.strftime("%Y-%m-%d"),
end_date=end_date.strftime("%Y-%m-%d")
)
if not kline_data:
logger.warning(f"No data from amazingData for {symbol} {period}")
# 回退到数据库查询
return KlineService.get_kline_data(symbol, period, start_date, end_date, page, page_size)
# 3. 写入缓存
if use_cache:
await cache_service.set_kline(
symbol=symbol,
period=period,
start_date=start_date,
end_date=end_date,
data=kline_data
)
logger.info(f"Cached {len(kline_data)} records for {symbol} {period}")
# 4. 分页返回
start_idx = (page - 1) * page_size
end_idx = start_idx + page_size
return kline_data[start_idx:end_idx]
except Exception as e:
logger.error(f"Failed to fetch from amazingData: {e}")
# 回退到数据库查询
return KlineService.get_kline_data(symbol, period, start_date, end_date, page, page_size)
@staticmethod
async def get_latest_kline_v2(
symbol: str,
period: str,
use_cache: bool = True
) -> Optional[dict]:
"""
获取最新一条 K 线数据缓存优先
Args:
symbol: 品种代码
period: 周期
use_cache: 是否使用缓存
Returns:
最新一条 K 线数据如果没有则返回 None
"""
from datetime import timedelta
# 查询最近 7 天的数据
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=7)
# 获取数据
data = await KlineService.get_kline_data_v2(
symbol=symbol,
period=period,
start_date=start_date,
end_date=end_date,
page=1,
page_size=1,
use_cache=use_cache
)
return data[0] if data else None