完善知行短线
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
GET /api/health 健康检查
|
||||
GET /api/candles/{sym} 取 K 线(支持 1d/1w/1M/1y 周期,日线为基底聚合)
|
||||
GET /api/stocks 全市场股票列表(基本信息 + 最新行情 + 缓存条数)
|
||||
GET /api/stock/{code}/chips 个股筹码峰(cyq_chips/cyq_perf,按复权口径换算)
|
||||
GET /api/market/overview 主页大盘总览(A 股/港美指数 + 两市市值成交统计)
|
||||
POST /api/backtest 跑回测,返回 K线+指标+买卖点+净值+绩效
|
||||
POST /api/screener/run 智能选股:自然语言 -> 条件 -> 全市场筛选
|
||||
@@ -11,6 +12,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import bisect
|
||||
import json
|
||||
from datetime import datetime
|
||||
@@ -27,7 +29,7 @@ from .auth import require_user
|
||||
from .backtest.events import EventEngineError, run_event_backtest
|
||||
from .backtest.strategies import build_strategy
|
||||
from .config import settings
|
||||
from .data import fetcher, repository
|
||||
from .data import fetcher, repository, tushare_provider
|
||||
from .data.aggregation import bars_per_year, resample_bars
|
||||
from .data.market_overview import MarketOverviewError, fetch_overview
|
||||
from .data.symbols import plain_code
|
||||
@@ -50,6 +52,8 @@ from .schemas import (
|
||||
BacktestRequest,
|
||||
BacktestResponse,
|
||||
CandleOut,
|
||||
ChipRowOut,
|
||||
ChipsResponse,
|
||||
EquityPoint,
|
||||
EventBacktestRequest,
|
||||
EventBacktestResponse,
|
||||
@@ -982,3 +986,78 @@ async def screener_preview(
|
||||
candles=candles, indicators=indicators, has_more=has_more)
|
||||
await cache.cache_set(f"pv:{cache_key}", resp.model_dump(mode="json"), ttl=600)
|
||||
return resp
|
||||
|
||||
|
||||
@router.get("/stock/{ts_code}/chips", response_model=ChipsResponse)
|
||||
async def stock_chips(
|
||||
ts_code: str, date: str | None = None, adjust: str = "qfq",
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> ChipsResponse:
|
||||
"""个股筹码峰(Tushare cyq_chips + cyq_perf,数据自 2018 年起)。
|
||||
|
||||
date=YYYY-MM-DD 为参考日(日 K 传当日;周/月 K 由前端传周期末):返回
|
||||
<=date 的最近有筹码数据的交易日截面;缺省取最新。
|
||||
价格/成本均按 adjust(bfq/qfq/hfq)用 adj_factor 本地换算,与 K 线同口径。
|
||||
"""
|
||||
if adjust not in _ADJUST_MODES:
|
||||
raise HTTPException(status_code=400, detail=f"adjust 仅支持 {'/'.join(_ADJUST_MODES)}")
|
||||
ref: str | None = None
|
||||
if date:
|
||||
try:
|
||||
ref = datetime.strptime(date.strip()[:10], "%Y-%m-%d").strftime("%Y%m%d")
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="date 格式应为 YYYY-MM-DD")
|
||||
|
||||
# 历史截面不可变;键带 candles 版本号(adj_factor 随同步更新后旧缓存失效)
|
||||
cache_key = cache.digest("chips", ts_code, ref or "latest", adjust, await cache.get_version("candles"))
|
||||
cached = await cache.cache_get(f"chips:{cache_key}")
|
||||
if cached is not None:
|
||||
return ChipsResponse.model_validate(cached)
|
||||
|
||||
try:
|
||||
perf, rows = await asyncio.to_thread(tushare_provider.fetch_chips, ts_code, ref)
|
||||
except Exception as e: # noqa: BLE001
|
||||
raise HTTPException(status_code=502, detail=f"筹码数据获取失败: {e}")
|
||||
if not perf:
|
||||
resp = ChipsResponse(
|
||||
ts_code=ts_code, trade_date=None, adjust=adjust,
|
||||
error="无筹码数据(cyq 数据自 2018 年起,或参考日早于数据起点)",
|
||||
)
|
||||
await cache.cache_set(f"chips:{cache_key}", resp.model_dump(mode="json"), ttl=3600)
|
||||
return resp
|
||||
|
||||
d = datetime.strptime(str(perf["trade_date"]), "%Y%m%d")
|
||||
|
||||
# 复权换算(与 _adjust_bars 同口径):qfq=f(d)/f_latest,hfq=f(d),bfq=1
|
||||
mult = 1.0
|
||||
if adjust != "bfq":
|
||||
factors = (await session.execute(
|
||||
select(AdjFactor).where(AdjFactor.ts_code == ts_code, AdjFactor.trade_date <= d)
|
||||
.order_by(AdjFactor.trade_date)
|
||||
)).scalars().all()
|
||||
if factors:
|
||||
latest_f = (await session.execute(
|
||||
select(AdjFactor).where(AdjFactor.ts_code == ts_code)
|
||||
.order_by(AdjFactor.trade_date.desc()).limit(1)
|
||||
)).scalars().first()
|
||||
f_at = float(factors[-1].adj_factor) # <=d 的最近因子(因子是阶梯函数)
|
||||
f_latest = float(latest_f.adj_factor) if latest_f else f_at
|
||||
mult = f_at / f_latest if adjust == "qfq" else f_at
|
||||
|
||||
def _px(v) -> float | None:
|
||||
return None if v is None or v != v else round(float(v) * mult, 3)
|
||||
|
||||
resp = ChipsResponse(
|
||||
ts_code=ts_code,
|
||||
trade_date=str(perf["trade_date"]),
|
||||
adjust=adjust,
|
||||
rows=[ChipRowOut(price=round(p * mult, 3), percent=v) for p, v in rows],
|
||||
his_low=_px(perf.get("his_low")), his_high=_px(perf.get("his_high")),
|
||||
cost_5pct=_px(perf.get("cost_5pct")), cost_15pct=_px(perf.get("cost_15pct")),
|
||||
cost_50pct=_px(perf.get("cost_50pct")), cost_85pct=_px(perf.get("cost_85pct")),
|
||||
cost_95pct=_px(perf.get("cost_95pct")),
|
||||
weight_avg=_px(perf.get("weight_avg")),
|
||||
winner_rate=_px(perf.get("winner_rate")),
|
||||
)
|
||||
await cache.cache_set(f"chips:{cache_key}", resp.model_dump(mode="json"), ttl=21600)
|
||||
return resp
|
||||
|
||||
Reference in New Issue
Block a user