|
|
"""
|
|
|
K 线数据模型 v2.2
|
|
|
支持股票 K 线、期货 K 线、复权因子等
|
|
|
"""
|
|
|
from datetime import datetime, date
|
|
|
from typing import Optional, List, Literal
|
|
|
from enum import Enum
|
|
|
from pydantic import BaseModel, Field
|
|
|
from sqlalchemy import Column, Integer, String, Numeric, BigInteger, DateTime, Boolean, Date, Index
|
|
|
from sqlalchemy.orm import declarative_base
|
|
|
|
|
|
# TimescaleDB 基础类
|
|
|
KlineBase = declarative_base()
|
|
|
|
|
|
|
|
|
# ==================== 枚举定义 ====================
|
|
|
|
|
|
class Frequency(str, Enum):
|
|
|
"""K 线周期枚举 - 支持 8 种周期"""
|
|
|
FREQ_1M = "1m"
|
|
|
FREQ_5M = "5m"
|
|
|
FREQ_15M = "15m"
|
|
|
FREQ_30M = "30m"
|
|
|
FREQ_1H = "1h" # 60分钟
|
|
|
FREQ_1D = "1d" # 日线
|
|
|
FREQ_1W = "1w" # 周线
|
|
|
FREQ_1MONTH = "1month" # 月线
|
|
|
|
|
|
|
|
|
class AdjustType(str, Enum):
|
|
|
"""复权类型"""
|
|
|
NONE = "" # 不复权
|
|
|
QFQ = "qfq" # 前复权
|
|
|
HFQ = "hfq" # 后复权
|
|
|
|
|
|
|
|
|
class AssetClass(str, Enum):
|
|
|
"""资产类别"""
|
|
|
STOCK = "stock"
|
|
|
FUTURES = "futures"
|
|
|
|
|
|
|
|
|
class Exchange(str, Enum):
|
|
|
"""交易所"""
|
|
|
# 股票交易所
|
|
|
SZ = "SZ" # 深交所
|
|
|
SH = "SH" # 上交所
|
|
|
BJ = "BJ" # 北交所
|
|
|
|
|
|
# 期货交易所
|
|
|
CFFEX = "CFFEX" # 中金所
|
|
|
SHFE = "SHFE" # 上期所
|
|
|
DCE = "DCE" # 大商所
|
|
|
CZCE = "CZCE" # 郑商所
|
|
|
INE = "INE" # 上期能源
|
|
|
GFEX = "GFEX" # 广期所
|
|
|
|
|
|
|
|
|
# ==================== SQLAlchemy ORM 模型 ====================
|
|
|
|
|
|
class StockKLine1D(KlineBase):
|
|
|
"""股票日线 K 线表"""
|
|
|
__tablename__ = "stock_klines_1d"
|
|
|
|
|
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
|
symbol_id = Column(String(20), nullable=False, index=True)
|
|
|
ts = Column(DateTime, nullable=False, index=True)
|
|
|
open = Column(Numeric(18, 4), nullable=False)
|
|
|
high = Column(Numeric(18, 4), nullable=False)
|
|
|
low = Column(Numeric(18, 4), nullable=False)
|
|
|
close = Column(Numeric(18, 4), nullable=False)
|
|
|
volume = Column(BigInteger, nullable=False)
|
|
|
amount = Column(Numeric(20, 4), nullable=False)
|
|
|
trade_date = Column(Date, nullable=False)
|
|
|
|
|
|
# 股票特有字段
|
|
|
is_limit_up = Column(Boolean, default=False)
|
|
|
is_limit_down = Column(Boolean, default=False)
|
|
|
total_market_cap = Column(Numeric(20, 2))
|
|
|
float_market_cap = Column(Numeric(20, 2))
|
|
|
inst_holding_ratio = Column(Numeric(5, 2))
|
|
|
trading_days = Column(Integer)
|
|
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
|
|
__table_args__ = (
|
|
|
Index('idx_stock_klines_symbol_ts', 'symbol_id', 'ts'),
|
|
|
)
|
|
|
|
|
|
|
|
|
class StockKLine1M(KlineBase):
|
|
|
"""股票 1 分钟 K 线表"""
|
|
|
__tablename__ = "stock_klines_1m"
|
|
|
|
|
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
|
symbol_id = Column(String(20), nullable=False, index=True)
|
|
|
ts = Column(DateTime, nullable=False, index=True)
|
|
|
open = Column(Numeric(18, 4), nullable=False)
|
|
|
high = Column(Numeric(18, 4), nullable=False)
|
|
|
low = Column(Numeric(18, 4), nullable=False)
|
|
|
close = Column(Numeric(18, 4), nullable=False)
|
|
|
volume = Column(BigInteger, nullable=False)
|
|
|
amount = Column(Numeric(20, 4), nullable=False)
|
|
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
__table_args__ = (
|
|
|
Index('idx_stock_klines_1m_symbol_ts', 'symbol_id', 'ts'),
|
|
|
)
|
|
|
|
|
|
|
|
|
class StockKLine5M(KlineBase):
|
|
|
"""股票 5 分钟 K 线表"""
|
|
|
__tablename__ = "stock_klines_5m"
|
|
|
|
|
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
|
symbol_id = Column(String(20), nullable=False, index=True)
|
|
|
ts = Column(DateTime, nullable=False, index=True)
|
|
|
open = Column(Numeric(18, 4), nullable=False)
|
|
|
high = Column(Numeric(18, 4), nullable=False)
|
|
|
low = Column(Numeric(18, 4), nullable=False)
|
|
|
close = Column(Numeric(18, 4), nullable=False)
|
|
|
volume = Column(BigInteger, nullable=False)
|
|
|
amount = Column(Numeric(20, 4), nullable=False)
|
|
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
__table_args__ = (
|
|
|
Index('idx_stock_klines_5m_symbol_ts', 'symbol_id', 'ts'),
|
|
|
)
|
|
|
|
|
|
|
|
|
class FuturesKLine1D(KlineBase):
|
|
|
"""期货日线 K 线表"""
|
|
|
__tablename__ = "futures_klines_1d"
|
|
|
|
|
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
|
symbol_id = Column(String(20), nullable=False, index=True)
|
|
|
ts = Column(DateTime, nullable=False, index=True)
|
|
|
open = Column(Numeric(18, 4), nullable=False)
|
|
|
high = Column(Numeric(18, 4), nullable=False)
|
|
|
low = Column(Numeric(18, 4), nullable=False)
|
|
|
close = Column(Numeric(18, 4), nullable=False)
|
|
|
volume = Column(BigInteger, nullable=False)
|
|
|
|
|
|
# 期货特有字段
|
|
|
open_interest = Column(BigInteger, nullable=False) # 持仓量
|
|
|
settlement_price = Column(Numeric(18, 4)) # 结算价
|
|
|
trade_date = Column(Date, nullable=False)
|
|
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
|
|
__table_args__ = (
|
|
|
Index('idx_futures_klines_symbol_ts', 'symbol_id', 'ts'),
|
|
|
)
|
|
|
|
|
|
|
|
|
class StockAdjustFactor(KlineBase):
|
|
|
"""股票复权因子表"""
|
|
|
__tablename__ = "stock_adjust_factors"
|
|
|
|
|
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
|
symbol_id = Column(String(20), nullable=False, index=True)
|
|
|
trade_date = Column(Date, nullable=False, index=True)
|
|
|
adj_factor = Column(Numeric(12, 6), nullable=False) # 前复权因子
|
|
|
hfq_factor = Column(Numeric(12, 6), nullable=False) # 后复权因子
|
|
|
dividend_ratio = Column(Numeric(8, 4)) # 分红比例
|
|
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
__table_args__ = (
|
|
|
Index('idx_adjust_factor_symbol_date', 'symbol_id', 'trade_date'),
|
|
|
)
|
|
|
|
|
|
|
|
|
class TradingCalendar(KlineBase):
|
|
|
"""交易日历表"""
|
|
|
__tablename__ = "trading_calendar"
|
|
|
|
|
|
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
|
|
market = Column(String(10), nullable=False) # SH, SZ, BJ, CFFEX 等
|
|
|
trade_date = Column(Date, nullable=False, unique=True, index=True)
|
|
|
is_open = Column(Boolean, nullable=False) # 是否交易日
|
|
|
preclose_days = Column(Integer) # 上一个交易日距离天数
|
|
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
|
|
|
# ==================== Pydantic 数据模型 ====================
|
|
|
|
|
|
class KLineItem(BaseModel):
|
|
|
"""单条 K 线数据"""
|
|
|
symbol: str = Field(..., description="证券代码")
|
|
|
time: datetime = Field(..., description="时间戳")
|
|
|
open: float = Field(..., description="开盘价")
|
|
|
high: float = Field(..., description="最高价")
|
|
|
low: float = Field(..., description="最低价")
|
|
|
close: float = Field(..., description="收盘价")
|
|
|
volume: int = Field(..., description="成交量")
|
|
|
amount: float = Field(..., description="成交额")
|
|
|
|
|
|
# 期货特有字段
|
|
|
open_interest: Optional[int] = Field(None, description="持仓量")
|
|
|
settlement_price: Optional[float] = Field(None, description="结算价")
|
|
|
|
|
|
# 股票特有字段
|
|
|
adj_factor: Optional[float] = Field(None, description="复权因子")
|
|
|
trade_date: Optional[str] = Field(None, description="交易日")
|
|
|
is_limit_up: Optional[bool] = Field(None, description="是否涨停")
|
|
|
is_limit_down: Optional[bool] = Field(None, description="是否跌停")
|
|
|
total_market_cap: Optional[float] = Field(None, description="总市值")
|
|
|
float_market_cap: Optional[float] = Field(None, description="流通市值")
|
|
|
inst_holding_ratio: Optional[float] = Field(None, description="机构持仓占比")
|
|
|
|
|
|
|
|
|
class StockKLineRequest(BaseModel):
|
|
|
"""股票 K 线查询请求"""
|
|
|
symbol: str = Field(..., description="股票代码,如 000001.SZ")
|
|
|
start: str = Field(..., description="开始日期 YYYYMMDD")
|
|
|
end: str = Field(..., description="结束日期 YYYYMMDD")
|
|
|
freq: Frequency = Field(Frequency.FREQ_1D, description="K 线周期")
|
|
|
adjust: AdjustType = Field(AdjustType.NONE, description="复权类型")
|
|
|
|
|
|
|
|
|
class StockKLineResponse(BaseModel):
|
|
|
"""股票 K 线响应"""
|
|
|
code: int = Field(0, description="响应码,0 表示成功")
|
|
|
message: str = Field("success", description="响应消息")
|
|
|
data: dict = Field(default_factory=dict)
|
|
|
|
|
|
class Config:
|
|
|
json_schema_extra = {
|
|
|
"example": {
|
|
|
"code": 0,
|
|
|
"message": "success",
|
|
|
"data": {
|
|
|
"symbol": "000001.SZ",
|
|
|
"name": "平安银行",
|
|
|
"freq": "1d",
|
|
|
"adjust": "qfq",
|
|
|
"count": 8,
|
|
|
"items": [
|
|
|
{
|
|
|
"symbol": "000001.SZ",
|
|
|
"time": "2026-03-01T00:00:00",
|
|
|
"open": 10.50,
|
|
|
"high": 10.80,
|
|
|
"low": 10.40,
|
|
|
"close": 10.65,
|
|
|
"volume": 1500000,
|
|
|
"amount": 15975000.00
|
|
|
}
|
|
|
]
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
class FuturesKLineRequest(BaseModel):
|
|
|
"""期货 K 线查询请求"""
|
|
|
symbol: str = Field(..., description="期货合约代码,如 AG2605.SHF")
|
|
|
start: str = Field(..., description="开始日期 YYYYMMDD")
|
|
|
end: str = Field(..., description="结束日期 YYYYMMDD")
|
|
|
freq: Frequency = Field(Frequency.FREQ_1D, description="K 线周期")
|
|
|
|
|
|
|
|
|
class FuturesKLineResponse(BaseModel):
|
|
|
"""期货 K 线响应"""
|
|
|
code: int = Field(0, description="响应码")
|
|
|
message: str = Field("success", description="响应消息")
|
|
|
data: dict = Field(default_factory=dict)
|
|
|
|
|
|
class Config:
|
|
|
json_schema_extra = {
|
|
|
"example": {
|
|
|
"code": 0,
|
|
|
"message": "success",
|
|
|
"data": {
|
|
|
"symbol": "AG2605.SHF",
|
|
|
"name": "银2605",
|
|
|
"freq": "1d",
|
|
|
"count": 8,
|
|
|
"items": [
|
|
|
{
|
|
|
"symbol": "AG2605.SHF",
|
|
|
"time": "2026-03-01T00:00:00",
|
|
|
"open": 7850.0,
|
|
|
"high": 7920.0,
|
|
|
"low": 7830.0,
|
|
|
"close": 7890.0,
|
|
|
"volume": 125000,
|
|
|
"open_interest": 85000,
|
|
|
"settlement_price": 7880.0
|
|
|
}
|
|
|
]
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
class TradingCalendarRequest(BaseModel):
|
|
|
"""交易日历查询请求"""
|
|
|
market: Exchange = Field(Exchange.SH, description="市场")
|
|
|
start: str = Field(..., description="开始日期 YYYYMMDD")
|
|
|
end: str = Field(..., description="结束日期 YYYYMMDD")
|
|
|
|
|
|
|
|
|
class TradingCalendarResponse(BaseModel):
|
|
|
"""交易日历响应"""
|
|
|
code: int = 0
|
|
|
message: str = "success"
|
|
|
data: dict = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
class BatchKLineRequest(BaseModel):
|
|
|
"""批量 K 线查询请求"""
|
|
|
symbols: List[str] = Field(..., description="证券代码列表,最多 100 个")
|
|
|
start: str = Field(..., description="开始日期 YYYYMMDD")
|
|
|
end: str = Field(..., description="结束日期 YYYYMMDD")
|
|
|
freq: Frequency = Field(Frequency.FREQ_1D, description="K 线周期")
|
|
|
adjust: AdjustType = Field(AdjustType.NONE, description="复权类型(股票)")
|
|
|
|
|
|
|
|
|
class BatchKLineResponse(BaseModel):
|
|
|
"""批量 K 线响应"""
|
|
|
code: int = 0
|
|
|
message: str = "success"
|
|
|
data: dict = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
class DataSourceStatus(BaseModel):
|
|
|
"""数据源状态"""
|
|
|
name: str = Field(..., description="数据源名称")
|
|
|
status: Literal["healthy", "degraded", "down"] = Field(..., description="状态")
|
|
|
latency_ms: Optional[int] = Field(None, description="延迟(毫秒)")
|
|
|
last_check: datetime = Field(..., description="最后检查时间")
|
|
|
error_message: Optional[str] = Field(None, description="错误信息")
|
|
|
|
|
|
|
|
|
# ==================== 验证函数 ====================
|
|
|
|
|
|
VALID_FREQUENCIES = ["1m", "5m", "15m", "30m", "1h", "1d", "1w", "1month"]
|
|
|
VALID_ADJUST_TYPES = ["", "qfq", "hfq"]
|
|
|
|
|
|
|
|
|
def validate_frequency(freq: str) -> bool:
|
|
|
"""验证周期是否有效"""
|
|
|
return freq in VALID_FREQUENCIES
|
|
|
|
|
|
|
|
|
def validate_adjust_type(adjust: str) -> bool:
|
|
|
"""验证复权类型是否有效"""
|
|
|
return adjust in VALID_ADJUST_TYPES
|
|
|
|
|
|
|
|
|
def parse_date(date_str: str) -> date:
|
|
|
"""解析日期字符串"""
|
|
|
try:
|
|
|
if len(date_str) == 8: # YYYYMMDD
|
|
|
return datetime.strptime(date_str, "%Y%m%d").date()
|
|
|
else: # YYYY-MM-DD
|
|
|
return datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
|
except ValueError:
|
|
|
raise ValueError(f"无效的日期格式: {date_str}") |