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.

366 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
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}")