From 359f9ae2e4453c51576bd9e4dfa59e98a1bb3bbb Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Mon, 7 Sep 2026 13:34:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api.py | 374 +++++- backend/app/backtest/broker.py | 7 +- backend/app/backtest/engine.py | 2 + backend/app/commission.py | 22 +- backend/app/data/fetcher.py | 66 +- backend/app/data/market_overview.py | 6 +- backend/app/data/symbols.py | 18 +- backend/app/models.py | 157 +++ backend/app/schemas.py | 205 ++++ backend/app/screener/market_sync.py | 103 +- backend/app/trades.py | 11 +- backend_run.log | 1035 ++++++++++++----- frontend/src/App.vue | 2 +- frontend/src/api/client.ts | 103 ++ frontend/src/api/types.ts | 215 ++++ frontend/src/components/DetailKLine.vue | 69 +- frontend/src/components/MarketOverview.vue | 110 +- frontend/src/components/MarketSyncBar.vue | 85 +- .../src/components/StockDetailOverlay.vue | 158 ++- frontend/src/router.ts | 3 + frontend/src/stores/settings.ts | 2 + frontend/src/views/HomeView.vue | 13 +- frontend_dev.log | 7 +- 23 files changed, 2260 insertions(+), 513 deletions(-) diff --git a/backend/app/api.py b/backend/app/api.py index a87fe0e..de3ecf2 100644 --- a/backend/app/api.py +++ b/backend/app/api.py @@ -3,8 +3,19 @@ GET /api/health 健康检查 GET /api/candles/{sym} 取 K 线(支持 1d/1w/1M/1y 周期,日线为基底聚合) GET /api/stocks 全市场股票列表(基本信息 + 最新行情 + 缓存条数) - GET /api/market/overview 主页大盘总览(A 股/港美指数 + 两市市值成交统计) + GET /api/stocks/{ts_code}/company 个股公司简介(tushare stock_company 按需懒加载) + GET /api/stocks/{ts_code}/finance 个股财务数据(fina_indicator+三大报表关键值,近五年懒加载) + GET /api/stocks/{ts_code}/dividends 个股分红送股(tushare dividend 全历史懒加载) + GET /api/stocks/{ts_code}/reference/{kind} 个股参考数据(top10/质押/回购/解禁/大宗/股东等 11 类懒加载) + GET /api/etfs 全市场场内 ETF 列表(东财快照 + candles 最新行情) + POST /api/etf/sync 启动全市场 ETF 数据同步(后台任务) + GET /api/etf/sync/status ETF 同步任务状态与数据实况 + GET /api/market/overview 主页大盘总览(中美指数 + 两市市值成交统计) GET /api/market/index-candles 上证指数全量 K 线(1d/1w/1M/1y 聚合) + GET /api/market/global-indexes 国际指数卡片列表(index_global 21 指数) + GET /api/market/indexes/{code} 指数详情(行情 + index_basic + 估值) + GET /api/market/indexes/{code}/candles 白名单指数全量 K 线(1d/1w/1M/1y 聚合) + GET /api/market/indexes/{code}/weights 指数成分股权重(index_weight 最近月度) POST /api/backtest 跑回测,返回 K线+指标+买卖点+净值+绩效 POST /api/screener/run 智能选股:自然语言 -> 条件 -> 全市场筛选 POST /api/screener/sync 启动全市场数据同步(后台任务) @@ -31,11 +42,17 @@ 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 company as company_mod +from .data import dividend as dividend_mod +from .data import finance as finance_mod +from .data import reference as reference_mod from .data import fetcher, repository +from .data import etf_sync as etf_sync_mod +from .data import index_global as index_mod from .data.aggregation import bars_per_year, resample_bars from .data.index_series import SH_INDEX, get_index_daily from .data.market_overview import MarketOverviewError, fetch_overview -from .data.symbols import plain_code +from .data.symbols import is_etf_symbol, plain_code, to_ts_code from .db import async_session, get_session from .domain import Bar from . import indicators as ind @@ -58,6 +75,17 @@ from .schemas import ( EquityPoint, EventBacktestRequest, EventBacktestResponse, + EtfListItemOut, + EtfListResponse, + EtfSyncRequest, + EtfSyncStatus, + GlobalIndexListResponse, + IndexBasicOut, + IndexDetailResponse, + IndexQuoteBriefOut, + IndexValuationPointOut, + IndexWeightItemOut, + IndexWeightsResponse, IndicatorOut, MarketOverviewResponse, MetricsOut, @@ -72,6 +100,10 @@ from .schemas import ( ScreenerSyncRequest, ScreenerSyncStatus, SignalOut, + StockCompanyOut, + StockDividendOut, + StockFinanceOut, + StockReferenceOut, StockListItemOut, StockListResponse, StockFacetsResponse, @@ -137,20 +169,34 @@ _FULL_MA_SET = (5, 10, 20, 30, 60, 120, 250) _INDEX_TIMEFRAMES = ("1d", "1w", "1M", "1y") # 信息卡一条 SQL 拿全:stock_basic 基本信息 + 「优先与行情同日、缺则最新日」的 daily_snapshot -# (LATERAL 单条替换原两条查询,语义不变:target 为 NULL 时全按最新日兜底) +# (LATERAL 单条替换原两条查询,语义不变:target 为 NULL 时全按最新日兜底)。 +# ETF 走 etf_basic 分支(代码前缀与股票不重叠,两分支至多一个命中): +# 名称/上市日来自表内,市值(元)换算成万元与快照口径一致,无 PE/PB。 _INFO_SQL = text( """ - SELECT sb.ts_code, sb.symbol, sb.name, sb.industry, sb.area, sb.market, sb.list_date, - ds.turnover_rate, ds.pe_ttm, ds.pb, ds.total_mv, ds.circ_mv - FROM stock_basic sb - LEFT JOIN LATERAL ( - SELECT turnover_rate, pe_ttm, pb, total_mv, circ_mv - FROM daily_snapshot - WHERE ts_code = sb.ts_code - ORDER BY (trade_date = cast(:target AS timestamp)) DESC, trade_date DESC - LIMIT 1 - ) ds ON true - WHERE sb.ts_code = :code + SELECT ts_code, symbol, name, industry, area, market, list_date, + turnover_rate, pe_ttm, pb, total_mv, circ_mv + FROM ( + SELECT sb.ts_code, sb.symbol, sb.name, sb.industry, sb.area, sb.market, sb.list_date, + ds.turnover_rate, ds.pe_ttm, ds.pb, ds.total_mv, ds.circ_mv + FROM stock_basic sb + LEFT JOIN LATERAL ( + SELECT turnover_rate, pe_ttm, pb, total_mv, circ_mv + FROM daily_snapshot + WHERE ts_code = sb.ts_code + ORDER BY (trade_date = cast(:target AS timestamp)) DESC, trade_date DESC + LIMIT 1 + ) ds ON true + WHERE sb.ts_code = :code + UNION ALL + SELECT eb.ts_code, eb.symbol, eb.name, NULL, NULL, + CASE eb.exchange WHEN 'SH' THEN '沪市' ELSE '深市' END, eb.list_date, + eb.turnover_rate, NULL, NULL, + eb.total_mv / 10000.0, eb.circ_mv / 10000.0 + FROM etf_basic eb + WHERE eb.ts_code = :code + ) t + LIMIT 1 """ ) @@ -389,6 +435,198 @@ async def stock_facets(session: AsyncSession = Depends(get_session)) -> Response return Response(content=raw, media_type="application/json") +@router.get("/stocks/{ts_code}/company", response_model=StockCompanyOut) +async def stock_company_info(ts_code: str, session: AsyncSession = Depends(get_session)) -> StockCompanyOut: + """公司简介:库内有新鲜行直返;否则锁内单查 tushare(stock_company)并 upsert(行即缓存, + 30 天新鲜度,无此股写墓碑负缓存)。ETF 前置短路;确认无数据 404;tushare 失败且 + 无旧行可降级时 503(有旧行则在数据层降级返回旧行)。""" + code = ts_code.strip().upper() + if "." not in code: + code = to_ts_code(code) # 防御:兼容 6 位裸代码 + if is_etf_symbol(code): + raise HTTPException(status_code=404, detail="ETF 无公司简介") + try: + row = await company_mod.get_company(session, code) + except Exception: + raise HTTPException(status_code=503, detail="tushare 公司简介拉取失败,请稍后重试") + if row is None: + raise HTTPException(status_code=404, detail=f"无公司信息: {code}") + return StockCompanyOut(**row) + + +@router.get("/stocks/{ts_code}/finance", response_model=StockFinanceOut) +async def stock_finance_info(ts_code: str, session: AsyncSession = Depends(get_session)) -> StockFinanceOut: + """财务数据(近五年,按报告期倒序):库内新鲜直返;否则锁内拉 tushare 四源 + (fina_indicator/income/balancesheet/cashflow)合并 upsert(7 天新鲜度,无数据写墓碑)。 + ETF 前置短路;确认无数据 404;tushare 四源全失败且无旧行可降级时 503。""" + code = ts_code.strip().upper() + if "." not in code: + code = to_ts_code(code) + if is_etf_symbol(code): + raise HTTPException(status_code=404, detail="ETF 无财务数据") + try: + rows = await finance_mod.get_finance(session, code) + except Exception: + raise HTTPException(status_code=503, detail="tushare 财务数据拉取失败,请稍后重试") + if not rows: + raise HTTPException(status_code=404, detail=f"无财务数据: {code}") + return StockFinanceOut(ts_code=code, records=rows) + + +@router.get("/stocks/{ts_code}/dividends", response_model=StockDividendOut) +async def stock_dividend_info(ts_code: str, session: AsyncSession = Depends(get_session)) -> StockDividendOut: + """分红送股(全历史,按分红年度倒序):库内新鲜直返;否则锁内拉 tushare dividend + 全量替换(7 天新鲜度,无分红写墓碑,空列表是正常返回)。 + ETF 前置短路;tushare 失败且无旧行可降级时 503。""" + code = ts_code.strip().upper() + if "." not in code: + code = to_ts_code(code) + if is_etf_symbol(code): + raise HTTPException(status_code=404, detail="ETF 无分红数据") + try: + rows = await dividend_mod.get_dividends(session, code) + except Exception: + raise HTTPException(status_code=503, detail="tushare 分红数据拉取失败,请稍后重试") + return StockDividendOut(ts_code=code, records=rows) + + +@router.get("/stocks/{ts_code}/reference/{kind}", response_model=StockReferenceOut) +async def stock_reference_info(ts_code: str, kind: str, session: AsyncSession = Depends(get_session)) -> StockReferenceOut: + """参考数据(11 类,kind 白名单见 reference.REFERENCE_KINDS):单股单分类 JSON 快照 + 懒加载(7 天新鲜度,无数据写墓碑,空 records 是正常返回)。repurchase 为全市场 + 按月回填的特殊管道:首次触发后台回填近 24 个月(本次可能返回空,稍后再看)。 + ETF 前置短路;未知 kind 404;tushare 失败且无旧行可降级时 503。""" + code = ts_code.strip().upper() + if "." not in code: + code = to_ts_code(code) + if is_etf_symbol(code): + raise HTTPException(status_code=404, detail="ETF 无参考数据") + if kind not in reference_mod.REFERENCE_KINDS: + raise HTTPException(status_code=404, detail=f"未知参考数据分类: {kind}") + try: + rows = await reference_mod.get_reference(session, code, kind) + except Exception: + raise HTTPException(status_code=503, detail="tushare 参考数据拉取失败,请稍后重试") + return StockReferenceOut(ts_code=code, kind=kind, records=rows) + + +# ---------- ETF 列表(全市场浏览;行情走 candles 底座,规模/换手走东财快照) ---------- +# 与 /stocks 不同:成交额来自 candles 最新 bar(LATERAL),必须在分页前 join 才能参与 +# 排序 —— ETF 全市场仅 ~1100 行,3 个索引探测/行 也就几 ms,可以承受。 +# 排序列白名单(键→表达式);order_by 由白名单拼接进模板,不接收用户原文。 +_ETFS_SORTS = { + "symbol": "eb.symbol", + "close": "c.close", + "pct_chg": "pct_chg", + "amount": "c.amount", + "total_mv": "eb.total_mv", + "circ_mv": "eb.circ_mv", + "turnover_rate": "eb.turnover_rate", +} + +_ETFS_SQL_TMPL = """ + SELECT eb.ts_code, eb.symbol, eb.name, eb.exchange, eb.list_date, + (w.id IS NOT NULL) AS watched, + eb.turnover_rate, + round((eb.total_mv / 100000000.0)::numeric, 2) AS total_mv, + round((eb.circ_mv / 100000000.0)::numeric, 2) AS circ_mv, + c.close AS close, prev.close AS prev_close, c.ts AS last_ts, + CASE WHEN c.close IS NOT NULL AND prev.close IS NOT NULL AND prev.close <> 0 + THEN round(((c.close / prev.close - 1) * 100)::numeric, 2) END AS pct_chg, + round((c.amount / 100000000.0)::numeric, 2) AS amount + FROM etf_basic eb + LEFT JOIN watchlist_items w ON w.ts_code = eb.ts_code AND w.user_id = :uid + LEFT JOIN LATERAL ( + SELECT close, ts, amount FROM candles + WHERE symbol = eb.symbol AND timeframe = '1d' + ORDER BY ts DESC LIMIT 1 + ) c ON true + LEFT JOIN LATERAL ( + SELECT close FROM candles + WHERE symbol = eb.symbol AND timeframe = '1d' AND ts < c.ts + ORDER BY ts DESC LIMIT 1 + ) prev ON c.ts IS NOT NULL + WHERE (:search = '' OR eb.symbol LIKE :psearch OR eb.name LIKE :psearch) + AND (:exchange = '' OR eb.exchange = :exchange) + AND (:watched_only = false OR w.id IS NOT NULL) + ORDER BY {order_by} + LIMIT :limit OFFSET :offset +""" + +_ETFS_COUNT_SQL = text(""" + SELECT count(*) FROM etf_basic eb + LEFT JOIN watchlist_items w ON w.ts_code = eb.ts_code AND w.user_id = :uid + WHERE (:search = '' OR eb.symbol LIKE :psearch OR eb.name LIKE :psearch) + AND (:exchange = '' OR eb.exchange = :exchange) + AND (:watched_only = false OR w.id IS NOT NULL) +""") + + +def _etfs_sql(sort: str, order: str) -> TextClause: + col = _ETFS_SORTS.get(sort, _ETFS_SORTS["symbol"]) + direction = "DESC" if order == "desc" else "ASC" + nulls = " NULLS LAST" if col != "eb.symbol" else "" # 无行情/无快照的排最后 + return text(_ETFS_SQL_TMPL.format(order_by=f"{col} {direction}{nulls}")) + + +@router.get("/etfs", response_model=EtfListResponse) +async def list_etfs( + search: str = "", + exchange: str = "", + watched_only: bool = False, + sort: str = "symbol", + order: str = "asc", + limit: int = 100, + offset: int = 0, + session: AsyncSession = Depends(get_session), + user=Depends(require_user), +) -> Response: + """全市场场内 ETF 列表:etf_basic 名称/规模(东财快照)+ candles 最新收盘/涨跌幅/成交额。 + exchange ∈ {SH, SZ}(空 = 全部);sort ∈ {symbol,close,pct_chg,amount,total_mv,circ_mv, + turnover_rate}(白名单,其他值回落 symbol),order ∈ asc/desc;快照/行情列排序时 + 缺失值恒排末尾。缓存:按「用户自选版本 + etf 版本 + 查询参数」缓存整页, + ETF 同步完成(bump ver:etf / ver:candles)或自选增删即失效。 + """ + search = search.strip() + sort = sort if sort in _ETFS_SORTS else "symbol" + order = "desc" if order.lower() == "desc" else "asc" + limit = max(1, min(limit, 500)) + offset = max(0, offset) + key = ( + f"etfsj:u{user.id}" + f":v{await cache.get_version(f'watchlist:{user.id}')}" + f":v{await cache.get_version('etf')}" + f":{cache.digest(search, exchange, watched_only, sort, order, limit, offset)}" + ) + cached = await _cached_json_response(key) + if cached is not None: + return cached + params = { + "search": search, "psearch": f"%{search}%", + "exchange": exchange.upper(), "watched_only": watched_only, + "uid": user.id, "limit": limit, "offset": offset, + } + total = (await session.execute(_ETFS_COUNT_SQL, params)).scalar_one() + rows = (await session.execute(_etfs_sql(sort, order), params)).mappings().all() + resp = EtfListResponse(total=total, items=[EtfListItemOut(**r) for r in rows]) + raw = _raw_json(resp) + cache.local_set(key, raw, ttl=min(120, settings.stocks_cache_ttl)) + asyncio.create_task(cache.cache_set(key, raw, ttl=settings.stocks_cache_ttl)) + return Response(content=raw, media_type="application/json") + + +@router.post("/etf/sync", response_model=EtfSyncStatus) +async def etf_sync_start(req: EtfSyncRequest) -> EtfSyncStatus: + """启动全市场 ETF 同步(后台任务:东财快照 -> etf_basic,逐只日线 -> candles)。""" + return EtfSyncStatus(**await etf_sync_mod.start_sync(full=req.full)) + + +@router.get("/etf/sync/status", response_model=EtfSyncStatus) +async def etf_sync_status(session: AsyncSession = Depends(get_session)) -> EtfSyncStatus: + """ETF 同步任务状态与数据实况(ETF 数 / 最新交易日)。""" + return EtfSyncStatus(**await etf_sync_mod.get_status(session)) + + @router.get("/market/overview", response_model=MarketOverviewResponse) async def get_market_overview(session: AsyncSession = Depends(get_session)) -> MarketOverviewResponse: """主页大盘总览:A 股 + 港美指数实时价(腾讯)叠加收盘历史走势(tushare), @@ -437,6 +675,113 @@ async def get_index_candles(timeframe: str = "1d") -> Response: return Response(content=raw, media_type="application/json") +@router.get("/market/global-indexes", response_model=GlobalIndexListResponse) +async def get_global_indexes() -> GlobalIndexListResponse: + """国际指数卡片列表(index_global 21 个指数最新收盘 + 45 日 spark,SWR 缓存)。""" + try: + data = await index_mod.fetch_global_list() + except index_mod.GlobalIndexError as e: + raise HTTPException(status_code=503, detail=str(e)) from e + payload = dict(data) + payload["updated_at"] = payload.pop("fetched_at") + return GlobalIndexListResponse(**payload) + + +def _index_or_404(code: str) -> str: + """详情/K线/权重接口只放行白名单内的指数 code。""" + if not index_mod.ensure_known(code): + raise HTTPException(status_code=404, detail=f"不支持的指数代码: {code}") + return code + + +@router.get("/market/indexes/{code}", response_model=IndexDetailResponse) +async def get_index_detail(code: str) -> IndexDetailResponse: + """指数详情聚合:最新行情(收盘口径)+ 基本信息(国内 index_basic / 国际静态表) + + 估值指标(index_dailybasic,仅部分国内指数)。各层自带缓存,直接组装。""" + code = _index_or_404(code) + if index_mod.is_cn_index(code): + name, region = index_mod.CN_INDEXES.get(code, code), "cn" + else: + g = index_mod.GLOBAL_META[code] + name, region = g["name"], g["region"] + + try: + quote = await index_mod.fetch_index_quote(code) + except index_mod.GlobalIndexError as e: + raise HTTPException(status_code=502, detail=f"指数行情获取失败: {e}") from e + + basic_raw = await index_mod.get_index_basic(code) + basic = IndexBasicOut(**basic_raw) if basic_raw else None + + valuation_rows = await index_mod.get_index_valuation(code) + valuation = IndexValuationPointOut(**valuation_rows[-1]) if valuation_rows else None + history = [IndexValuationPointOut(**r) for r in valuation_rows] + + return IndexDetailResponse( + code=code, name=name, region=region, + quote=IndexQuoteBriefOut(**quote), + basic=basic, valuation=valuation, valuation_history=history, + ) + + +@router.get("/market/indexes/{code}/candles", response_model=list[CandleOut]) +async def get_any_index_candles(code: str, timeframe: str = "1d") -> Response: + """白名单指数全量 K 线(国内 index_daily / 国际 index_global),1d/1w/1M/1y 聚合。 + 收盘口径,历史不可变、TTL 兜到当日更新(与首页上证 K 线同款缓存策略)。""" + code = _index_or_404(code) + if timeframe not in _INDEX_TIMEFRAMES: + raise HTTPException(status_code=400, detail=f"timeframe 仅支持 {'/'.join(_INDEX_TIMEFRAMES)}") + key = f"idxck:{cache.digest('idxk2', code, timeframe)}" + cached = await _cached_json_response(key) + if cached is not None: + return cached + try: + bars = resample_bars(await index_mod.get_index_bars(code), timeframe) + except Exception as e: # noqa: BLE001 + raise HTTPException(status_code=502, detail=f"指数数据获取失败: {e}") + outs = [ + CandleOut(ts=b.ts, open=b.open, high=b.high, low=b.low, close=b.close, + volume=b.volume, amount=b.amount, turnover=None) + for b in bars + ] + raw = TypeAdapter(list[CandleOut]).dump_json(outs).decode() + cache.local_set(key, raw, ttl=300) + await cache.cache_set(key, raw, ttl=7200) + return Response(content=raw, media_type="application/json") + + +@router.get("/market/indexes/{code}/weights", response_model=IndexWeightsResponse) +async def get_index_weights( + code: str, + limit: int = 50, + session: AsyncSession = Depends(get_session), +) -> IndexWeightsResponse: + """指数成分股权重(index_weight 最近月度快照,按权重降序取前 limit)。 + 仅国内指数有数据;成分股名称从本地 stock_basic 回填。""" + code = _index_or_404(code) + limit = max(1, min(limit, 300)) + data = await index_mod.get_index_weights(code) + if data is None: + raise HTTPException(status_code=404, detail=f"该指数暂无成分权重数据: {code}") + + items = data["items"][:limit] + codes = [it["con_code"] for it in items] + names: dict[str, str] = {} + if codes: + try: + rows = await session.execute( + select(StockBasic.ts_code, StockBasic.name).where(StockBasic.ts_code.in_(codes)) + ) + names = {r[0]: r[1] for r in rows.all()} + except Exception: # noqa: BLE001 —— 名称缺失不阻塞权重展示 + pass + return IndexWeightsResponse( + trade_date=data["trade_date"], total=data["total"], + items=[IndexWeightItemOut(con_code=c, name=names.get(c), weight=w) + for c, w in ((it["con_code"], it["weight"]) for it in items)], + ) + + @router.post("/backtest", response_model=BacktestResponse) async def backtest( req: BacktestRequest, @@ -468,6 +813,7 @@ async def backtest( initial_cash=req.initial_cash, fast_mode=req.fast_mode, bars_per_year=bars_per_year(req.timeframe), + is_fund=is_etf_symbol(req.symbol), # ETF 免印花税/过户费 ) result = run_backtest(bars, strategy, cfg) diff --git a/backend/app/backtest/broker.py b/backend/app/backtest/broker.py index 613cbe4..21affb5 100644 --- a/backend/app/backtest/broker.py +++ b/backend/app/backtest/broker.py @@ -22,6 +22,7 @@ class PaperBroker: schedule: CostSchedule = field(default_factory=lambda: DEFAULT) enable_costs: bool = True enable_t_plus_1: bool = True + is_fund: bool = False # 场内基金(ETF):免印花税/过户费,仅佣金 cash: float = field(init=False) holdings: float = 0.0 # 可卖数量 @@ -49,7 +50,7 @@ class PaperBroker: return None rate = ( self.schedule.commission_rate - + self.schedule.transfer_fee_rate + + (0.0 if self.is_fund else self.schedule.transfer_fee_rate) + self.schedule.slippage_rate ) if self.enable_costs else 0.0 affordable_qty = self.cash / (price * (1 + rate)) @@ -67,7 +68,7 @@ class PaperBroker: def _execute_buy(self, ts, price: float, qty: int) -> Fill: if self.enable_costs: - fp, comm, tf = buy_cost(price, qty, self.schedule) + fp, comm, tf = buy_cost(price, qty, self.schedule, is_fund=self.is_fund) else: fp, comm, tf = price, 0.0, 0.0 cost = fp * qty + comm + tf @@ -85,7 +86,7 @@ class PaperBroker: def _execute_sell(self, ts, price: float, qty: int) -> Fill: if self.enable_costs: - fp, comm, tf, sd = sell_cost(price, qty, self.schedule) + fp, comm, tf, sd = sell_cost(price, qty, self.schedule, is_fund=self.is_fund) else: fp, comm, tf, sd = price, 0.0, 0.0, 0.0 proceeds = fp * qty - comm - tf - sd diff --git a/backend/app/backtest/engine.py b/backend/app/backtest/engine.py index bfe0035..1080231 100644 --- a/backend/app/backtest/engine.py +++ b/backend/app/backtest/engine.py @@ -24,6 +24,7 @@ class BacktestConfig: initial_cash: float = 1000000.0 fast_mode: bool = False # True: 关 T+1/费用,交互试探 bars_per_year: int = 252 # 日线 252;分钟级另算 + is_fund: bool = False # 场内基金(ETF):免印花税/过户费 def run_backtest(bars: list[Bar], strategy, cfg: BacktestConfig | None = None) -> dict: @@ -45,6 +46,7 @@ def run_backtest(bars: list[Bar], strategy, cfg: BacktestConfig | None = None) - initial_cash=cfg.initial_cash, enable_costs=not cfg.fast_mode, enable_t_plus_1=not cfg.fast_mode, + is_fund=cfg.is_fund, ) equity_values = [] diff --git a/backend/app/commission.py b/backend/app/commission.py index 55fc261..a87774f 100644 --- a/backend/app/commission.py +++ b/backend/app/commission.py @@ -28,20 +28,28 @@ class CostSchedule: DEFAULT = CostSchedule() -def buy_cost(price: float, qty: float, sch: CostSchedule = DEFAULT) -> tuple[float, float, float]: - """买入成本。返回 (成交价, 佣金, 过户费)。买入无印花税。""" +def buy_cost(price: float, qty: float, sch: CostSchedule = DEFAULT, + is_fund: bool = False) -> tuple[float, float, float]: + """买入成本。返回 (成交价, 佣金, 过户费)。买入无印花税。 + + is_fund=True 为场内基金(ETF):免过户费,仅佣金 + 滑点。 + """ fill_price = price * (1 + sch.slippage_rate) gross = fill_price * qty commission = max(gross * sch.commission_rate, sch.commission_min) - transfer_fee = gross * sch.transfer_fee_rate + transfer_fee = 0.0 if is_fund else gross * sch.transfer_fee_rate return fill_price, commission, transfer_fee -def sell_cost(price: float, qty: float, sch: CostSchedule = DEFAULT) -> tuple[float, float, float, float]: - """卖出成本。返回 (成交价, 佣金, 过户费, 印花税)。""" +def sell_cost(price: float, qty: float, sch: CostSchedule = DEFAULT, + is_fund: bool = False) -> tuple[float, float, float, float]: + """卖出成本。返回 (成交价, 佣金, 过户费, 印花税)。 + + is_fund=True 为场内基金(ETF):免印花税、免过户费(现行规则),仅佣金 + 滑点。 + """ fill_price = price * (1 - sch.slippage_rate) gross = fill_price * qty commission = max(gross * sch.commission_rate, sch.commission_min) - transfer_fee = gross * sch.transfer_fee_rate - stamp_duty = gross * sch.stamp_duty_rate + transfer_fee = 0.0 if is_fund else gross * sch.transfer_fee_rate + stamp_duty = 0.0 if is_fund else gross * sch.stamp_duty_rate return fill_price, commission, transfer_fee, stamp_duty diff --git a/backend/app/data/fetcher.py b/backend/app/data/fetcher.py index baa72cd..24f30c8 100644 --- a/backend/app/data/fetcher.py +++ b/backend/app/data/fetcher.py @@ -7,6 +7,7 @@ from __future__ import annotations import asyncio +from datetime import datetime from sqlalchemy import func, select from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -16,6 +17,7 @@ from ..config import settings from ..domain import Bar from ..models import Candle from . import akshare_provider, tushare_provider +from .symbols import is_etf_symbol, to_ts_code DEFAULT_START = settings.data_default_start or "20200101" @@ -74,19 +76,19 @@ async def sync_symbol( # 增量:从缓存最后一根当天开始(重叠一天重新拉取,容忍数据源漏行/盘后修订) start = last_ts.strftime("%Y%m%d") start = start or DEFAULT_START - errors: list[str] = [] - bars: list[Bar] = [] - used = None - for name, fn in _providers(source): + # ETF 走 Tushare fund_daily(quicksync 镜像可用;与股票同源同控频), + # 按ts_code 增量拉取,未收盘当日数据未生成时自然返回空。 + if is_etf_symbol(code): + errors: list[str] = [] try: - # tushare/akshare 是同步网络 IO,丢到线程池避免阻塞事件循环; - # adjust=None -> 不复权(复权在读取时按 adj_factor 换算) - bars = await asyncio.to_thread(fn, code, start, end, None) - used = name - break + bars = await asyncio.to_thread(_fetch_etf_daily, code, start, end) + used = "tushare" except Exception as e: # noqa: BLE001 - errors.append(f"{name}: {e}") + errors.append(f"tushare: {e}") + bars = [] + else: + bars, used, errors = await _fetch_stock(code, start, end, source) if not bars: if last_ts is not None: @@ -112,3 +114,47 @@ async def sync_symbol( await session.execute(stmt) await session.commit() return {"symbol": code, "bars": len(bars), "source": used} + + +def _fetch_etf_daily(code: str, start: str | None, end: str | None) -> list[Bar]: + """Tushare fund_daily 按 ts_code 拉 ETF 日线(同步网络 IO,to_thread 调用)。 + 单位沿用 Tushare:vol 手、amount 千元,此处换算为 股/元。""" + from .tushare_provider import get_pro + + pro = get_pro() + df = pro.fund_daily(ts_code=to_ts_code(code), start_date=start, end_date=end) + if df is None or df.empty: + raise RuntimeError(f"Tushare 无数据: {to_ts_code(code)}") + df = df.sort_values("trade_date") + bars: list[Bar] = [] + for _, r in df.iterrows(): + amt = r.get("amount") + bars.append( + Bar( + ts=datetime.strptime(str(r["trade_date"]), "%Y%m%d"), + open=float(r["open"]), high=float(r["high"]), + low=float(r["low"]), close=float(r["close"]), + volume=float(r["vol"]) * 100.0, # 手 -> 份 + amount=float(amt) * 1000.0 if amt is not None and amt == amt else None, # 千元 -> 元 + ) + ) + return bars + + +async def _fetch_stock(code: str, start: str, end: str | None, source: str): + """Tushare 主 -> AKShare 兜底拉股票日线(不复权)。""" + errors: list[str] = [] + bars: list[Bar] = [] + used = None + + for name, fn in _providers(source): + try: + # tushare/akshare 是同步网络 IO,丢到线程池避免阻塞事件循环; + # adjust=None -> 不复权(复权在读取时按 adj_factor 换算) + bars = await asyncio.to_thread(fn, code, start, end, None) + used = name + break + except Exception as e: # noqa: BLE001 + errors.append(f"{name}: {e}") + + return bars, used, errors diff --git a/backend/app/data/market_overview.py b/backend/app/data/market_overview.py index 408dfbb..e9f3639 100644 --- a/backend/app/data/market_overview.py +++ b/backend/app/data/market_overview.py @@ -28,14 +28,12 @@ from .. import cache from ..config import settings # (tushare代码, 名称, 地区, 腾讯符号) —— 展示顺序即列表顺序 -# 标普500 腾讯符号是 s_usINX(不是 s_usSPX);恒生科技是 s_hkHSTECH(不是 HSTECH) +# 首页聚焦中美(港股/国际指数在 /indexes 国际指数页);标普500 腾讯符号是 s_usINX(不是 s_usSPX) MARKET_INDEXES: list[tuple[str, str, str, str]] = [ ("000001.SH", "上证指数", "cn", "s_sh000001"), ("399001.SZ", "深证成指", "cn", "s_sz399001"), ("399006.SZ", "创业板指", "cn", "s_sz399006"), ("000688.SH", "科创50", "cn", "s_sh000688"), - ("HSI", "恒生指数", "hk", "s_hkHSI"), - ("HKTECH", "恒生科技", "hk", "s_hkHSTECH"), ("DJI", "道琼斯", "us", "s_usDJI"), ("IXIC", "纳斯达克", "us", "s_usIXIC"), ("SPX", "标普500", "us", "s_usINX"), @@ -49,7 +47,7 @@ _TENCENT_URL = "http://qt.gtimg.cn/q=" + ",".join([*_TENCENT_MAP.values(), _TENC _SPARK_DAYS = 45 # 迷你走势取最近 45 个交易日收盘 _HISTORY_DAYS = 150 # 日历日窗口(约 100 个交易日,够取 spark) _CALL_INTERVAL = 0.12 # 顺序调用间隔(秒),对 tushare 控频 -_EOD_KEY = "market_overview:eod:v2" # v2:与旧整包缓存 v1 的 payload 形状不同,天然隔离 +_EOD_KEY = "market_overview:eod:v3" # v3:首页列表移除港股(移入国际指数页),与 v2 隔离 _AMOUNT_HIST_CAL_DAYS = 190 # 成交额历史的日历日窗口(≈128 交易日) _AMOUNT_HIST_BARS = 120 # 输出的柱数(取尾部) _LIVE_FAIL_TTL = 15.0 # live 层失败负缓存(秒) diff --git a/backend/app/data/symbols.py b/backend/app/data/symbols.py index 81b282a..ad80760 100644 --- a/backend/app/data/symbols.py +++ b/backend/app/data/symbols.py @@ -1,23 +1,33 @@ """A 股代码归一化。支持 6 位纯数字或带交易所后缀(000001 / 000001.SZ)。""" from __future__ import annotations +# 场内 ETF 代码前缀(与股票区间不重叠):沪 51/56/58 开头、深 159 开头。 +# 50/57/16 开头是 LOF,不在此列。 +_ETF_PREFIXES = ("51", "56", "58", "159") + def plain_code(code: str) -> str: """000001.SZ -> 000001""" return code.strip().upper().split(".")[0] +def is_etf_symbol(code: str) -> bool: + """按 6 位纯代码前缀判断是否场内 ETF(fetcher 据此路由到东财数据源)。""" + c = plain_code(code) + return len(c) == 6 and c.startswith(_ETF_PREFIXES) + + def to_ts_code(code: str) -> str: """转 Tushare ts_code(带交易所后缀)。""" c = code.strip().upper() if "." in c: return c c = plain_code(c) - # 沪市:60xxxx 主板、68xxxx 科创、9xxxxx B 股 - if c.startswith(("60", "68", "9")): + # 沪市:60xxxx 主板、68xxxx 科创、9xxxxx B 股、5xxxxx 基金(51/56/58 场内 ETF) + if c.startswith(("60", "68", "9", "51", "56", "58")): return c + ".SH" - # 深市:00xxxx 主板/中小、30xxxx 创业、20xxxx B 股 - if c.startswith(("00", "30", "20")): + # 深市:00xxxx 主板/中小、30xxxx 创业、20xxxx B 股、159xxx 场内 ETF + if c.startswith(("00", "30", "20", "159")): return c + ".SZ" # 北交所:8xxxxx / 4xxxxx if c.startswith(("8", "4")): diff --git a/backend/app/models.py b/backend/app/models.py index 695d2be..512e9e9 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -79,6 +79,163 @@ class StockBasic(Base): delist_date: Mapped[str | None] = mapped_column(String(8)) +class EtfBasic(Base): + """场内 ETF 列表(东财全市场快照;ETF 同步任务维护,行情另走 candles 底座)。 + + ts_code 如 510300.SH;symbol 为 6 位纯数字,与 candles.symbol 同口径。 + 代码区间与股票不冲突:沪 51/56/58 开头、深 159 开头。 + """ + __tablename__ = "etf_basic" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ts_code: Mapped[str] = mapped_column(String(12), unique=True, index=True) + symbol: Mapped[str] = mapped_column(String(10), index=True) # 510300 + name: Mapped[str] = mapped_column(String(64)) + exchange: Mapped[str] = mapped_column(String(8)) # SH/SZ + list_date: Mapped[str | None] = mapped_column(String(8)) # YYYYMMDD(首根K线日回填) + total_mv: Mapped[float | None] = mapped_column(Float) # 总市值(元,东财快照) + circ_mv: Mapped[float | None] = mapped_column(Float) # 流通市值(元) + turnover_rate: Mapped[float | None] = mapped_column(Float) # 换手率 % + updated_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, onupdate=_utcnow) + + +class StockCompany(Base): + """上市公司基本信息(tushare stock_company,详情页按需单查懒加载 + 30 天新鲜度)。 + + 行存在但 com_name 为 NULL = 负缓存墓碑(tushare 无此股,避免无数据代码反复穿透)。 + """ + __tablename__ = "stock_company" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ts_code: Mapped[str] = mapped_column(String(12), unique=True, index=True) # 000001.SZ + com_name: Mapped[str | None] = mapped_column(String(255)) # 公司全称 + com_id: Mapped[str | None] = mapped_column(String(32)) # 统一社会信用代码 + chairman: Mapped[str | None] = mapped_column(String(128)) # 法人代表 + manager: Mapped[str | None] = mapped_column(String(128)) # 总经理 + secretary: Mapped[str | None] = mapped_column(String(128)) # 董秘 + reg_capital: Mapped[float | None] = mapped_column(Float) # 注册资本(万元) + setup_date: Mapped[str | None] = mapped_column(String(8)) # 注册日期 YYYYMMDD + province: Mapped[str | None] = mapped_column(String(32)) + city: Mapped[str | None] = mapped_column(String(32)) + introduction: Mapped[str | None] = mapped_column(Text) # 公司介绍 + website: Mapped[str | None] = mapped_column(String(255)) + email: Mapped[str | None] = mapped_column(String(255)) + office: Mapped[str | None] = mapped_column(String(255)) # 办公地址 + employees: Mapped[int | None] = mapped_column(Integer) # 员工人数 + main_business: Mapped[str | None] = mapped_column(Text) # 主要业务及产品 + business_scope: Mapped[str | None] = mapped_column(Text) # 经营范围 + updated_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, onupdate=_utcnow) + + +class StockFinancial(Base): + """个股财务数据宽表(fina_indicator 指标 + 三大报表关键绝对值,按报告期一行)。 + + 详情页懒加载,四源合并 upsert:金额单位元(Tushare 原始), + 比率/同比为百分数。fina_indicator 列无 report_type 概念; + 三大报表列只取 report_type=1(合并报表)。 + """ + __tablename__ = "stock_financial" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ts_code: Mapped[str] = mapped_column(String(12), index=True) # 000001.SZ + end_date: Mapped[str] = mapped_column(String(8)) # 报告期 YYYYMMDD + ann_date: Mapped[str | None] = mapped_column(String(8)) # 公告日 YYYYMMDD + # ---- fina_indicator(doc 79)---- + eps: Mapped[float | None] = mapped_column(Float) # 基本每股收益(元) + bps: Mapped[float | None] = mapped_column(Float) # 每股净资产(元) + ocfps: Mapped[float | None] = mapped_column(Float) # 每股经营现金流净额(元) + roe: Mapped[float | None] = mapped_column(Float) # 净资产收益率 % + roe_dt: Mapped[float | None] = mapped_column(Float) # 扣非净资产收益率 % + grossprofit_margin: Mapped[float | None] = mapped_column(Float) # 销售毛利率 % + netprofit_margin: Mapped[float | None] = mapped_column(Float) # 销售净利率 % + debt_to_assets: Mapped[float | None] = mapped_column(Float) # 资产负债率 % + or_yoy: Mapped[float | None] = mapped_column(Float) # 营业收入同比 % + netprofit_yoy: Mapped[float | None] = mapped_column(Float) # 归母净利润同比 % + dt_netprofit_yoy: Mapped[float | None] = mapped_column(Float) # 扣非净利润同比 % + profit_dedt: Mapped[float | None] = mapped_column(Float) # 扣非净利润(元) + rd_exp: Mapped[float | None] = mapped_column(Float) # 研发投入(元) + # ---- income(doc 33,report_type=1)---- + total_revenue: Mapped[float | None] = mapped_column(Float) # 营业总收入(元) + operate_profit: Mapped[float | None] = mapped_column(Float) # 营业利润(元) + n_income_attr_p: Mapped[float | None] = mapped_column(Float) # 归母净利润(元) + # ---- balancesheet(report_type=1)---- + total_assets: Mapped[float | None] = mapped_column(Float) # 总资产(元) + total_hldr_eqy: Mapped[float | None] = mapped_column(Float) # 归母股东权益(元) + # ---- cashflow(report_type=1)---- + n_cashflow_act: Mapped[float | None] = mapped_column(Float) # 经营现金流净额(元) + updated_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, onupdate=_utcnow) + + __table_args__ = ( + UniqueConstraint("ts_code", "end_date", name="uq_stock_financial_code_end"), + ) + + +class StockDividend(Base): + """个股分红送股(tushare dividend 全历史;每 code 同步时全量替换,无唯一业务键)。 + + 预案行(div_proc != 实施)无 ex_date,K 线标记与统计只用实施行。 + """ + __tablename__ = "stock_dividend" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ts_code: Mapped[str] = mapped_column(String(12), index=True) # 000001.SZ + end_date: Mapped[str | None] = mapped_column(String(8)) # 分红年度 YYYYMMDD + ann_date: Mapped[str | None] = mapped_column(String(8)) # 预案公告日 + div_proc: Mapped[str | None] = mapped_column(String(16)) # 实施进度(预案/实施) + stk_div: Mapped[float | None] = mapped_column(Float) # 每股送转 + stk_bo_rate: Mapped[float | None] = mapped_column(Float) # 每股送股比例 + stk_co_rate: Mapped[float | None] = mapped_column(Float) # 每股转增比例 + cash_div: Mapped[float | None] = mapped_column(Float) # 每股分红(税后,元) + cash_div_tax: Mapped[float | None] = mapped_column(Float) # 每股分红(税前,元) + base_share: Mapped[float | None] = mapped_column(Float) # 基准股本(万股) + record_date: Mapped[str | None] = mapped_column(String(8)) # 股权登记日 + ex_date: Mapped[str | None] = mapped_column(String(8)) # 除权除息日(K线标记锚点) + pay_date: Mapped[str | None] = mapped_column(String(8)) # 派息日 + div_listdate: Mapped[str | None] = mapped_column(String(8)) # 红股上市日 + imp_ann_date: Mapped[str | None] = mapped_column(String(8)) # 实施公告日 + updated_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, onupdate=_utcnow) + + +class StockSyncState(Base): + """多行懒加载管道的同步状态(新鲜度门控 + 无数据墓碑)。 + + 多行表无法像 stock_company 那样用「行存在但业务列 NULL」当墓碑, + 用本表记录每个 (ts_code, kind) 的同步时间与是否有数据。 + kind: finance | dividend。 + """ + __tablename__ = "stock_sync_state" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ts_code: Mapped[str] = mapped_column(String(12), index=True) + kind: Mapped[str] = mapped_column(String(16)) + last_synced_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow) + has_data: Mapped[bool] = mapped_column(Boolean, default=False) + + __table_args__ = ( + UniqueConstraint("ts_code", "kind", name="uq_stock_sync_code_kind"), + ) + + +class StockReference(Base): + """个股参考数据(tushare 参考数据版块按需懒加载,一行 = 一股一分类的 JSON 快照)。 + + rows_json 存清洗后的 tushare 原始行数组(单位沿用各接口原始口径,见 reference.py 注册表); + 行存在但 rows_json 为 NULL = 负缓存墓碑(该股该分类确认无数据)。 + kind 白名单见 app/data/reference.py 的 REFERENCE_KINDS。 + """ + __tablename__ = "stock_reference" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + ts_code: Mapped[str] = mapped_column(String(12), index=True) # 000001.SZ + kind: Mapped[str] = mapped_column(String(24)) # top10_holders | ... + rows_json: Mapped[str | None] = mapped_column(Text) # JSON 数组;NULL=墓碑 + updated_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, onupdate=_utcnow) + + __table_args__ = ( + UniqueConstraint("ts_code", "kind", name="uq_stock_reference_code_kind"), + ) + + class MarketDaily(Base): """全市场未复权日线(选股专用,与回测 candles(qfq) 隔离)。 diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 4daed28..326b9db 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -269,6 +269,89 @@ class PreviewResponse(BaseModel): has_more: bool = False # 返回窗口之前是否还有更早历史(前端向左滚动翻页用) +# ---------- 公司简介(tushare stock_company,详情页按需懒加载) ---------- +class StockCompanyOut(BaseModel): + ts_code: str + com_name: str | None = None # 公司全称 + com_id: str | None = None # 统一社会信用代码 + chairman: str | None = None # 法人代表 + manager: str | None = None # 总经理 + secretary: str | None = None # 董秘 + reg_capital: float | None = None # 注册资本(万元) + setup_date: str | None = None # 注册日期 YYYYMMDD(展示层换算) + province: str | None = None # 所在省份 + city: str | None = None # 所在城市 + introduction: str | None = None # 公司介绍 + website: str | None = None # 公司主页 + email: str | None = None # 电子邮件 + office: str | None = None # 办公地址 + employees: int | None = None # 员工人数 + main_business: str | None = None # 主要业务及产品 + business_scope: str | None = None # 经营范围 + + +# ---------- 财务数据(fina_indicator + 三大报表关键值,详情页按需懒加载) ---------- +class StockFinanceRecordOut(BaseModel): + """一行 = 一个报告期。金额单位元(展示层换算亿/万),比率与同比为百分数。""" + end_date: str # 报告期 YYYYMMDD + ann_date: str | None = None # 公告日 YYYYMMDD + eps: float | None = None # 基本每股收益(元) + bps: float | None = None # 每股净资产(元) + ocfps: float | None = None # 每股经营现金流净额(元) + roe: float | None = None # 净资产收益率 % + roe_dt: float | None = None # 扣非净资产收益率 % + grossprofit_margin: float | None = None # 销售毛利率 % + netprofit_margin: float | None = None # 销售净利率 % + debt_to_assets: float | None = None # 资产负债率 % + or_yoy: float | None = None # 营业收入同比 % + netprofit_yoy: float | None = None # 归母净利润同比 % + dt_netprofit_yoy: float | None = None # 扣非净利润同比 % + profit_dedt: float | None = None # 扣非净利润(元) + rd_exp: float | None = None # 研发投入(元) + total_revenue: float | None = None # 营业总收入(元) + operate_profit: float | None = None # 营业利润(元) + n_income_attr_p: float | None = None # 归母净利润(元) + total_assets: float | None = None # 总资产(元) + total_hldr_eqy: float | None = None # 归母股东权益(元) + n_cashflow_act: float | None = None # 经营现金流净额(元) + + +class StockFinanceOut(BaseModel): + ts_code: str + records: list[StockFinanceRecordOut] # 按报告期倒序(records[0] = 最新报告期) + + +# ---------- 分红送股(tushare dividend,详情页按需懒加载) ---------- +class StockDividendRecordOut(BaseModel): + end_date: str | None = None # 分红年度 YYYYMMDD + ann_date: str | None = None # 预案公告日 + div_proc: str | None = None # 实施进度(预案/实施) + stk_div: float | None = None # 每股送转 + stk_bo_rate: float | None = None # 每股送股比例 + stk_co_rate: float | None = None # 每股转增比例 + cash_div: float | None = None # 每股分红(税后,元) + cash_div_tax: float | None = None # 每股分红(税前,元) + base_share: float | None = None # 基准股本(万股) + record_date: str | None = None # 股权登记日 + ex_date: str | None = None # 除权除息日(K线标记锚点) + pay_date: str | None = None # 派息日 + div_listdate: str | None = None # 红股上市日 + imp_ann_date: str | None = None # 实施公告日 + + +class StockDividendOut(BaseModel): + ts_code: str + records: list[StockDividendRecordOut] # 按分红年度倒序;空列表 = 确认无分红 + + +# ---------- 参考数据(tushare 参考数据版块,详情页按需懒加载) ---------- +class StockReferenceOut(BaseModel): + """rows_json 快照直出:records 为清洗后的 tushare 原始行,字段随 kind 而异。""" + ts_code: str + kind: str # 白名单见 app/data/reference.py REFERENCE_KINDS + records: list[dict[str, str | float | None]] = Field(default_factory=list) + + # ---------- Auth ---------- class LoginRequest(BaseModel): username: str = Field(min_length=1, max_length=64) @@ -322,6 +405,45 @@ class StockFacetsResponse(BaseModel): areas: list[FacetItemOut] = Field(default_factory=list) +# ---------- ETF 列表(全市场浏览;行情走 candles 底座,规模走东财快照) ---------- +class EtfListItemOut(BaseModel): + ts_code: str # 510300.SH + symbol: str # 510300 + name: str + exchange: str # SH/SZ + close: float | None = None # 最新收盘(candles 未复权) + prev_close: float | None = None + pct_chg: float | None = None # 最新两根日线计算 + amount: float | None = None # 最新成交额(亿元,candles 最新 bar) + last_ts: datetime | None = None + turnover_rate: float | None = None # 换手率 %(东财快照) + total_mv: float | None = None # 总市值(亿元,东财快照) + circ_mv: float | None = None # 流通市值(亿元) + list_date: str | None = None # 上市日 YYYYMMDD(首根K线日) + watched: bool = False # 是否自选(当前用户) + + +class EtfListResponse(BaseModel): + total: int + items: list[EtfListItemOut] + + +class EtfSyncRequest(BaseModel): + full: bool = False # true = 忽略增量起点全量重拉(修数据用) + + +class EtfSyncStatus(BaseModel): + running: bool + step: str | None = None + total: int = 0 # 本次需拉 K 线的 ETF 数 + done: int = 0 + error: str | None = None + ready: bool = False # etf_basic 非空且已有日线 + last_trade_date: datetime | None = None + last_synced_at: datetime | None = None + stats: dict[str, int] = Field(default_factory=dict) # etfs + + # ---------- 用户偏好 / 自选股 / 提问历史 ---------- class PreferencesOut(BaseModel): prefs: dict[str, object] = Field(default_factory=dict) # key -> JSON 值 @@ -413,3 +535,86 @@ class MarketOverviewResponse(BaseModel): amount_history: list[AmountBarOut] = [] # 近 N 交易日两市成交额(旧 -> 新,末根可能盘中) errors: list[str] = [] # 部分来源失败的说明(透明但不阻塞展示) + +# ---------- 指数专题(国际指数卡片 + 指数详情) ---------- + +class GlobalIndexQuoteOut(BaseModel): + """国际指数卡片(index_global 最新收盘 + 45 日 spark)。""" + code: str # DJI / SPX / HSI ... + name: str # 道琼斯工业指数 + region: str # americas | europe | asia(前端分组) + country: str # 美国 / 英国 / 日本 ... + close: float | None = None + change: float | None = None + pct_chg: float | None = None + trade_date: date | None = None + spark: list[float] = [] + spark_dates: list[str] = [] + + +class GlobalIndexListResponse(BaseModel): + updated_at: datetime + items: list[GlobalIndexQuoteOut] = [] + errors: list[str] = [] + + +class IndexBasicOut(BaseModel): + """指数基本信息:国内 index_basic(市场/发布方/基期基点),国际由静态表合成(国家/地区)。""" + ts_code: str + name: str + market: str | None = None # SSE / CSI / SZSE ... + publisher: str | None = None # 中证指数 / 上交所 + category: str | None = None # 规模指数 / 综合指数 + base_date: date | None = None # 基期 + base_point: float | None = None # 基点 + list_date: date | None = None # 发布日期 + country: str | None = None # 国际指数:国家/地区 + region: str | None = None # 国际指数:americas/europe/asia + + +class IndexValuationPointOut(BaseModel): + """大盘指数每日指标的一日(index_dailybasic,仅部分国内指数有数据)。""" + trade_date: date + pe: float | None = None + pe_ttm: float | None = None + pb: float | None = None + turnover_rate: float | None = None # 换手率 % + total_mv: float | None = None # 总市值(元;tushare 文档标注万元,实测按元才对得上量级) + float_mv: float | None = None # 流通市值(元) + + +class IndexQuoteBriefOut(BaseModel): + """详情页头部最新行情(收盘口径)。""" + close: float | None = None + change: float | None = None + pct_chg: float | None = None + open: float | None = None + high: float | None = None + low: float | None = None + pre_close: float | None = None + trade_date: date | None = None + spark: list[float] = [] + spark_dates: list[str] = [] + + +class IndexDetailResponse(BaseModel): + code: str + name: str + region: str # cn | americas | europe | asia + quote: IndexQuoteBriefOut + basic: IndexBasicOut | None = None + valuation: IndexValuationPointOut | None = None # 最新一日(接口不覆盖时 None) + valuation_history: list[IndexValuationPointOut] = [] # 近 N 日(PE 走势小图) + + +class IndexWeightItemOut(BaseModel): + con_code: str # 成分股 000001.SZ + name: str | None = None # 平安银行(本地 stock_basic 回填,缺则 None) + weight: float # 权重 % + + +class IndexWeightsResponse(BaseModel): + trade_date: date + total: int # 成分股总数(返回 items 为按权重降序的子集) + items: list[IndexWeightItemOut] = [] + diff --git a/backend/app/screener/market_sync.py b/backend/app/screener/market_sync.py index 847cdae..eafaf6c 100644 --- a/backend/app/screener/market_sync.py +++ b/backend/app/screener/market_sync.py @@ -218,9 +218,16 @@ async def _replace_day(session: AsyncSession, model, rows: list[dict], d_str: st async def _existing_candle_dates(session: AsyncSession) -> set[str]: - """candles 表已落库的交易日集合(YYYYMMDD 字符串,便于比对)。""" + """candles 表已落库的交易日集合(YYYYMMDD 字符串,便于比对)。 + + 限定在市股票符号:candles 底座同时容纳 ETF(etf_sync 写入),若不隔离, + 只同步了 ETF 的交易日会被误判为「股票日线已完成」而跳过当日股票同步。 + """ res = await session.execute( - select(func.distinct(func.date(Candle.ts))).where(Candle.timeframe == "1d") + select(func.distinct(func.date(Candle.ts))).where( + Candle.timeframe == "1d", + Candle.symbol.in_(select(StockBasic.symbol).where(StockBasic.list_status == "L")), + ) ) return {r[0].strftime("%Y%m%d") for r in res if r[0] is not None} @@ -339,12 +346,12 @@ async def _run_sync(days: int, force: bool) -> None: # candles/复权因子已更新:作废旧 K 线预览缓存(键含版本号,自增即全体失效) await cache.bump_version("candles") - # 预热同步状态缓存:同步任务自己付一次重聚合(>10s),轮询方毫秒级拿到新数字 + # 预热统计缓存:同步任务自己付一次重聚合(>10s)。SWR 下轮询方不等待—— + # 先拿到旧值(last_trade_date 本就实时),重算完成后数字自然换新 _sync_state["step"] = "正在更新统计缓存" try: - async with async_session() as s2: - await _db_stats(s2) - except Exception: # noqa: BLE001 —— 预热失败只影响下一次轮询的时延 + await _refresh_stats(await cache.get_version("candles")) + except Exception: # noqa: BLE001 —— 预热失败只影响统计数字的新鲜度 pass _sync_state["step"] = "同步完成" except Exception as e: # noqa: BLE001 @@ -369,12 +376,26 @@ async def start_sync(session: AsyncSession, days: int, force: bool) -> dict: return dict(_sync_state) -# candles 是千万行表,重聚合(全表 count / distinct 日期)在远程库实测 >11s; -# 结果按 ver:candles 版本号缓存(同步完成即 bump 失效),前端 2s 轮询只付毫秒级。 +# candles 是千万行表,重聚合(全表 count / distinct 日期)在远程库实测 >11s。 +# 读路径 SWR:版本失效(同步完成 bump)/进程重启后,先吐最近一次旧值(进程内 → +# Redis 无版本 last 键),后台单飞重算——轮询请求**永不等待**重聚合(旧设计里 +# 轮询会在 _stats_lock 上排队 >10s,表现为首页「数据更新至」加载不出来); +# 只有史上第一次(进程内与 Redis 都无记录)才现场算。 +# last_daily(max(ts),走索引很快)保持每次实时——它是 UI 主展示字段。 _status_stats_cache: dict = {"at": 0.0, "ver": -1, "data": None} _STATS_TTL = 120.0 # 进程内兜底 TTL(Redis 不可用时重聚合的最小间隔) -_stats_bg_tasks: set[asyncio.Task] = set() # 后台写缓存的引用,防 GC -_stats_lock = asyncio.Lock() # 单飞锁:同步尾部的预热与轮询并发时,重聚合只跑一次 +_stats_bg_tasks: set[asyncio.Task] = set() # 后台任务引用,防 GC +_stats_lock = asyncio.Lock() # 单飞锁:同一时刻至多一个重聚合在跑 + +_STATS_LAST_KEY = "syncstats:last" # 无版本号的最近一次结果(跨版本/跨重启兜底) + + +def _fresh_local(ver: int) -> dict | None: + d = _status_stats_cache["data"] + if d is not None and _status_stats_cache["ver"] == ver \ + and time.time() - _status_stats_cache["at"] < _STATS_TTL: + return d + return None async def _heavy_stats(session: AsyncSession) -> dict: @@ -391,30 +412,52 @@ async def _heavy_stats(session: AsyncSession) -> dict: } +async def _store_stats(ver: int, data: dict) -> None: + _status_stats_cache.update(at=time.time(), ver=ver, data=data) + # 写 Redis 后台执行,失败由 cache 层静默降级,不拖慢调用方 + tasks = [ + asyncio.create_task(cache.cache_set( + f"syncstats:v{ver}", data, ttl=settings.sync_stats_redis_ttl)), + asyncio.create_task(cache.cache_set( + _STATS_LAST_KEY, data, ttl=settings.sync_stats_redis_ttl)), + ] + _stats_bg_tasks.update(tasks) + for t in tasks: + t.add_done_callback(_stats_bg_tasks.discard) + + +async def _refresh_stats(ver: int) -> None: + """后台重算(单飞):等锁双检后重聚合并写缓存。已有重算在跑则直接返回。""" + from ..db import async_session # 延迟导入避免循环 + + if _stats_lock.locked(): + return + async with _stats_lock: + if _fresh_local(ver) is not None: + return # 等锁期间已被并发填充 + async with async_session() as s: + data = await _heavy_stats(s) + await _store_stats(ver, data) + + async def _db_stats(session: AsyncSession) -> dict: - """candles/快照/股票列表实况:重聚合走「版本化 Redis + 进程内」双层缓存, - last_daily(max(ts),走索引很快)保持每次实时——它是 UI 主展示字段。""" - - def _fresh_local(ver: int) -> dict | None: - d = _status_stats_cache["data"] - if d is not None and _status_stats_cache["ver"] == ver \ - and time.time() - _status_stats_cache["at"] < _STATS_TTL: - return d - return None - ver = await cache.get_version("candles") heavy = _fresh_local(ver) if heavy is None: - async with _stats_lock: # 双检:等锁期间可能已被并发请求/同步预热填充 - heavy = _fresh_local(ver) or await cache.cache_get(f"syncstats:v{ver}") - if heavy is None: - heavy = await _heavy_stats(session) - # 写 Redis 后台执行,失败由 cache 层静默降级,不拖慢本次返回 - task = asyncio.create_task( - cache.cache_set(f"syncstats:v{ver}", heavy, ttl=settings.sync_stats_redis_ttl)) - _stats_bg_tasks.add(task) - task.add_done_callback(_stats_bg_tasks.discard) - _status_stats_cache.update(at=time.time(), ver=ver, data=heavy) + stale = _status_stats_cache["data"] or await cache.cache_get(_STATS_LAST_KEY) + if stale is not None: + # SWR:先返旧值(行数等数字仅展示用,旧几秒无害),后台重算 + heavy = stale + t = asyncio.create_task(_refresh_stats(ver)) + _stats_bg_tasks.add(t) + t.add_done_callback(_stats_bg_tasks.discard) + else: + # 史上第一次(进程内与 Redis 均无记录):只能现场算,锁内单飞 + async with _stats_lock: + heavy = _fresh_local(ver) or await cache.cache_get(f"syncstats:v{ver}") + if heavy is None: + heavy = await _heavy_stats(session) + await _store_stats(ver, heavy) last_daily = await session.scalar( select(func.max(Candle.ts)).where(Candle.timeframe == "1d") ) diff --git a/backend/app/trades.py b/backend/app/trades.py index f15a169..732bce0 100644 --- a/backend/app/trades.py +++ b/backend/app/trades.py @@ -139,12 +139,11 @@ def _to_date(v) -> date | None: def _to_code_suffix(code: str) -> str: - """6 位代码 → 交易所后缀(60/68 沪,00/30 深,4/8/92 北交所)。""" - if code.startswith(("60", "68", "90")): - return ".SH" - if code.startswith(("00", "30", "20")): - return ".SZ" - return ".BJ" + """6 位代码 → 交易所后缀。委托 to_ts_code(含场内 ETF:51/56/58 沪、159 深)。""" + from .data.symbols import to_ts_code + + ts_code = to_ts_code(code) + return ts_code[ts_code.index("."):] def _direction(op: str) -> str | None: diff --git a/backend_run.log b/backend_run.log index 9d5b931..b246ee1 100644 --- a/backend_run.log +++ b/backend_run.log @@ -1,274 +1,783 @@ INFO: Will watch for changes in these directories: ['D:\\Project\\stock\\backend'] INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [2136] using WatchFiles -INFO: Started server process [3296] +INFO: Started reloader process [28028] using WatchFiles +INFO: Started server process [16456] INFO: Waiting for application startup. INFO: Application startup complete. -INFO: 127.0.0.1:48937 - "GET /api/health HTTP/1.1" 200 OK -INFO: 127.0.0.1:49077 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:49092 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:49093 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:49094 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -WARNING: WatchFiles detected changes in 'scripts\diag_market_sources.py'. Reloading... - INFO: 127.0.0.1:52219 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:52237 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:52238 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:52239 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:52861 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:52871 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:52876 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:52883 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:53667 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:53671 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:53670 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:53678 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:53710 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:53753 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:54745 - "GET /api/health HTTP/1.1" 200 OK -INFO: 127.0.0.1:54746 - "POST /api/auth/login HTTP/1.1" 200 OK -INFO: 127.0.0.1:59489 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:59506 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:59507 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:59508 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:59720 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:59721 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:59727 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:59739 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:59740 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:59748 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:59753 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:59756 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:59754 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:59771 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:59778 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:59777 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:59730 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:59749 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:59760 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:59779 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:13210 - "GET /api/health HTTP/1.1" 200 OK -INFO: 127.0.0.1:16263 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:16275 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:16276 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16303 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:16309 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16308 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:16319 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:16322 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16320 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:16349 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:16357 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16352 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:16310 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:16277 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:16325 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:16358 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:16598 - "POST /api/auth/login HTTP/1.1" 200 OK -INFO: 127.0.0.1:16599 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16600 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16601 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16602 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:44639 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:44642 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44662 - "GET /api/stocks/600519.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44669 - "GET /api/stocks/600519.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44671 - "GET /api/stocks/510300.SH/company HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:44673 - "GET /api/stocks/999999.SZ/company HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:44683 - "GET /api/stocks/000002/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44686 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 401 Unauthorized +INFO: 127.0.0.1:44701 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44707 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44704 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44700 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:44703 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:45455 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:45461 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:45464 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:45463 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:45472 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:45469 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:45471 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:45476 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:45474 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +WARNING: WatchFiles detected changes in 'app\data\index_global.py'. Reloading... + INFO: 127.0.0.1:46980 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:46985 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:46982 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:46984 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:46990 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:46992 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:46991 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:46998 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:46996 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:47008 - "GET /openapi.json HTTP/1.1" 200 OK INFO: Will watch for changes in these directories: ['D:\\Project\\stock\\backend'] INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [43156] using WatchFiles -INFO: Started server process [35852] +INFO: Started reloader process [29204] using WatchFiles +INFO: Started server process [28644] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: 127.0.0.1:47041 - "GET /openapi.json HTTP/1.1" 200 OK +INFO: 127.0.0.1:47042 - "GET /api/health HTTP/1.1" 200 OK +WARNING: WatchFiles detected changes in 'scripts\_ui_drive_tmp.py'. Reloading... + INFO: 127.0.0.1:47137 - "POST /api/auth/login HTTP/1.1" 200 OK +INFO: 127.0.0.1:47183 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:47185 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:47188 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:47194 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:47206 - "PUT /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:47202 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:47227 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:47233 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:47235 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:47269 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:47272 - "GET /api/market/indexes/SPX HTTP/1.1" 200 OK +INFO: 127.0.0.1:47270 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:47278 - "GET /api/market/indexes/SPX/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:47193 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:47307 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:47308 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:47310 - "GET /api/market/indexes/000300.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:47319 - "GET /api/market/indexes/000300.SH/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:47317 - "GET /api/market/indexes/000300.SH/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:48863 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized +INFO: 127.0.0.1:48877 - "POST /api/auth/login HTTP/1.1" 200 OK +INFO: 127.0.0.1:48878 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:48880 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:48879 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:48886 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:48881 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:48895 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:48904 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:48912 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:48917 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:49244 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:49252 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:49254 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:49253 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:49260 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:49258 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:49259 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:49264 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:49262 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:49269 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:49266 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:49273 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:49270 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:49365 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:49369 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:49367 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:49374 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:49378 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:49380 - "GET /api/market/indexes/399001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:49388 - "GET /api/market/indexes/399001.SZ/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:49382 - "GET /api/market/indexes/399001.SZ/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:49393 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:49395 - "GET /api/market/indexes/IXIC HTTP/1.1" 200 OK +INFO: 127.0.0.1:49397 - "GET /api/market/indexes/IXIC/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:49408 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:49410 - "GET /api/market/indexes/RUT HTTP/1.1" 200 OK +INFO: 127.0.0.1:49412 - "GET /api/market/indexes/RUT/candles?timeframe=1d HTTP/1.1" 502 Bad Gateway +INFO: 127.0.0.1:49425 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:49427 - "GET /api/market/indexes/SPTSX HTTP/1.1" 200 OK +INFO: 127.0.0.1:49429 - "GET /api/market/indexes/SPTSX/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:49437 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:49441 - "GET /api/market/indexes/IBOVESPA HTTP/1.1" 200 OK +INFO: 127.0.0.1:49442 - "GET /api/market/indexes/IBOVESPA/candles?timeframe=1d HTTP/1.1" 502 Bad Gateway +INFO: 127.0.0.1:49447 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:49455 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:49457 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64126 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:64123 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64132 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:64131 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:64155 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64149 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64124 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64374 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:64377 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64376 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64375 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:64380 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:64395 - "GET /api/market/indexes/SPTSX HTTP/1.1" 200 OK +INFO: 127.0.0.1:64397 - "GET /api/market/indexes/SPTSX/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64459 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:64464 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64461 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:64469 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64465 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64482 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:64489 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64487 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64497 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64494 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:64500 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64498 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64507 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:64506 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64517 - "GET /api/trades?ts_code=000010.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:64519 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:64518 - "GET /api/screener/preview/000010.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64521 - "GET /api/screener/preview/000010.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64523 - "GET /api/stocks/000010.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:64558 - "GET /api/screener/preview/000010.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64560 - "PUT /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:64562 - "GET /api/screener/preview/000010.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-13 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64564 - "PUT /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:64802 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64799 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:64808 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64803 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64816 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:64817 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:64818 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:64825 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64828 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64826 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64865 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:64871 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64869 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:64876 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64917 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64918 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:64914 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:64921 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64940 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:64942 - "GET /api/market/indexes/DJI HTTP/1.1" 200 OK +INFO: 127.0.0.1:64944 - "GET /api/market/indexes/DJI/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64951 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:64956 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64954 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:64961 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64972 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:64976 - "GET /api/market/indexes/399001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:64984 - "GET /api/market/indexes/399001.SZ/weights?limit=50 HTTP/1.1" 200 OK +INFO: 127.0.0.1:64978 - "GET /api/market/indexes/399001.SZ/candles?timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:64990 - "GET /api/market/global-indexes HTTP/1.1" 200 OK +INFO: 127.0.0.1:10165 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:10163 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:10167 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:10252 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:10249 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:10253 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:10732 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:10731 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:10733 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:10746 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:10751 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:10759 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:10760 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:10764 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:10770 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:10768 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: Started server process [25400] INFO: Waiting for application startup. INFO: Application startup complete. -INFO: 127.0.0.1:16663 - "POST /api/auth/login HTTP/1.1" 200 OK -INFO: 127.0.0.1:16664 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16668 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:16670 - "GET /api/market/overview HTTP/1.1" 200 OK -WARNING: WatchFiles detected changes in 'scripts\_tmp_crosscheck.py'. Reloading... - INFO: 127.0.0.1:19181 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:19196 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:19198 - "GET /api/screener/sync/status HTTP/1.1" 200 OK -INFO: 127.0.0.1:19197 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:19510 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:19513 - "GET /api/market/overview HTTP/1.1" 200 OK -INFO: 127.0.0.1:22090 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:22089 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22101 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22107 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22106 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22111 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22115 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22123 - "GET /api/trades?ts_code=000002.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22124 - "GET /api/screener/preview/000002.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22126 - "GET /api/screener/preview/000002.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22138 - "GET /api/trades?ts_code=000006.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22139 - "GET /api/screener/preview/000006.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22141 - "GET /api/screener/preview/000006.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22173 - "GET /api/stocks?search=%E9%A2%86%E7%9B%8A%E7%A7%91%E6%8A%80&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22175 - "GET /api/stocks?search=%E9%A2%86%E7%9B%8A%E7%A7%91&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22179 - "GET /api/stocks?search=%E9%A2%86%E7%9B%8A&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22181 - "GET /api/stocks?search=%E9%A2%86&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22190 - "GET /api/trades?ts_code=002600.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22192 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22191 - "GET /api/screener/preview/002600.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22195 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22199 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22203 - "GET /api/stock/002600.SZ/chips?date=2026-09-04&adjust=qfq HTTP/1.1" 200 OK -INFO: 127.0.0.1:22213 - "GET /api/stock/002600.SZ/chips?date=2026-09-04&adjust=qfq HTTP/1.1" 200 OK -INFO: 127.0.0.1:22231 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22235 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22237 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22242 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22245 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22247 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22251 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22254 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22256 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22260 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22262 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22265 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22269 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22271 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22275 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22279 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22283 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22286 - "GET /api/screener/preview/002600.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22320 - "POST /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22322 - "GET /api/stocks?search=%E9%A2%86&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22332 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22334 - "GET /api/stocks?search=%E6%B5%B7%E8%9E%BA&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22338 - "POST /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22343 - "GET /api/trades?ts_code=600585.SH HTTP/1.1" 200 OK -INFO: 127.0.0.1:22349 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22348 - "GET /api/screener/preview/600585.SH?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22353 - "GET /api/screener/preview/600585.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22368 - "GET /api/screener/preview/600585.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22483 - "GET /api/stocks?search=%E6%B5%B7%E8%9E%BA&watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22485 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22501 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22499 - "GET /api/trades?ts_code=002074.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22500 - "GET /api/screener/preview/002074.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22503 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22508 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22571 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22579 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22577 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22581 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22585 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22587 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22594 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5%2C20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22596 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22598 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22601 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22606 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=10%2C20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22609 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22611 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=10%2C20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22615 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=20&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22618 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22621 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22624 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22628 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22630 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22635 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22638 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22642 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22644 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22663 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22667 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&mas=5&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22676 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22678 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22805 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:22820 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:22819 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22817 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22824 - "GET /api/screener/preview/002074.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22823 - "GET /api/trades?ts_code=002074.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22829 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22831 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22845 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:22847 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22852 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22853 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:22856 - "GET /api/screener/preview/002074.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22855 - "GET /api/trades?ts_code=002074.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22861 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22859 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22872 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:22874 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:22882 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22883 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:22889 - "GET /api/screener/preview/002074.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22885 - "GET /api/trades?ts_code=002074.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22898 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22895 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:22908 - "GET /api/screener/preview/002074.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:22907 - "GET /api/trades?ts_code=002074.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:22913 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:22911 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23054 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23056 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23064 - "GET /api/trades?ts_code=000783.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23065 - "GET /api/screener/preview/000783.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:23071 - "GET /api/screener/preview/002074.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:23069 - "GET /api/trades?ts_code=002074.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23074 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23085 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:23087 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23091 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23099 - "GET /api/screener/preview/002074.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:23094 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:23098 - "GET /api/trades?ts_code=002074.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23105 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23103 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23119 - "GET /api/stock/002074.SZ/chips?date=2026-09-04&adjust=qfq HTTP/1.1" 200 OK -INFO: 127.0.0.1:23328 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23331 - "GET /api/screener/preview/002074.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23335 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23359 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23360 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK -INFO: 127.0.0.1:23362 - "GET /api/screener/preview/002100.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23381 - "GET /api/screener/preview/002100.SZ?limit=800&adjust=qfq&timeframe=1d&end=2021-04-28 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23383 - "GET /api/screener/preview/002100.SZ?limit=800&adjust=qfq&timeframe=1d&end=2018-01-10 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23405 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1w HTTP/1.1" 200 OK -INFO: 127.0.0.1:23407 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23410 - "GET /api/screener/preview/002100.SZ?limit=800&adjust=qfq&timeframe=1w&end=2017-01-16 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23453 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1w HTTP/1.1" 200 OK -INFO: 127.0.0.1:23452 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23456 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23461 - "GET /api/screener/preview/002100.SZ?limit=800&adjust=qfq&timeframe=1w&end=2017-01-16 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23466 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1M HTTP/1.1" 200 OK -INFO: 127.0.0.1:23470 - "PUT /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23511 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1M HTTP/1.1" 200 OK -INFO: 127.0.0.1:23510 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23514 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23630 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:23642 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23645 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:23644 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23652 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1M HTTP/1.1" 200 OK -INFO: 127.0.0.1:23651 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23654 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23692 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:23694 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23698 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23704 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:23708 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1M HTTP/1.1" 200 OK -INFO: 127.0.0.1:23705 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23713 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23720 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:23722 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23728 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23729 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:23734 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1M HTTP/1.1" 200 OK -INFO: 127.0.0.1:23731 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK -INFO: 127.0.0.1:23739 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23753 - "GET /api/auth/me HTTP/1.1" 200 OK -INFO: 127.0.0.1:23756 - "GET /api/preferences HTTP/1.1" 200 OK -INFO: 127.0.0.1:23771 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK -INFO: 127.0.0.1:23772 - "GET /api/stocks/facets HTTP/1.1" 200 OK -INFO: 127.0.0.1:23774 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1M HTTP/1.1" 200 OK -INFO: 127.0.0.1:23775 - "GET /api/watchlist HTTP/1.1" 200 OK -INFO: 127.0.0.1:23773 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK -INFO: Will watch for changes in these directories: ['D:\\Project\\stock\\backend'] INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [18388] using WatchFiles -INFO: Started server process [4824] +INFO: 127.0.0.1:12480 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:12505 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:12524 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 500 Internal Server Error +ERROR: Exception in ASGI application +Traceback (most recent call last): + File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 422, in run_asgi + result = await app( # type: ignore[func-returns-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + self.scope, self.receive, self.send + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 63, in __call__ + return await self.app(scope, receive, send) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\applications.py", line 1163, in __call__ + await super().__call__(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\applications.py", line 90, in __call__ + await self.middleware_stack(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__ + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__ + await self.app(scope, receive, _send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__ + await self.app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__ + await self.app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__ + await self.middleware_stack(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2734, in app + await route.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1780, in handle + await self.original_router.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2789, in handle + await included_router._handle_selected(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1800, in _handle_selected + await original_route.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1279, in handle + await app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 158, in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 144, in app + response = await f(request) + ^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 481, in app + solved_result = await solve_dependencies( + ^^^^^^^^^^^^^^^^^^^^^^^^^ + ...<6 lines>... + ) + ^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\dependencies\utils.py", line 674, in solve_dependencies + solved = await call(**solved_result.values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\app\auth.py", line 127, in require_user + if expires_mono > time.monotonic() and sess_expires > utcnow() and user.is_active: + ^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 569, in __get__ + return self.impl.get(state, dict_) # type: ignore[no-any-return] + ~~~~~~~~~~~~~^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1096, in get + value = self._fire_loader_callables(state, key, passive) + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1126, in _fire_loader_callables + return state._load_expired(state, passive) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\state.py", line 828, in _load_expired + self.manager.expired_attribute_loader(self, toload, passive) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\loading.py", line 1607, in load_scalar_attributes + raise orm_exc.DetachedInstanceError( + ...<2 lines>... + ) +sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3) +INFO: 127.0.0.1:12525 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 500 Internal Server Error +ERROR: Exception in ASGI application +Traceback (most recent call last): + File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 422, in run_asgi + result = await app( # type: ignore[func-returns-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + self.scope, self.receive, self.send + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 63, in __call__ + return await self.app(scope, receive, send) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\applications.py", line 1163, in __call__ + await super().__call__(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\applications.py", line 90, in __call__ + await self.middleware_stack(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__ + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__ + await self.app(scope, receive, _send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__ + await self.app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__ + await self.app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__ + await self.middleware_stack(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2734, in app + await route.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1780, in handle + await self.original_router.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2789, in handle + await included_router._handle_selected(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1800, in _handle_selected + await original_route.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1279, in handle + await app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 158, in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 144, in app + response = await f(request) + ^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 481, in app + solved_result = await solve_dependencies( + ^^^^^^^^^^^^^^^^^^^^^^^^^ + ...<6 lines>... + ) + ^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\dependencies\utils.py", line 674, in solve_dependencies + solved = await call(**solved_result.values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\app\auth.py", line 127, in require_user + if expires_mono > time.monotonic() and sess_expires > utcnow() and user.is_active: + ^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 569, in __get__ + return self.impl.get(state, dict_) # type: ignore[no-any-return] + ~~~~~~~~~~~~~^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1096, in get + value = self._fire_loader_callables(state, key, passive) + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1126, in _fire_loader_callables + return state._load_expired(state, passive) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\state.py", line 828, in _load_expired + self.manager.expired_attribute_loader(self, toload, passive) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\loading.py", line 1607, in load_scalar_attributes + raise orm_exc.DetachedInstanceError( + ...<2 lines>... + ) +sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3) +INFO: 127.0.0.1:12530 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 500 Internal Server Error +ERROR: Exception in ASGI application +Traceback (most recent call last): + File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 422, in run_asgi + result = await app( # type: ignore[func-returns-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + self.scope, self.receive, self.send + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 63, in __call__ + return await self.app(scope, receive, send) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\applications.py", line 1163, in __call__ + await super().__call__(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\applications.py", line 90, in __call__ + await self.middleware_stack(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__ + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__ + await self.app(scope, receive, _send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__ + await self.app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__ + await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__ + await self.app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__ + await self.middleware_stack(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2734, in app + await route.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1780, in handle + await self.original_router.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2789, in handle + await included_router._handle_selected(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1800, in _handle_selected + await original_route.handle(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1279, in handle + await app(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 158, in app + await wrap_app_handling_exceptions(app, request)(scope, receive, send) + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app + raise exc + File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app + await app(scope, receive, sender) + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 144, in app + response = await f(request) + ^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 481, in app + solved_result = await solve_dependencies( + ^^^^^^^^^^^^^^^^^^^^^^^^^ + ...<6 lines>... + ) + ^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\dependencies\utils.py", line 674, in solve_dependencies + solved = await call(**solved_result.values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\app\auth.py", line 127, in require_user + if expires_mono > time.monotonic() and sess_expires > utcnow() and user.is_active: + ^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 569, in __get__ + return self.impl.get(state, dict_) # type: ignore[no-any-return] + ~~~~~~~~~~~~~^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1096, in get + value = self._fire_loader_callables(state, key, passive) + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1126, in _fire_loader_callables + return state._load_expired(state, passive) + ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\state.py", line 828, in _load_expired + self.manager.expired_attribute_loader(self, toload, passive) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\loading.py", line 1607, in load_scalar_attributes + raise orm_exc.DetachedInstanceError( + ...<2 lines>... + ) +sqlalchemy.orm.exc.DetachedInstanceError: Instance is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3) +INFO: 127.0.0.1:12740 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:12741 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:12747 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:12748 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:12750 - "GET /api/stocks/000010.SZ/company HTTP/1.1" 200 OK +INFO: Started server process [20424] INFO: Waiting for application startup. INFO: Application startup complete. -INFO: 127.0.0.1:27988 - "GET /api/health HTTP/1.1" 200 OK -INFO: 127.0.0.1:28005 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 401 Unauthorized -INFO: 127.0.0.1:28014 - "GET /api/market/overview HTTP/1.1" 401 Unauthorized +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:12839 - "POST /api/auth/login HTTP/1.1" 200 OK +INFO: 127.0.0.1:12841 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:12846 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:12851 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:12855 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:12862 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:12861 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:12857 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:12874 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:12870 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:12873 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:12886 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:12887 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:12888 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:12889 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:12893 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:12894 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:12971 - "GET /api/trades?ts_code=000002.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:12973 - "GET /api/screener/preview/000002.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:12979 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:12980 - "GET /api/stocks/510300.SH/finance HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:12975 - "GET /api/screener/preview/000002.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:12972 - "GET /api/stocks/000002.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:12984 - "GET /api/stocks/000002.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:12983 - "GET /api/stocks/000002.SZ/finance HTTP/1.1" 200 OK +INFO: Started server process [19680] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:13034 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:13037 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:13046 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:13045 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:13044 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:13058 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:13060 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:13059 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:13074 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:13151 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:13152 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:13155 - "GET /api/etf/sync/status HTTP/1.1" 200 OK +INFO: 127.0.0.1:13153 - "GET /api/market/overview HTTP/1.1" 200 OK +INFO: 127.0.0.1:13229 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:13154 - "GET /api/screener/sync/status HTTP/1.1" 200 OK +INFO: Started server process [13624] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:26668 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:26677 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26680 - "GET /api/stocks/000001.SZ/reference/top10_floatholders HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26682 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26684 - "GET /api/stocks/000014.SZ/reference/pledge_detail HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26686 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26688 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26693 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26696 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 503 Service Unavailable +INFO: 127.0.0.1:26698 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:26700 - "GET /api/stocks/000001.SZ/reference/high_shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:26705 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:26707 - "GET /api/stocks/000001.SZ/reference/bogus HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:26708 - "GET /api/stocks/510300.SH/reference/top10_holders HTTP/1.1" 404 Not Found +INFO: Started server process [23460] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:26776 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:26780 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:26783 - "GET /api/stocks/000001.SZ/reference/top10_floatholders HTTP/1.1" 200 OK +INFO: 127.0.0.1:26785 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:26791 - "GET /api/stocks/000014.SZ/reference/pledge_detail HTTP/1.1" 200 OK +INFO: 127.0.0.1:26793 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:26795 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK +INFO: 127.0.0.1:26798 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:26800 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 200 OK +INFO: 127.0.0.1:26802 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:26803 - "GET /api/stocks/000001.SZ/reference/high_shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:26805 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:26806 - "GET /api/stocks/000001.SZ/reference/bogus HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:26807 - "GET /api/stocks/510300.SH/reference/top10_holders HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:26819 - "GET /api/stocks/688433.SH/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:26821 - "GET /api/stocks/001260.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:26826 - "GET /api/stocks/000792.SZ/reference/pledge_detail HTTP/1.1" 200 OK +INFO: 127.0.0.1:26828 - "GET /api/stocks/000408.SZ/reference/pledge_detail HTTP/1.1" 200 OK +INFO: Started server process [8048] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:26862 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:26866 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:26869 - "GET /api/stocks/000001.SZ/reference/top10_floatholders HTTP/1.1" 200 OK +INFO: 127.0.0.1:26871 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:26873 - "GET /api/stocks/000014.SZ/reference/pledge_detail HTTP/1.1" 200 OK +INFO: 127.0.0.1:26893 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:26898 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK +INFO: 127.0.0.1:26900 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:26910 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 200 OK +INFO: 127.0.0.1:26915 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:26917 - "GET /api/stocks/000001.SZ/reference/high_shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:26919 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:26921 - "GET /api/stocks/000001.SZ/reference/bogus HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:26922 - "GET /api/stocks/510300.SH/reference/top10_holders HTTP/1.1" 404 Not Found +INFO: 127.0.0.1:26972 - "GET /api/stocks/688433.SH/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:26975 - "GET /api/stocks/001260.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:26977 - "GET /api/stocks/000792.SZ/reference/pledge_detail HTTP/1.1" 200 OK +INFO: 127.0.0.1:26981 - "GET /api/stocks/000408.SZ/reference/pledge_detail HTTP/1.1" 200 OK +INFO: Started server process [22596] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:27093 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:27102 - "GET /api/stocks/688433.SH/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:27105 - "GET /api/stocks/001260.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:27107 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:27109 - "GET /api/stocks/000792.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:27154 - "GET /api/stocks/000792.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:27165 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:27169 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK +INFO: 127.0.0.1:27170 - "GET /api/stocks/000792.SZ/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:27173 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:27174 - "GET /api/stocks/000792.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:27175 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:27938 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:27949 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:27955 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:27954 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:27959 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:27960 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:27966 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:27965 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:27981 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:27980 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:27977 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:28003 - "GET /api/stocks/002648.SZ/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:28008 - "GET /api/stocks/002648.SZ/reference/top10_floatholders HTTP/1.1" 200 OK +INFO: 127.0.0.1:28016 - "GET /api/stocks/002648.SZ/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:28021 - "GET /api/stocks/002648.SZ/reference/pledge_detail HTTP/1.1" 200 OK +INFO: 127.0.0.1:28024 - "GET /api/stocks/002648.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:28031 - "GET /api/stocks/002648.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:28034 - "GET /api/stocks/002648.SZ/reference/block_trade HTTP/1.1" 200 OK +INFO: 127.0.0.1:28039 - "GET /api/stocks/002648.SZ/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:28042 - "GET /api/stocks/002648.SZ/reference/holdertrade HTTP/1.1" 200 OK +INFO: 127.0.0.1:28093 - "GET /api/stocks/002648.SZ/reference/shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:28105 - "GET /api/stocks/002648.SZ/reference/high_shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:28989 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:28995 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:28993 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:28994 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:29003 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:29006 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:29001 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:29005 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:29015 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:29016 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:29012 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:29030 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:29032 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:29037 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:29041 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK +INFO: 127.0.0.1:29045 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 200 OK +INFO: 127.0.0.1:29077 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:29080 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: 127.0.0.1:29085 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK +INFO: 127.0.0.1:30638 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:30689 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:30659 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:30688 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:30700 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:30702 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:30717 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:30716 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:30729 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:30728 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:30725 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59133 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized +INFO: 127.0.0.1:59380 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized +INFO: 127.0.0.1:59474 - "GET /api/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:59497 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized +INFO: 127.0.0.1:59569 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:59575 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:59578 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:59577 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59587 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:59581 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:59584 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:59586 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:59597 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:59596 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:59593 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59708 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:59715 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59716 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:59709 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:59719 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:59723 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:59721 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:59722 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59733 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:59730 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59734 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:59918 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:59923 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:59922 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59928 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59921 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:59929 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:59925 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:59933 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59926 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:59939 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59937 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:59936 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:59946 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:59950 - "GET /api/stocks/600848.SH/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:59965 - "GET /api/stocks/600848.SH/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:59973 - "GET /api/stocks/600848.SH/reference/high_shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:60101 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:60107 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:60106 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60112 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60102 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:60113 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:60109 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:60111 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:60116 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60122 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60120 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:60119 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:60128 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:60136 - "GET /api/stocks/600848.SH/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:60153 - "GET /api/stocks/600848.SH/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:60159 - "GET /api/stocks/600848.SH/reference/high_shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:60237 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:60243 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:60242 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60240 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:60248 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60246 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:60249 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:60257 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:60247 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:60255 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60256 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:60272 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:60508 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:60515 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:60514 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60510 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:60523 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60521 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:60524 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:60522 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:60540 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60542 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:60541 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:60565 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:60731 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:60738 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:60737 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60743 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60735 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:60740 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:60744 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:60742 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:60753 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:60752 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:60749 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60761 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:60755 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60848 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:60852 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60853 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:60849 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:60858 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60859 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:60854 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:60856 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:60862 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60866 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:60865 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:60874 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:60990 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:60993 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:60995 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:60996 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:61001 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:61002 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:61004 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:61003 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:61017 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:61016 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:61013 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61021 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:61113 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:61114 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:61117 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61118 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:61123 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:61124 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:61122 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK +INFO: 127.0.0.1:61120 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:61128 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61131 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:61130 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:61144 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:61154 - "GET /api/stocks/600848.SH/reference/pledge_stat HTTP/1.1" 200 OK +INFO: 127.0.0.1:61159 - "GET /api/stocks/600848.SH/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:61164 - "GET /api/stocks/600848.SH/reference/high_shock HTTP/1.1" 200 OK +INFO: 127.0.0.1:61735 - "GET /api/auth/me HTTP/1.1" 200 OK +INFO: 127.0.0.1:61743 - "GET /api/preferences HTTP/1.1" 200 OK +INFO: 127.0.0.1:61747 - "GET /api/stocks/facets HTTP/1.1" 200 OK +INFO: 127.0.0.1:61746 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61751 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK +INFO: 127.0.0.1:61753 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK +INFO: 127.0.0.1:61757 - "GET /api/watchlist HTTP/1.1" 200 OK +INFO: 127.0.0.1:61756 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK +INFO: 127.0.0.1:61770 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK +INFO: 127.0.0.1:61769 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK +INFO: 127.0.0.1:61766 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK +INFO: 127.0.0.1:61795 - "GET /api/stocks/002648.SZ/reference/top10_holders HTTP/1.1" 200 OK +INFO: 127.0.0.1:61807 - "GET /api/stocks/002648.SZ/reference/block_trade HTTP/1.1" 200 OK +INFO: 127.0.0.1:61813 - "GET /api/stocks/002648.SZ/reference/holdernumber HTTP/1.1" 200 OK +INFO: 127.0.0.1:61843 - "GET /api/stocks/002648.SZ/reference/holdertrade HTTP/1.1" 200 OK +INFO: 127.0.0.1:61870 - "GET /api/stocks/002648.SZ/reference/repurchase HTTP/1.1" 200 OK +INFO: Started server process [43184] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:62984 - "GET /api/stocks/000001.SZ/reference/moneyflow HTTP/1.1" 200 OK +INFO: 127.0.0.1:62991 - "GET /api/stocks/000001.SZ/reference/moneyflow HTTP/1.1" 200 OK diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 7218303..17d4142 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -70,7 +70,7 @@ async function signOut() {
- 数据来源 Tushare · 仅供研究学习,不构成投资建议 + 数据来源 Tushare / 东方财富 · 仅供研究学习,不构成投资建议
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 10cb32e..3eae377 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -6,6 +6,11 @@ import type { CurrentUser, EventBacktestRequest, EventBacktestResponse, + EtfListResponse, + EtfSyncStatus, + GlobalIndexList, + IndexDetail, + IndexWeights, LoginRequest, LoginResponse, MarketOverview, @@ -16,7 +21,11 @@ import type { ScreenerRunResponse, ScreenerSyncRequest, ScreenerSyncStatus, + StockCompanyInfo, + StockDividendOut, StockFacets, + StockFinanceOut, + StockReferenceOut, StockListResponse, SyncRequest, SyncResponse, @@ -191,6 +200,34 @@ export async function getStockPreview( return (await res.json()) as PreviewResponse; } +/** 个股公司简介(tushare stock_company 按需懒加载;404 = ETF/无此股,调用方据此隐藏面板) */ +export async function getStockCompany(tsCode: string): Promise { + const res = await apiFetch(`/api/stocks/${encodeURIComponent(tsCode)}/company`); + if (!res.ok) throw new ApiError(await readError(res, `获取公司简介失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as StockCompanyInfo; +} + +/** 个股财务数据(fina_indicator+三大报表关键值,近五年;404 = ETF/无数据,调用方据此隐藏面板) */ +export async function getStockFinance(tsCode: string): Promise { + const res = await apiFetch(`/api/stocks/${encodeURIComponent(tsCode)}/finance`); + if (!res.ok) throw new ApiError(await readError(res, `获取财务数据失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as StockFinanceOut; +} + +/** 个股分红送股(tushare dividend 全历史;空 records = 确认无分红) */ +export async function getStockDividends(tsCode: string): Promise { + const res = await apiFetch(`/api/stocks/${encodeURIComponent(tsCode)}/dividends`); + if (!res.ok) throw new ApiError(await readError(res, `获取分红数据失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as StockDividendOut; +} + +/** 个股参考数据(tushare 参考数据版块按 kind 懒加载;空 records = 确认无数据) */ +export async function getStockReference(tsCode: string, kind: string): Promise { + const res = await apiFetch(`/api/stocks/${encodeURIComponent(tsCode)}/reference/${encodeURIComponent(kind)}`); + if (!res.ok) throw new ApiError(await readError(res, `获取参考数据失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as StockReferenceOut; +} + // ---------- 大盘总览(主页) ---------- export async function getMarketOverview(): Promise { const res = await apiFetch('/api/market/overview'); @@ -205,6 +242,37 @@ export async function getIndexCandles(timeframe: Timeframe): Promise { return (await res.json()) as Candle[]; } +// ---------- 指数专题(国际指数卡片 + 指数详情) ---------- +export async function getGlobalIndexes(): Promise { + const res = await apiFetch('/api/market/global-indexes'); + if (!res.ok) throw new ApiError(await readError(res, '获取国际指数失败'), res.status); + return (await res.json()) as GlobalIndexList; +} + +export async function getIndexDetail(code: string): Promise { + const res = await apiFetch(`/api/market/indexes/${encodeURIComponent(code)}`); + if (!res.ok) throw new ApiError(await readError(res, '获取指数详情失败'), res.status); + return (await res.json()) as IndexDetail; +} + +/** 白名单指数全量 K 线(国内 index_daily / 国际 index_global,日线基底聚合) */ +export async function getAnyIndexCandles(code: string, timeframe: Timeframe): Promise { + const res = await apiFetch( + `/api/market/indexes/${encodeURIComponent(code)}/candles?timeframe=${encodeURIComponent(timeframe)}`, + ); + if (!res.ok) throw new ApiError(await readError(res, `获取指数K线失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as Candle[]; +} + +/** 指数成分股权重(仅国内指数;国际指数后端 404) */ +export async function getIndexWeights(code: string, limit = 50): Promise { + const res = await apiFetch( + `/api/market/indexes/${encodeURIComponent(code)}/weights?limit=${limit}`, + ); + if (!res.ok) throw new ApiError(await readError(res, '获取成分权重失败'), res.status); + return (await res.json()) as IndexWeights; +} + export async function getStocks(params: { search?: string; market?: string; @@ -237,6 +305,41 @@ export async function getStockFacets(): Promise { return (await res.json()) as StockFacets; } +// ---------- ETF 列表(全市场浏览) ---------- +export async function getEtfs(params: { + search?: string; + exchange?: string; + watched_only?: boolean; + sort?: string; + order?: 'asc' | 'desc'; + limit?: number; + offset?: number; +}): Promise { + const q = new URLSearchParams(); + if (params.search) q.set('search', params.search); + if (params.exchange) q.set('exchange', params.exchange); + if (params.watched_only) q.set('watched_only', 'true'); + if (params.sort) q.set('sort', params.sort); + if (params.order) q.set('order', params.order); + q.set('limit', String(params.limit ?? 100)); + q.set('offset', String(params.offset ?? 0)); + const res = await apiFetch(`/api/etfs?${q.toString()}`); + if (!res.ok) throw new ApiError(await readError(res, `获取 ETF 列表失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as EtfListResponse; +} + +export async function startEtfSync(full = false): Promise { + const res = await apiFetch('/api/etf/sync', { method: 'POST', body: JSON.stringify({ full }) }); + if (!res.ok) throw new ApiError(await readError(res, `启动 ETF 同步失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as EtfSyncStatus; +} + +export async function getEtfSyncStatus(): Promise { + const res = await apiFetch('/api/etf/sync/status'); + if (!res.ok) throw new ApiError(await readError(res, `获取 ETF 同步状态失败 (HTTP ${res.status})`), res.status); + return (await res.json()) as EtfSyncStatus; +} + // ---------- 用户偏好 / 自选股 / 提问历史 ---------- export async function getPreferences(): Promise> { const res = await apiFetch('/api/preferences'); diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index dfe4710..d80f4ce 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -209,6 +209,105 @@ export interface PreviewResponse { has_more?: boolean; // 返回窗口之前是否还有更早历史(前端向左滚动翻页用) } +// ---------- 公司简介(镜像 app/schemas.py StockCompanyOut) ---------- +export interface StockCompanyInfo { + ts_code: string; + com_name?: string | null; // 公司全称 + com_id?: string | null; // 统一社会信用代码 + chairman?: string | null; // 法人代表 + manager?: string | null; // 总经理 + secretary?: string | null; // 董秘 + reg_capital?: number | null; // 万元 + setup_date?: string | null; // YYYYMMDD + province?: string | null; + city?: string | null; + introduction?: string | null; // 公司介绍 + website?: string | null; + email?: string | null; + office?: string | null; + employees?: number | null; + main_business?: string | null; // 主要业务及产品 + business_scope?: string | null; // 经营范围 +} + +// ---------- 财务数据(镜像 app/schemas.py StockFinanceOut,近五年按报告期倒序) ---------- +export interface StockFinanceRecord { + end_date: string; // 报告期 YYYYMMDD + ann_date?: string | null; // 公告日 YYYYMMDD + eps?: number | null; // 基本每股收益(元) + bps?: number | null; // 每股净资产(元) + ocfps?: number | null; // 每股经营现金流净额(元) + roe?: number | null; // 净资产收益率 % + roe_dt?: number | null; // 扣非净资产收益率 % + grossprofit_margin?: number | null; // 销售毛利率 % + netprofit_margin?: number | null; // 销售净利率 % + debt_to_assets?: number | null; // 资产负债率 % + or_yoy?: number | null; // 营业收入同比 % + netprofit_yoy?: number | null; // 归母净利润同比 % + dt_netprofit_yoy?: number | null; // 扣非净利润同比 % + profit_dedt?: number | null; // 扣非净利润(元) + rd_exp?: number | null; // 研发投入(元) + total_revenue?: number | null; // 营业总收入(元) + operate_profit?: number | null; // 营业利润(元) + n_income_attr_p?: number | null; // 归母净利润(元) + total_assets?: number | null; // 总资产(元) + total_hldr_eqy?: number | null; // 归母股东权益(元) + n_cashflow_act?: number | null; // 经营现金流净额(元) +} + +export interface StockFinanceOut { + ts_code: string; + records: StockFinanceRecord[]; // records[0] = 最新报告期 +} + +// ---------- 分红送股(镜像 app/schemas.py StockDividendOut,全历史按分红年度倒序) ---------- +export interface StockDividendRecord { + end_date?: string | null; // 分红年度 YYYYMMDD + ann_date?: string | null; // 预案公告日 + div_proc?: string | null; // 实施进度(预案/实施) + stk_div?: number | null; // 每股送转 + stk_bo_rate?: number | null; // 每股送股比例 + stk_co_rate?: number | null; // 每股转增比例 + cash_div?: number | null; // 每股分红(税后,元) + cash_div_tax?: number | null; // 每股分红(税前,元) + base_share?: number | null; // 基准股本(万股) + record_date?: string | null; // 股权登记日 + ex_date?: string | null; // 除权除息日(K线标记锚点) + pay_date?: string | null; // 派息日 + div_listdate?: string | null; // 红股上市日 + imp_ann_date?: string | null; // 实施公告日 +} + +export interface StockDividendOut { + ts_code: string; + records: StockDividendRecord[]; // 空数组 = 确认无分红 +} + +// ---------- 参考数据(镜像 app/schemas.py StockReferenceOut,字段随 kind 而异) ---------- +export type StockReferenceRecord = Record; + +export interface StockReferenceOut { + ts_code: string; + kind: string; // 白名单 key,见 REFERENCE_KINDS + records: StockReferenceRecord[]; // 空数组 = 确认无数据 +} + +/** 参考数据分类目录(与后端 reference.REFERENCE_KINDS 白名单一一对应) */ +export const REFERENCE_KINDS: { key: string; label: string }[] = [ + { key: 'top10_holders', label: '十大股东' }, + { key: 'top10_floatholders', label: '十大流通' }, + { key: 'pledge_stat', label: '质押统计' }, + { key: 'pledge_detail', label: '质押明细' }, + { key: 'repurchase', label: '回购' }, + { key: 'share_float', label: '解禁' }, + { key: 'block_trade', label: '大宗' }, + { key: 'moneyflow', label: '资金流向' }, + { key: 'holdernumber', label: '股东人数' }, + { key: 'holdertrade', label: '增减持' }, + { key: 'shock', label: '异动' }, + { key: 'high_shock', label: '严重异动' }, +]; + // ---------- 股票列表(全市场浏览) ---------- export interface StockListItem { ts_code: string; @@ -243,6 +342,41 @@ export interface StockFacets { areas: FacetItem[]; } +// ---------- ETF 列表(全市场浏览) ---------- +export interface EtfListItem { + ts_code: string; + symbol: string; + name: string; + exchange: string; // SH/SZ + close?: number | null; + prev_close?: number | null; + pct_chg?: number | null; + amount?: number | null; // 最新成交额(亿元) + last_ts?: string | null; + turnover_rate?: number | null; // 换手率 %(东财快照) + total_mv?: number | null; // 总市值(亿元) + circ_mv?: number | null; // 流通市值(亿元) + list_date?: string | null; // 上市日 YYYYMMDD + watched: boolean; +} + +export interface EtfListResponse { + total: number; + items: EtfListItem[]; +} + +export interface EtfSyncStatus { + running: boolean; + step?: string | null; + total: number; + done: number; + error?: string | null; + ready: boolean; + last_trade_date?: string | null; + last_synced_at?: string | null; + stats: { etfs?: number } & Record; +} + // ---------- 用户偏好 / 自选股 / 提问历史 ---------- export type Timeframe = '1d' | '1w' | '1M' | '1y'; export type AdjustMode = 'bfq' | 'qfq' | 'hfq'; @@ -263,6 +397,7 @@ export interface ChartLayoutPrefs { indexTimeframe?: Timeframe; // 首页上证指数 K 线周期 showBoll?: boolean; // 主图叠加 BOLL showZhixing?: boolean; // 主图叠加知行指标(趋势线+多空线) + showDividends?: boolean; // K线分红标记(默认开) } export interface ScreenerQueryItem { @@ -396,3 +531,83 @@ export interface MarketOverview { amount_history: AmountBar[]; // 近 N 交易日两市成交额(旧 -> 新,末根可能盘中) errors: string[]; } + +// ---------- 指数专题(国际指数卡片 + 指数详情) ---------- +export type GlobalRegion = 'americas' | 'europe' | 'asia'; + +export interface GlobalIndexQuote { + code: string; // DJI / SPX / HSI ... + name: string; // 道琼斯工业指数 + region: GlobalRegion; + country: string; // 美国 / 英国 / 日本 ... + close: number | null; + change: number | null; + pct_chg: number | null; + trade_date: string | null; // ISO 日期 + spark: number[]; // 近 45 交易日收盘(旧 -> 新) + spark_dates: string[]; +} + +export interface GlobalIndexList { + updated_at: string; + items: GlobalIndexQuote[]; + errors: string[]; +} + +export interface IndexBasic { + ts_code: string; + name: string; + market?: string | null; // SSE / CSI / SZSE(国内) + publisher?: string | null; // 中证指数 / 上交所 + category?: string | null; // 规模指数 / 综合指数 + base_date?: string | null; // 基期 + base_point?: number | null; // 基点 + list_date?: string | null; // 发布日期 + country?: string | null; // 国际指数:国家/地区 + region?: string | null; // 国际指数:americas/europe/asia +} + +export interface IndexValuationPoint { + trade_date: string; + pe?: number | null; + pe_ttm?: number | null; + pb?: number | null; + turnover_rate?: number | null; // 换手率 % + total_mv?: number | null; // 总市值(万元) + float_mv?: number | null; // 流通市值(万元) +} + +export interface IndexQuoteBrief { + close: number | null; + change: number | null; + pct_chg: number | null; + open: number | null; + high: number | null; + low: number | null; + pre_close: number | null; + trade_date: string | null; + spark: number[]; + spark_dates: string[]; +} + +export interface IndexDetail { + code: string; + name: string; + region: 'cn' | GlobalRegion; + quote: IndexQuoteBrief; + basic: IndexBasic | null; + valuation: IndexValuationPoint | null; // 接口不覆盖该指数时为 null + valuation_history: IndexValuationPoint[]; // 近 N 日(PE 走势小图) +} + +export interface IndexWeightItem { + con_code: string; // 000001.SZ + name: string | null; // 平安银行(本地回填,缺则 null) + weight: number; // 权重 % +} + +export interface IndexWeights { + trade_date: string; + total: number; + items: IndexWeightItem[]; +} diff --git a/frontend/src/components/DetailKLine.vue b/frontend/src/components/DetailKLine.vue index 2ac1ab6..bcd60cf 100644 --- a/frontend/src/components/DetailKLine.vue +++ b/frontend/src/components/DetailKLine.vue @@ -29,15 +29,16 @@ for (const t of [ registerOverlay(t); } -// ---------- 实盘买卖点标记(交割单导入) ---------- +// ---------- 实盘买卖点 / 分红事件标记(同一模板,两组 groupId 独立管理) ---------- // v10 无 v9 的 simpleMarker,须注册自定义模板;字母种类/明细经 extendData 传入。 // A股惯例(通达信/同花顺同款):B 买贴 low 下方、S 卖贴 high 上方、T 当日买+卖(做T)贴 high 上方; -// 图上只显示单个字母徽章(色底白字,用户指定固定配色,不随涨跌设置),成交明细(数量/均价/费用) -// 悬停字母时由组件浮层展示——onMouseEnter/onMouseLeave 是创建项级回调(OverlayCreate 未 Omit -// 事件键),闭包进组件状态即可(模板是模块级的,拿不到组件实例)。 +// D 分红除权日贴 high 上方(紫)。图上只显示单个字母徽章(色底白字,用户指定固定配色, +// 不随涨跌设置),明细(成交数量/均价/费用 或 分红方案)悬停字母时由组件浮层展示—— +// onMouseEnter/onMouseLeave 是创建项级回调(OverlayCreate 未 Omit 事件键), +// 闭包进组件状态即可(模板是模块级的,拿不到组件实例)。 interface TradeRow { label: string; text: string; tone: 'buy' | 'sell' | '' } -interface TradeMarkExt { kind: 'B' | 'S' | 'T'; rows: TradeRow[] } -const TRADE_COLORS: Record<'B' | 'S' | 'T', string> = { B: '#FE354B', S: '#3B7BBF', T: '#F9A504' }; +interface TradeMarkExt { kind: 'B' | 'S' | 'T' | 'D'; rows: TradeRow[] } +const TRADE_COLORS: Record<'B' | 'S' | 'T' | 'D', string> = { B: '#FE354B', S: '#3B7BBF', T: '#F9A504', D: '#9C6ADE' }; const tradeMarkerTemplate: OverlayTemplate = { name: 'tradeMarker', totalStep: 2, @@ -106,6 +107,9 @@ const props = defineProps<{ * T=当日买+卖(做T)贴 high 上方;rows 为悬停明细(数量/均价/费用)。 * 只画落在已渲染窗口内的(更早的等左滑翻页后自动补画) */ tradeMarkers?: { key: string; ts: number; kind: 'B' | 'S' | 'T'; rows: TradeRow[] }[]; + /** 分红事件标记(tushare dividend,按除权除息日贴 high 上方,kind 固定 'D'): + * rows 为悬停明细(每股分红/送转/登记日等)。与买卖点同模板不同 groupId,独立开关 */ + dividendMarkers?: { key: string; ts: number; rows: TradeRow[] }[]; }>(); const emit = defineEmits<{ @@ -520,8 +524,8 @@ function pickTool(key: string) { function clearOverlays() { chart?.removeOverlay(); activeTool.value = ''; - // removeOverlay() 无参清的是全部 overlay(含交易点)——交易点不是用户画线,重画回来 - renderTradeMarkers(); + // removeOverlay() 无参清的是全部 overlay(含交易点/分红)——事件标记不是用户画线,重画回来 + renderMarkers(); } // ---------- 日期跳转居中 ---------- @@ -559,12 +563,13 @@ function centerOn(ts: number): boolean { } defineExpose({ centerOn }); -// ---------- 实盘买卖点标记渲染 ---------- +// ---------- 事件标记渲染(买卖点 + 分红,同模板不同 groupId) ---------- const TRADE_GROUP = 'trades'; +const DIVIDEND_GROUP = 'dividends'; -/** 交易点允许吸附到「晚于最后一根K时间戳」的窗口,按周期放大:周/月/年K的 bar 时间戳 - * 是周期首日(周一/1日/1月1日),当前周期内的成交(如月中)仍应贴到最后一根上。 - * 日K严格为 0:行情未同步到成交日时宁可先不画(数据同步后重建图表自动补上), +/** 事件允许吸附到「晚于最后一根K时间戳」的窗口,按周期放大:周/月/年K的 bar 时间戳 + * 是周期首日(周一/1日/1月1日),当前周期内的事件(如月中成交/除权)仍应贴到最后一根上。 + * 日K严格为 0:行情未同步到事件日时宁可先不画(数据同步后重建图表自动补上), * 也不能把周一的成交错标到周五的K线上。 */ const TRADE_AHEAD_MS: Record = { '1d': 0, @@ -573,27 +578,27 @@ const TRADE_AHEAD_MS: Record = { '1y': 366 * 86400000, }; -/** 按 groupId 整组重建买卖点标记(先删后建,幂等)。交易日期按时间戳吸附到所在 bar: - * B 贴 bar.low 下方、S/T 贴 bar.high 上方;坐标随复权切换自动重算(value 取自当前数据)。 - * 早于已渲染窗口的交易先跳过——左滑翻页 serveOlder 吐出新数据后会重跑本函数补画。 - * 列表为空(关闭显示/清空成交/切到无成交股票)也必须清组,否则旧标记残留。 */ -function renderTradeMarkers() { +/** 按 groupId 整组重建事件标记(先删后建,幂等)。事件日期按时间戳吸附到所在 bar: + * B 贴 bar.low 下方、S/T/D 贴 bar.high 上方;坐标随复权切换自动重算(value 取自当前数据)。 + * 早于已渲染窗口的事件先跳过——左滑翻页 serveOlder 吐出新数据后会重跑本函数补画。 + * 列表为空(关闭显示/清空成交/切到无分红股票)也必须清组,否则旧标记残留。 */ +function renderMarkerGroup(groupId: string, markers: { key: string; ts: number; kind: 'B' | 'S' | 'T' | 'D'; rows: TradeRow[] }[] | undefined) { if (!chart) return; tradeTip.value = null; // 组重建期间字母已换位,旧明细浮层不能留在原地 - chart.removeOverlay({ groupId: TRADE_GROUP }); - if (!props.tradeMarkers?.length) return; + chart.removeOverlay({ groupId }); + if (!markers?.length) return; const list = chart.getDataList(); if (list.length === 0) return; const lastTs = list[list.length - 1].timestamp; const aheadMs = TRADE_AHEAD_MS[props.timeframe] ?? 0; const creates: OverlayCreate[] = []; - for (const m of props.tradeMarkers) { + for (const m of markers) { const i = idxAtOrBefore(list, m.ts); if (i < 0 || m.ts > lastTs + aheadMs) continue; // 未翻到 / 行情尚未覆盖该周期 const bar = list[i]; creates.push({ - id: `trade-${m.key}`, - groupId: TRADE_GROUP, + id: `${groupId}-${m.key}`, + groupId, name: 'tradeMarker', points: [{ timestamp: bar.timestamp, value: m.kind === 'B' ? bar.low : bar.high }], extendData: { kind: m.kind, rows: m.rows }, @@ -615,8 +620,14 @@ function renderTradeMarkers() { if (creates.length) chart.createOverlay(creates); } -// ---------- 交易点悬停明细(悬停 B/S/T 字母才显示,离开/滚动即隐) ---------- -interface TradeTip { x: number; y: number; kind: 'B' | 'S' | 'T'; date: string; rows: TradeRow[] } +/** 全部事件标记重画(买卖点 + 分红),触发点:build 尾部 / serveOlder 左扩 / props 变化 / 清画线 */ +function renderMarkers() { + renderMarkerGroup(TRADE_GROUP, props.tradeMarkers); + renderMarkerGroup(DIVIDEND_GROUP, props.dividendMarkers?.map((m) => ({ ...m, kind: 'D' as const }))); +} + +// ---------- 事件标记悬停明细(悬停 B/S/T/D 字母才显示,离开/滚动即隐) ---------- +interface TradeTip { x: number; y: number; kind: 'B' | 'S' | 'T' | 'D'; date: string; rows: TradeRow[] } const tradeTip = ref(null); /** 贴鼠标定位并在右缘/下缘自动翻转(与十字线浮层 placeHover 同款策略,宽度略大) */ @@ -669,7 +680,7 @@ function build() { const start = allData.length - served - take; served += take; callback(allData.slice(start, start + take), { forward: canBack(), backward: false }); - renderTradeMarkers(); // 窗口左扩后补画此前跳过的更早交易点 + renderMarkers(); // 窗口左扩后补画此前跳过的更早事件标记(交易/分红) }; const answerEmpty = () => callback([], { forward: false, backward: false }); if (type === 'init') { @@ -764,8 +775,8 @@ function build() { // 居中失败(锚点早于上市首日/晚于最后一根)必须上报:否则锚点 chip 与统计口径 // 仍停留在「已定位」状态,视口却悄悄回到最新行情。 if (props.centerTs != null && !centerOn(props.centerTs)) emit('centerMiss', props.centerTs); - // init 数据在 setPeriod 时已同步落入图表,可直接画首屏窗口内的交易点 - renderTradeMarkers(); + // init 数据在 setPeriod 时已同步落入图表,可直接画首屏窗口内的事件标记 + renderMarkers(); } function teardown() { @@ -780,8 +791,8 @@ function teardown() { onMounted(build); onBeforeUnmount(teardown); watch(() => [props.candles, props.indicators, props.subPanes, props.showBoll, props.showZhixing, props.zhixingBlocks, props.maPeriods, props.timeframe], () => { teardown(); build(); }, { deep: true }); -// 买卖点数据变化(导入/清空/开关显示):只重画标记,不重建图表(保留滚动位置与用户画线) -watch(() => props.tradeMarkers, renderTradeMarkers, { deep: true }); +// 事件标记数据变化(导入/清空/开关显示/分红数据到达):只重画标记,不重建图表(保留滚动位置与用户画线) +watch(() => [props.tradeMarkers, props.dividendMarkers], renderMarkers, { deep: true }); // 涨跌配色切换:重建图表以应用新颜色 watch(() => settings.priceTone, () => { teardown(); build(); }); // 副图高度变化:仅调 pane 高度,不重建(保留滚动/画线状态) diff --git a/frontend/src/components/MarketOverview.vue b/frontend/src/components/MarketOverview.vue index a19967f..1ca71ab 100644 --- a/frontend/src/components/MarketOverview.vue +++ b/frontend/src/components/MarketOverview.vue @@ -1,14 +1,15 @@ diff --git a/frontend/src/components/StockDetailOverlay.vue b/frontend/src/components/StockDetailOverlay.vue index cea6f70..8bdbc20 100644 --- a/frontend/src/components/StockDetailOverlay.vue +++ b/frontend/src/components/StockDetailOverlay.vue @@ -1,15 +1,18 @@