update
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
DATABASE_URL=postgresql+asyncpg://postgres:Cirry0115@cirry.cn:5432/stock
|
||||
TUSHARE_TOKEN=d0bc5620d6523ae40f379ed4415576f58dca2361f2f47a68cdcd0a98
|
||||
TUSHARE_TOKEN=22edda0afe44c0609a187ff1ac0bb2a8fc61430f490ec19f7fec8390
|
||||
DATA_ADJUST=qfq
|
||||
DATA_DEFAULT_START=20200101
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import json
|
||||
|
||||
import pandas as pd
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .backtest.engine import BacktestConfig, run_backtest
|
||||
@@ -20,10 +21,12 @@ from .backtest.strategies import build_strategy
|
||||
from .config import settings
|
||||
from .data import fetcher, repository
|
||||
from .data.aggregation import bars_per_year, resample_bars
|
||||
from .data.symbols import plain_code
|
||||
from .data.synthetic import seed_if_empty
|
||||
from .db import get_session
|
||||
from .domain import Bar
|
||||
from .models import BacktestRun
|
||||
from . import indicators as ind
|
||||
from .models import BacktestRun, DailySnapshot, MarketDaily, StockBasic
|
||||
from .schemas import (
|
||||
BacktestRequest,
|
||||
BacktestResponse,
|
||||
@@ -31,6 +34,8 @@ from .schemas import (
|
||||
EquityPoint,
|
||||
IndicatorOut,
|
||||
MetricsOut,
|
||||
PreviewInfoOut,
|
||||
PreviewResponse,
|
||||
ScreenerRunRequest,
|
||||
ScreenerRunResponse,
|
||||
ScreenerSyncRequest,
|
||||
@@ -219,3 +224,128 @@ async def screener_sync_status(session: AsyncSession = Depends(get_session)) ->
|
||||
"""同步任务状态 + 数据实况(最新交易日/行数/ready)。"""
|
||||
status = await market_sync.get_sync_status(session)
|
||||
return ScreenerSyncStatus(**{k: status.get(k) for k in ScreenerSyncStatus.model_fields})
|
||||
|
||||
|
||||
@router.get("/screener/preview/{ts_code}", response_model=PreviewResponse)
|
||||
async def screener_preview(
|
||||
ts_code: str, limit: int = 260, session: AsyncSession = Depends(get_session)
|
||||
) -> PreviewResponse:
|
||||
"""个股详情预览:日线(qfq 全量缓存,未缓存/过期自动拉取,失败退 market_daily 近段)
|
||||
+ 全套指标(indicators.py 单一事实源)+ 最新截面信息卡。"""
|
||||
symbol = plain_code(ts_code)
|
||||
|
||||
# 先取 market_daily 最新行:既做缓存过期判断,也做信息卡数据源
|
||||
md = (
|
||||
await session.execute(
|
||||
select(MarketDaily).where(MarketDaily.ts_code == ts_code).order_by(MarketDaily.trade_date.desc()).limit(1)
|
||||
)
|
||||
).scalars().first()
|
||||
|
||||
# --- 日线:candles(qfq 全量) 优先;未缓存拉取,缓存落后于全市场最新交易日则强制刷新(每日至多一次) ---
|
||||
rows = await repository.get_candles(session, symbol, "1d", limit=100000)
|
||||
source = "qfq"
|
||||
try:
|
||||
if not rows:
|
||||
await fetcher.sync_symbol(session, symbol, source="auto")
|
||||
rows = await repository.get_candles(session, symbol, "1d", limit=100000)
|
||||
elif md is not None and rows and rows[-1].ts.date() < md.trade_date.date():
|
||||
await fetcher.sync_symbol(session, symbol, source="auto", force=True)
|
||||
rows = await repository.get_candles(session, symbol, "1d", limit=100000)
|
||||
except Exception: # noqa: BLE001 —— tushare/写库失败时回滚会话(否则毒化后兜底查询 500)
|
||||
await session.rollback()
|
||||
if not rows:
|
||||
rows = []
|
||||
bars = _rows_to_bars(rows)
|
||||
|
||||
if not bars:
|
||||
source = "market"
|
||||
res = await session.execute(
|
||||
select(MarketDaily).where(MarketDaily.ts_code == ts_code).order_by(MarketDaily.trade_date)
|
||||
)
|
||||
bars = [
|
||||
Bar(ts=r.trade_date, open=r.open, high=r.high, low=r.low, close=r.close, volume=r.vol * 100.0)
|
||||
for r in res.scalars()
|
||||
]
|
||||
if not bars:
|
||||
raise HTTPException(status_code=404, detail=f"无数据: {ts_code}(可先点「同步市场数据」)")
|
||||
|
||||
# --- 指标(在全量历史上计算后截尾,保证预热正确) ---
|
||||
df = pd.DataFrame({"close": [b.close for b in bars], "high": [b.high for b in bars], "low": [b.low for b in bars]})
|
||||
closes, highs, lows = df["close"], df["high"], df["low"]
|
||||
macd = ind.macd(closes)
|
||||
kdj = ind.kdj(highs, lows, closes)
|
||||
boll = ind.bollinger(closes)
|
||||
indicators: dict[str, dict[str, list[float | None]]] = {
|
||||
"ma": {f"ma{p}": _series_to_jsonable(ind.ma(closes, p)) for p in (5, 10, 20, 60)},
|
||||
"macd": {
|
||||
"dif": _series_to_jsonable(macd["macd"]),
|
||||
"dea": _series_to_jsonable(macd["signal"]),
|
||||
"hist": _series_to_jsonable(macd["hist"]),
|
||||
},
|
||||
"kdj": {k: _series_to_jsonable(kdj[k]) for k in ("k", "d", "j")},
|
||||
"rsi": {
|
||||
"rsi6": _series_to_jsonable(ind.rsi(closes, 6)),
|
||||
"rsi12": _series_to_jsonable(ind.rsi(closes, 12)),
|
||||
"rsi24": _series_to_jsonable(ind.rsi(closes, 24)),
|
||||
},
|
||||
"boll": {k: _series_to_jsonable(boll[k]) for k in ("upper", "mid", "lower")},
|
||||
}
|
||||
limit = max(30, min(limit, len(bars)))
|
||||
for group in indicators.values():
|
||||
for key in group:
|
||||
group[key] = group[key][-limit:]
|
||||
|
||||
# --- 信息卡:stock_basic + 最新 market_daily + 与其对齐的快照(避免混用不同交易日) ---
|
||||
sb = (await session.execute(select(StockBasic).where(StockBasic.ts_code == ts_code))).scalars().first()
|
||||
ds = None
|
||||
if md is not None:
|
||||
# 优先取与行情同日的快照;缺当日快照时退最新(字段可能与行情差日期,罕见)
|
||||
ds = (
|
||||
await session.execute(
|
||||
select(DailySnapshot).where(
|
||||
DailySnapshot.ts_code == ts_code, DailySnapshot.trade_date == md.trade_date
|
||||
)
|
||||
)
|
||||
).scalars().first()
|
||||
if ds is None:
|
||||
ds = (
|
||||
await session.execute(
|
||||
select(DailySnapshot).where(DailySnapshot.ts_code == ts_code).order_by(DailySnapshot.trade_date.desc()).limit(1)
|
||||
)
|
||||
).scalars().first()
|
||||
|
||||
def _yi(v) -> float | None:
|
||||
if v is None:
|
||||
return None
|
||||
v = float(v)
|
||||
return None if v != v else round(v / 1e4, 2) # 万元 -> 亿元
|
||||
|
||||
info = PreviewInfoOut(
|
||||
ts_code=ts_code,
|
||||
symbol=symbol,
|
||||
name=sb.name if sb else ts_code,
|
||||
industry=sb.industry if sb else None,
|
||||
area=sb.area if sb else None,
|
||||
market=sb.market if sb else None,
|
||||
list_date=sb.list_date if sb else None,
|
||||
trade_date=md.trade_date if md else None,
|
||||
open=md.open if md else None,
|
||||
high=md.high if md else None,
|
||||
low=md.low if md else None,
|
||||
close=md.close if md else None,
|
||||
pre_close=md.pre_close if md else None,
|
||||
pct_chg=md.pct_chg if md else None,
|
||||
volume_hand=round(md.vol, 0) if md else None,
|
||||
amount_yi=round(md.amount / 100000, 2) if md else None, # 千元 -> 亿元
|
||||
turnover_rate=ds.turnover_rate if ds else None,
|
||||
pe_ttm=ds.pe_ttm if ds else None,
|
||||
pb=ds.pb if ds else None,
|
||||
total_mv=_yi(ds.total_mv) if ds else None,
|
||||
circ_mv=_yi(ds.circ_mv) if ds else None,
|
||||
)
|
||||
|
||||
candles = [
|
||||
CandleOut(ts=b.ts, open=b.open, high=b.high, low=b.low, close=b.close, volume=b.volume)
|
||||
for b in bars[-limit:]
|
||||
]
|
||||
return PreviewResponse(ts_code=ts_code, symbol=symbol, source=source, info=info, candles=candles, indicators=indicators)
|
||||
|
||||
@@ -9,8 +9,11 @@ class Base(DeclarativeBase):
|
||||
"""所有 ORM 模型的基类。"""
|
||||
|
||||
|
||||
# echo=False;生产环境可用连接池参数调优
|
||||
engine = create_async_engine(settings.database_url, echo=False, future=True)
|
||||
# echo=False;远程 PG 的空闲连接可能被中间层断开,pre_ping + recycle 自动剔除死连接
|
||||
engine = create_async_engine(
|
||||
settings.database_url, echo=False, future=True,
|
||||
pool_pre_ping=True, pool_recycle=1800,
|
||||
)
|
||||
async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
||||
|
||||
|
||||
|
||||
@@ -167,3 +167,38 @@ class ScreenerSyncStatus(BaseModel):
|
||||
last_trade_date: datetime | None = None
|
||||
last_synced_at: datetime | None = None
|
||||
stats: dict[str, int] = Field(default_factory=dict) # stocks/daily_rows/snapshot_rows/dates
|
||||
|
||||
|
||||
# ---------- 个股详情预览(选股结果点入,全屏同花顺/通达信式) ----------
|
||||
class PreviewInfoOut(BaseModel):
|
||||
ts_code: str
|
||||
symbol: str
|
||||
name: str
|
||||
industry: str | None = None
|
||||
area: str | None = None
|
||||
market: str | None = None # 主板/创业板/科创板/北交所
|
||||
list_date: str | None = None
|
||||
trade_date: datetime | None = None # 行情/信息卡数据基准交易日
|
||||
open: float | None = None
|
||||
high: float | None = None
|
||||
low: float | None = None
|
||||
close: float | None = None
|
||||
pre_close: float | None = None
|
||||
pct_chg: float | None = None # 日涨跌幅 %
|
||||
volume_hand: float | None = None # 成交量(手)
|
||||
amount_yi: float | None = None # 成交额(亿元)
|
||||
turnover_rate: float | None = None # 换手率 %
|
||||
pe_ttm: float | None = None
|
||||
pb: float | None = None
|
||||
total_mv: float | None = None # 总市值(亿元)
|
||||
circ_mv: float | None = None # 流通市值(亿元)
|
||||
|
||||
|
||||
class PreviewResponse(BaseModel):
|
||||
ts_code: str
|
||||
symbol: str
|
||||
source: str # qfq=回测缓存全量前复权 | market=近段未复权兜底
|
||||
info: PreviewInfoOut
|
||||
candles: list[CandleOut]
|
||||
indicators: dict[str, dict[str, list[float | None]]] = Field(default_factory=dict)
|
||||
# indicators 形如 {"ma": {"ma5": [...], ...}, "macd": {"dif": ...}, "kdj": {...}, "rsi": {...}, "boll": {...}}
|
||||
|
||||
Reference in New Issue
Block a user