From ad9245abdd692f8711b2dac56e19b0e0049eef52 Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Sat, 5 Sep 2026 00:12:19 +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 | 117 +++------- backend/app/data/tushare_provider.py | 29 --- backend/app/schemas.py | 22 -- backend_run.log | 204 ++++++++++++++++++ frontend/src/api/client.ts | 25 +-- frontend/src/api/types.ts | 26 +-- frontend/src/components/DetailKLine.vue | 132 +----------- frontend/src/components/MarketOverview.vue | 4 + .../src/components/StockDetailOverlay.vue | 105 +-------- frontend/src/indicators/chips.ts | 188 ---------------- frontend/src/indicators/index.ts | 10 +- frontend/src/stores/settings.ts | 20 +- 12 files changed, 287 insertions(+), 595 deletions(-) delete mode 100644 frontend/src/indicators/chips.ts diff --git a/backend/app/api.py b/backend/app/api.py index dbdf2c1..a87fe0e 100644 --- a/backend/app/api.py +++ b/backend/app/api.py @@ -3,8 +3,8 @@ GET /api/health 健康检查 GET /api/candles/{sym} 取 K 线(支持 1d/1w/1M/1y 周期,日线为基底聚合) GET /api/stocks 全市场股票列表(基本信息 + 最新行情 + 缓存条数) - GET /api/stock/{code}/chips 个股筹码峰(cyq_chips/cyq_perf,按复权口径换算) GET /api/market/overview 主页大盘总览(A 股/港美指数 + 两市市值成交统计) + GET /api/market/index-candles 上证指数全量 K 线(1d/1w/1M/1y 聚合) POST /api/backtest 跑回测,返回 K线+指标+买卖点+净值+绩效 POST /api/screener/run 智能选股:自然语言 -> 条件 -> 全市场筛选 POST /api/screener/sync 启动全市场数据同步(后台任务) @@ -18,6 +18,7 @@ import json from datetime import datetime import pandas as pd +from pydantic import TypeAdapter from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile from fastapi.responses import StreamingResponse from sqlalchemy import delete, func, select, text @@ -30,8 +31,9 @@ from .auth import require_user from .backtest.events import EventEngineError, run_event_backtest from .backtest.strategies import build_strategy from .config import settings -from .data import fetcher, repository, tushare_provider +from .data import fetcher, repository 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 .db import async_session, get_session @@ -53,8 +55,6 @@ from .schemas import ( BacktestRequest, BacktestResponse, CandleOut, - ChipRowOut, - ChipsResponse, EquityPoint, EventBacktestRequest, EventBacktestResponse, @@ -133,6 +133,8 @@ def _rows_to_bars(rows) -> list[Bar]: _ADJUST_MODES = ("bfq", "qfq", "hfq") # MA 全量集合(前端已改为本地计算 MA,后端始终返回此集合以保证缓存一致) _FULL_MA_SET = (5, 10, 20, 30, 60, 120, 250) +# 指数 K 线支持的周期(日线基底聚合) +_INDEX_TIMEFRAMES = ("1d", "1w", "1M", "1y") # 信息卡一条 SQL 拿全:stock_basic 基本信息 + 「优先与行情同日、缺则最新日」的 daily_snapshot # (LATERAL 单条替换原两条查询,语义不变:target 为 NULL 时全按最新日兜底) @@ -409,6 +411,32 @@ async def get_market_overview(session: AsyncSession = Depends(get_session)) -> M return MarketOverviewResponse(**data) +@router.get("/market/index-candles", response_model=list[CandleOut]) +async def get_index_candles(timeframe: str = "1d") -> Response: + """上证指数全量 K 线:日线为基底(tushare index_daily,进程内+Redis 缓存), + 聚合到 1d/1w/1M/1y。收盘口径(数据随 EOD 更新,与总览 spark 一致)。""" + if timeframe not in _INDEX_TIMEFRAMES: + raise HTTPException(status_code=400, detail=f"timeframe 仅支持 {'/'.join(_INDEX_TIMEFRAMES)}") + key = f"idxkj:{cache.digest('idxc', SH_INDEX, timeframe)}" + cached = await _cached_json_response(key) + if cached is not None: + return cached + try: + bars = resample_bars(await get_index_daily(), 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 + ] + # pydantic-core 序列化(与 _raw_json 同款),历史不可变、TTL 兜到当日更新 + 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.post("/backtest", response_model=BacktestResponse) async def backtest( req: BacktestRequest, @@ -1112,84 +1140,3 @@ async def screener_preview( cache.local_set(f"pvj:{cache_key}", raw, ttl=120) asyncio.create_task(cache.cache_set(f"pvj:{cache_key}", raw, ttl=600)) return Response(content=raw, media_type="application/json") - - -@router.get("/stock/{ts_code}/chips", response_model=ChipsResponse) -async def stock_chips( - ts_code: str, date: str | None = None, adjust: str = "qfq", - session: AsyncSession = Depends(get_session), -) -> Response: - """个股筹码峰(Tushare cyq_chips + cyq_perf,数据自 2018 年起)。 - - date=YYYY-MM-DD 为参考日(日 K 传当日;周/月 K 由前端传周期末):返回 - <=date 的最近有筹码数据的交易日截面;缺省取最新。 - 价格/成本均按 adjust(bfq/qfq/hfq)用 adj_factor 本地换算,与 K 线同口径。 - """ - if adjust not in _ADJUST_MODES: - raise HTTPException(status_code=400, detail=f"adjust 仅支持 {'/'.join(_ADJUST_MODES)}") - ref: str | None = None - if date: - try: - ref = datetime.strptime(date.strip()[:10], "%Y-%m-%d").strftime("%Y%m%d") - except ValueError: - raise HTTPException(status_code=400, detail="date 格式应为 YYYY-MM-DD") - - # 历史截面不可变;键带 candles 版本号(adj_factor 随同步更新后旧缓存失效)。 - # 同 preview:存序列化 JSON 直返(j: 前缀),跳过 pydantic 校验/序列化。 - cache_key = cache.digest("chips", ts_code, ref or "latest", adjust, await cache.get_version("candles")) - cached = await _cached_json_response(f"chipsj:{cache_key}") - if cached is not None: - return cached - - try: - perf, rows = await asyncio.to_thread(tushare_provider.fetch_chips, ts_code, ref) - except Exception as e: # noqa: BLE001 - raise HTTPException(status_code=502, detail=f"筹码数据获取失败: {e}") - if not perf: - resp = ChipsResponse( - ts_code=ts_code, trade_date=None, adjust=adjust, - error="无筹码数据(cyq 数据自 2018 年起,或参考日早于数据起点)", - ) - raw = _raw_json(resp) - cache.local_set(f"chipsj:{cache_key}", raw, ttl=120) - await cache.cache_set(f"chipsj:{cache_key}", raw, ttl=3600) - return Response(content=raw, media_type="application/json") - - d = datetime.strptime(str(perf["trade_date"]), "%Y%m%d") - - # 复权换算(与 _adjust_bars 同口径):qfq=f(d)/f_latest,hfq=f(d),bfq=1 - mult = 1.0 - if adjust != "bfq": - factors = (await session.execute( - select(AdjFactor.trade_date, AdjFactor.adj_factor) - .where(AdjFactor.ts_code == ts_code, AdjFactor.trade_date <= d) - .order_by(AdjFactor.trade_date) - )).all() - if factors: - latest_f = (await session.execute( - select(AdjFactor.trade_date, AdjFactor.adj_factor).where(AdjFactor.ts_code == ts_code) - .order_by(AdjFactor.trade_date.desc()).limit(1) - )).first() - f_at = float(factors[-1][1]) # <=d 的最近因子(因子是阶梯函数) - f_latest = float(latest_f[1]) if latest_f else f_at - mult = f_at / f_latest if adjust == "qfq" else f_at - - def _px(v) -> float | None: - return None if v is None or v != v else round(float(v) * mult, 3) - - resp = ChipsResponse( - ts_code=ts_code, - trade_date=str(perf["trade_date"]), - adjust=adjust, - rows=[ChipRowOut(price=round(p * mult, 3), percent=v) for p, v in rows], - his_low=_px(perf.get("his_low")), his_high=_px(perf.get("his_high")), - cost_5pct=_px(perf.get("cost_5pct")), cost_15pct=_px(perf.get("cost_15pct")), - cost_50pct=_px(perf.get("cost_50pct")), cost_85pct=_px(perf.get("cost_85pct")), - cost_95pct=_px(perf.get("cost_95pct")), - weight_avg=_px(perf.get("weight_avg")), - winner_rate=_px(perf.get("winner_rate")), - ) - raw = _raw_json(resp) - cache.local_set(f"chipsj:{cache_key}", raw, ttl=120) - await cache.cache_set(f"chipsj:{cache_key}", raw, ttl=21600) - return Response(content=raw, media_type="application/json") diff --git a/backend/app/data/tushare_provider.py b/backend/app/data/tushare_provider.py index 0c40a33..fd97ad5 100644 --- a/backend/app/data/tushare_provider.py +++ b/backend/app/data/tushare_provider.py @@ -66,32 +66,3 @@ def fetch_daily(code: str, start: str = "20200101", end: str | None = None, ) ) return bars - - -def fetch_chips(ts_code: str, end: str | None = None): - """拉取筹码分布截面:cyq_perf(成本分位/获利比例/平均成本)+ cyq_chips(价位→占比)。 - - end 为 YYYYMMDD 参考日:取 <=end 的最近有数据交易日(周/月 K 线由前端换算成 - 周期末传入,这里只需向前吸附到实际数据日);None 取最新。 - 数据自 2018 年起,早于此返回 (None, [])。返回 (perf_dict|None, [(price, percent)])。 - """ - from datetime import timedelta - - pro = get_pro() - e = end or datetime.now().strftime("%Y%m%d") - if e < "20180101": - return None, [] - # 吸附余量 60 个自然日:覆盖春节等长假 + 月初参考日(如月 K 传上月末) - s = (datetime.strptime(e, "%Y%m%d") - timedelta(days=60)).strftime("%Y%m%d") - s = max(s, "20180101") - - perf = pro.cyq_perf(ts_code=ts_code, start_date=s, end_date=e) - if perf is None or perf.empty: - return None, [] - perf = perf.sort_values("trade_date").iloc[-1] # <=end 的最近一条 - d = str(perf["trade_date"]) - chips = pro.cyq_chips(ts_code=ts_code, trade_date=d) - rows: list[tuple[float, float]] = [] - if chips is not None and not chips.empty: - rows = [(float(p), float(v)) for p, v in zip(chips["price"], chips["percent"])] - return perf.to_dict(), rows diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 0f4303d..4daed28 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -413,25 +413,3 @@ class MarketOverviewResponse(BaseModel): amount_history: list[AmountBarOut] = [] # 近 N 交易日两市成交额(旧 -> 新,末根可能盘中) errors: list[str] = [] # 部分来源失败的说明(透明但不阻塞展示) - -# ---------- 筹码峰(个股详情) ---------- -class ChipRowOut(BaseModel): - price: float # 已按 adjust 换算的价位 - percent: float # 该价位筹码占比(0~1,全价位合计 ≈1) - - -class ChipsResponse(BaseModel): - ts_code: str - trade_date: str | None = None # 筹码截面所在交易日(YYYYMMDD) - adjust: str # rows[].price 的复权口径 bfq/qfq/hfq - rows: list[ChipRowOut] = Field(default_factory=list) - his_low: float | None = None # 历史最低/最高价(换算后) - his_high: float | None = None - cost_5pct: float | None = None # 成本分位(换算后),5/95 即 90% 筹码成本区间 - cost_15pct: float | None = None - cost_50pct: float | None = None - cost_85pct: float | None = None - cost_95pct: float | None = None - weight_avg: float | None = None # 平均成本(换算后) - winner_rate: float | None = None # 获利比例 % - error: str | None = None # 无数据时的提示(2018 前无数据等) diff --git a/backend_run.log b/backend_run.log index f5d4fe0..9d5b931 100644 --- a/backend_run.log +++ b/backend_run.log @@ -68,3 +68,207 @@ 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: 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: 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: 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 diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index c2cb830..10cb32e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -2,7 +2,7 @@ import type { AdjustMode, BacktestRequest, BacktestResponse, - ChipsResponse, + Candle, CurrentUser, EventBacktestRequest, EventBacktestResponse, @@ -191,22 +191,6 @@ export async function getStockPreview( return (await res.json()) as PreviewResponse; } -// ---------- 筹码分布(个股筹码峰) ---------- -/** 获取某参考日的筹码截面:date 缺省取最新;周/月 K 线由调用方换算成周期末日传入, - * 后端吸附到 <=date 的最近有数据交易日。价格与成本均已按 adjust 口径换算。 */ -export async function getStockChips( - tsCode: string, - opts: { date?: string; adjust?: AdjustMode } = {}, -): Promise { - const q = new URLSearchParams(); - if (opts.date) q.set('date', opts.date); - if (opts.adjust) q.set('adjust', opts.adjust); - const qs = q.toString(); - const res = await apiFetch(`/api/stock/${encodeURIComponent(tsCode)}/chips${qs ? `?${qs}` : ''}`); - if (!res.ok) throw new ApiError(await readError(res, `获取筹码分布失败 (HTTP ${res.status})`), res.status); - return (await res.json()) as ChipsResponse; -} - // ---------- 大盘总览(主页) ---------- export async function getMarketOverview(): Promise { const res = await apiFetch('/api/market/overview'); @@ -214,6 +198,13 @@ export async function getMarketOverview(): Promise { return (await res.json()) as MarketOverview; } +/** 上证指数全量 K 线(日线基底聚合到目标周期;收盘口径) */ +export async function getIndexCandles(timeframe: Timeframe): Promise { + const res = await apiFetch(`/api/market/index-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[]; +} + export async function getStocks(params: { search?: string; market?: string; diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 97ccd24..dfe4710 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -260,6 +260,9 @@ export interface ChartLayoutPrefs { subHeights: Record; // 面板高度 px timeframe?: Timeframe; tooltipFields?: TooltipField[]; // 浮层显示的指标(顺序即行序;空数组=仅日期头) + indexTimeframe?: Timeframe; // 首页上证指数 K 线周期 + showBoll?: boolean; // 主图叠加 BOLL + showZhixing?: boolean; // 主图叠加知行指标(趋势线+多空线) } export interface ScreenerQueryItem { @@ -357,29 +360,6 @@ export interface EventBacktestResponse { total: number; } -// ---------- 筹码分布(cyq_chips/cyq_perf,数据自 2018 年起) ---------- -export interface ChipRow { - price: number; // 已按 adjust 口径换算的价位 - percent: number; // 该价位筹码占比(0~1,全价位合计约 1) -} - -export interface ChipsResponse { - ts_code: string; - trade_date: string | null; // YYYYMMDD 截面日 - adjust: string; - rows: ChipRow[]; - his_low: number | null; - his_high: number | null; - cost_5pct: number | null; - cost_15pct: number | null; - cost_50pct: number | null; - cost_85pct: number | null; - cost_95pct: number | null; - weight_avg: number | null; // 平均成本 - winner_rate: number | null; // 获利比例 % - error: string | null; -} - // ---------- 大盘总览(主页) ---------- export interface IndexQuote { code: string; // 000001.SH / HKTECH / DJI diff --git a/frontend/src/components/DetailKLine.vue b/frontend/src/components/DetailKLine.vue index dce376b..2ac1ab6 100644 --- a/frontend/src/components/DetailKLine.vue +++ b/frontend/src/components/DetailKLine.vue @@ -11,7 +11,8 @@ import { threeWaves, triangle, xabcd, } from '@klinecharts/extension'; import { useSettingsStore, TOOLTIP_FIELDS, DEFAULT_TOOLTIP_FIELDS } from '@/stores/settings'; -import { registerCustomIndicators, computeZhixingSeries, DKX_PERIODS, ZHIXING_NAME, CHIPS_NAME, CHIPS_COL_PX, type ChipsExt } from '@/indicators'; +import { darkStyles as sharedDarkStyles } from '@/chartStyles'; +import { registerCustomIndicators, computeZhixingSeries, DKX_PERIODS, ZHIXING_NAME } from '@/indicators'; import type { Candle, TooltipField } from '@/api/types'; // 通达信导入的自定义主图指标注册表;知行序列走 PV 管道按时间戳取预计算值。 @@ -95,10 +96,6 @@ const props = defineProps<{ showZhixing: boolean; /** 知行指标图例行的板块文本(通达信 HYBLOCK+DYBLOCK 的近似,行业+地域) */ zhixingBlocks?: string; - /** 主图是否叠加筹码峰(cyq 截面横向直方图,纯 draw 绘制) */ - showChips: boolean; - /** 筹码峰截面数据(父组件按 当前窗口末根/十字线 所在周期取数;null=暂无) */ - chips?: ChipsExt | null; /** K线周期标签(仅用于 MA 指标名缓存 key) */ timeframe: string; /** 浮层显示的指标(可选;缺省=目录全开,空数组=仅日期头) */ @@ -114,8 +111,6 @@ const props = defineProps<{ const emit = defineEmits<{ /** 日期跳转锚点在本次数据窗口里找不到(早于上市/晚于最后一根):请父组件回退到最新行情并提示 */ (e: 'centerMiss', ts: number): void; - /** 十字线落在某根K线上(开启筹码峰时逐根上报,父组件防抖后取该周期筹码截面) */ - (e: 'crosshairDate', ts: number): void; }>(); // A股语义色(黑底高对比);UP/DOWN 跟随设置中的涨跌配色 @@ -327,35 +322,9 @@ function maybePrefetch(myEpoch: number) { } function darkStyles() { - return { - grid: { horizontal: { color: '#1C1E24' }, vertical: { color: '#1C1E24' } }, - // 内建 VOL 等指标柱的涨跌色缺省是库默认绿涨红跌,与全站语义相反; - // bars[0] 与库默认项深合并(init 时 merge 到 getDefaultStyles),只覆盖三个颜色键, - // UP/DOWN 在 build() 里随 settings 刷新,故切「绿涨红跌」重建后同样生效 - indicator: { bars: [{ upColor: UP, downColor: DOWN, noChangeColor: '#7A818C' }] }, - candle: { - bar: { - upColor: UP, downColor: DOWN, - upBorderColor: UP, downBorderColor: DOWN, - upWickColor: UP, downWickColor: DOWN, - }, - priceMark: { - high: { color: '#9AA0AA' }, low: { color: '#9AA0AA' }, - last: { upColor: UP, downColor: DOWN }, - }, - }, - xAxis: { axisLine: { color: '#2A2D34' }, tickText: { color: '#9AA0AA', size: 12 }, tickLine: { color: '#2A2D34' } }, - yAxis: { axisLine: { color: '#2A2D34' }, tickText: { color: '#9AA0AA', size: 12 }, tickLine: { color: '#2A2D34' } }, - crosshair: { - horizontal: { text: { backgroundColor: '#333A45' } }, - vertical: { text: { backgroundColor: '#333A45' } }, - }, - separator: { - color: '#23252B', - // 悬停/拖拽分隔条时的底色(库默认 8% 蓝在纯黑底上不可见,加重为可感知的拖拽提示) - activeBackgroundColor: 'rgba(37, 99, 235, 0.30)', - }, - }; + // 共享暗色主题(chartStyles.ts);UP/DOWN 在 build() 里随 settings 刷新, + // 故切「绿涨红跌」重建后同样生效 + return sharedDarkStyles(UP, DOWN); } // 副图默认高度 @@ -437,8 +406,6 @@ function bindCrosshair(ch: Chart) { const idx = ts != null ? idxOfTs(ts) : -1; // 全量数组与指标序列按索引对齐 if (idx < 0) { hover.value = null; return; } const k = allData[idx]; - // 筹码峰跟随:十字线逐根上报(父组件防抖取截面);不开启时不上报,避免无谓事件 - if (props.showChips) emit('crosshairDate', k.timestamp); const prev = idx > 0 ? allData[idx - 1] : null; const diff = prev ? k.close - prev.close : null; const chg = prev ? (diff! / prev.close) * 100 : null; @@ -662,70 +629,8 @@ function placeTradeTip(px: number, py: number, rowCount: number): { x: number; y return { x, y }; } -// ---------- 筹码峰指标同步(不重建图表:createIndicator 不支持原地改 extendData, -// 先删后建即可;数据/开关变化走这里,保留滚动位置与用户画线) ---------- -function syncChipsIndicator() { - if (!chart) return; - const exists = chart.getIndicators().some((i) => i.name === CHIPS_NAME); - const want = props.showChips && !!props.chips; - if (want) { - if (exists) chart.removeIndicator({ name: CHIPS_NAME }); - chart.createIndicator({ name: CHIPS_NAME, paneId: 'candle_pane', extendData: props.chips ?? undefined }); - syncRightOffset(); // 开关即扩留白(不等数据),数据到达时列直接落在已预留的空白里 - } else if (exists) { - chart.removeIndicator({ name: CHIPS_NAME }); - restoreRightOffset(); - } else { - restoreRightOffset(); // 数据未到即关:同样要还原已扩出去的留白 - } -} - -// ---------- 筹码峰竖列的右侧留白管理 ---------- -// klinecharts 滚动状态唯一变量是 diff(最后一根K距绘图区右缘的 bar 数), -// setOffsetRightDistance(px) 会把 diff 直接重置为 px/barSpace 并 relayout——等价于 -// “视口拽回实时位并留 px 空白”。因此只在「右区」(末根K在视野内,getVisibleRange().to -// 达到 data 长度)时调用;用户在看历史时绝不调,滚回右区后由 onScroll 防抖补一次吸附 -// (同花顺松手吸附同款)。绝不调 setMaxOffsetRightDistance:会把滚动限制切到 -// 'distance' 模式,默认钳到 50px 且改变滚动边缘行为。 +// 右侧固定留白(价格轴与末根K之间的呼吸空间;scrollToRealTime 以它为锚点) const BASE_RIGHT_PX = 28; -let syncingRightOffset = false; // 重入守卫:set 会同步派发 onVisibleRangeChange -let scrollReanchorTimer: ReturnType | null = null; -let managedRightPx = 0; // 筹码开启期间我们 set 过的目标值(关筹码时判断“是否还归我们管”) - -/** 目标右侧留白:开筹码 = 列宽 + 一根K线 + 余量(bar 最宽 50px → 峰值 ~182px) */ -function targetRightPx(): number { - if (!props.showChips) return BASE_RIGHT_PX; - const bar = chart?.getBarSpace().bar || 10; - return Math.round(CHIPS_COL_PX + bar + 12); -} - -/** 开筹码期间保证留白盖住竖列:只扩不缩(用户自己拖出的更大留白不动) */ -function syncRightOffset(): void { - if (!chart || syncingRightOffset || !props.showChips) return; - const list = chart.getDataList(); - if (!list.length || chart.getVisibleRange().to < list.length) return; // 右区守卫 - const target = targetRightPx(); - if (chart.getOffsetRightDistance() >= target - 2) return; // 阈值防循环 + 只扩不缩 - syncingRightOffset = true; - try { - chart.setOffsetRightDistance(target); - managedRightPx = target; - } finally { - syncingRightOffset = false; - } -} - -/** 关筹码时一次性还原 28px:仅当留白仍在我们管理范围内(用户没手动拖大过) */ -function restoreRightOffset(): void { - if (!chart || managedRightPx === 0) return; - const list = chart.getDataList(); - const cur = chart.getOffsetRightDistance(); - if (list.length && chart.getVisibleRange().to >= list.length - && cur > BASE_RIGHT_PX + 2 && cur <= managedRightPx + 2) { - chart.setOffsetRightDistance(BASE_RIGHT_PX); - } - managedRightPx = 0; -} function build() { if (!container.value || props.candles.length === 0) return; @@ -819,7 +724,6 @@ function build() { calcParams: DKX_PERIODS, extendData: { blocks: props.zhixingBlocks }, }); } - if (props.showChips) syncChipsIndicator(); // 筹码峰(数据未到时 draw 直接空转,到货后由 watch 补建) // 副图按用户顺序创建,并设置用户高度;主图吃剩余高度。 // minHeight 交给库在分隔条拖拽时强制执行(与 subH 的 40px 下限一致) @@ -852,23 +756,8 @@ function build() { const from = (payload as { data?: { from?: unknown } }).data?.from; if (typeof from === 'number' && from < 200) maybePrefetch(myEpoch); }); - // 右侧留白:开筹码时按竖列宽度预留(scrollToRealTime 以它为锚点,先后顺序不能换) - ch.setOffsetRightDistance(props.showChips ? targetRightPx() : BASE_RIGHT_PX); - // 筹码竖列的留白跟随:缩放后 barSpace 变化(onZoom 触发时新值已生效)立即补扩; - // 拖拽期间 onScroll 逐帧触发、且拖拽每帧从手势起点快照重算 diff,逐帧同步会与拖拽 - // “互搏”(从实时位拖向历史会被逐帧拽回),故防抖 150ms 只在手势结束后补一次 - ch.subscribeAction('onZoom', () => { - if (myEpoch !== epoch) return; - syncRightOffset(); - }); - ch.subscribeAction('onScroll', () => { - if (myEpoch !== epoch) return; - if (scrollReanchorTimer) clearTimeout(scrollReanchorTimer); - scrollReanchorTimer = setTimeout(() => { - scrollReanchorTimer = null; - if (myEpoch === epoch) syncRightOffset(); - }, 150); - }); + // 右侧留白(scrollToRealTime 以它为锚点,先后顺序不能换) + ch.setOffsetRightDistance(BASE_RIGHT_PX); ch.scrollToRealTime(); // 日期跳转:build 尾部的 scrollToRealTime 会把视口重置到最新一根,居中必须放在它之后 //(init 数据在 setPeriod 时已同步落入图表,这里可直接定位)。 @@ -881,8 +770,6 @@ function build() { function teardown() { if (subHTimer) { clearTimeout(subHTimer); subHTimer = null; } - if (scrollReanchorTimer) { clearTimeout(scrollReanchorTimer); scrollReanchorTimer = null; } - managedRightPx = 0; if (container.value) dispose(container.value); chart = null; hover.value = null; @@ -893,9 +780,6 @@ 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.showChips, props.chips], syncChipsIndicator, { deep: true }); // 买卖点数据变化(导入/清空/开关显示):只重画标记,不重建图表(保留滚动位置与用户画线) watch(() => props.tradeMarkers, renderTradeMarkers, { deep: true }); // 涨跌配色切换:重建图表以应用新颜色 diff --git a/frontend/src/components/MarketOverview.vue b/frontend/src/components/MarketOverview.vue index bc53036..a19967f 100644 --- a/frontend/src/components/MarketOverview.vue +++ b/frontend/src/components/MarketOverview.vue @@ -5,6 +5,7 @@ import { getMarketOverview } from '@/api/client'; import type { IndexQuote, MarketOverview as Overview } from '@/api/types'; import AmountHistoryChart from '@/components/AmountHistoryChart.vue'; +import IndexKLine from '@/components/IndexKLine.vue'; // 主页大盘总览:A 股 + 港美指数最近收盘(收盘口径,标注交易日),沪深两市市值/成交统计。 // 数据为 EOD 口径,进页面拉一次 + 手动刷新即可,不做轮询。 @@ -226,6 +227,9 @@ function hoverText(it: IndexQuote): string { + + +
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { - addWatchlist, clearTrades, getStockChips, getStockPreview, getTrades, getWatchlist as getWatchlistApi, + addWatchlist, clearTrades, getStockPreview, getTrades, getWatchlist as getWatchlistApi, importTrades, removeWatchlist, } from '@/api/client'; import type { - ChartLayoutPrefs, ChipsResponse, PreviewResponse, ScreenerItemOut, Timeframe, TooltipField, + ChartLayoutPrefs, PreviewResponse, ScreenerItemOut, Timeframe, TooltipField, TradesImportResponse, UserTrade, } from '@/api/types'; import { useSettingsStore, DEFAULT_TOOLTIP_FIELDS, TOOLTIP_FIELDS, type PriceAdjust } from '@/stores/settings'; -import type { ChipsExt } from '@/indicators'; import DetailKLine from './DetailKLine.vue'; const props = defineProps<{ @@ -67,8 +66,11 @@ const subPanes = computed(() => layout.value.subPanes); const maPeriods = computed(() => layout.value.maPeriods); const subHeights = computed(() => layout.value.subHeights); const tooltipFields = computed(() => layout.value.tooltipFields ?? DEFAULT_TOOLTIP_FIELDS); -const showBoll = ref(false); -const showZhixing = ref(false); +// 主图可选叠加(BOLL / 知行):随用户偏好持久化,跨会话/跨设备记忆开关 +const showBoll = computed(() => layout.value.showBoll === true); +const showZhixing = computed(() => layout.value.showZhixing === true); +function toggleBoll() { settings.setChartLayout({ showBoll: !showBoll.value }); } +function toggleZhixing() { settings.setChartLayout({ showZhixing: !showZhixing.value }); } // 知行指标图例行的板块文本(通达信 HYBLOCK+' '+DYBLOCK 的近似;GNBLOCK 概念板块无数据源) const zhixingBlocks = computed(() => { const info = data.value?.info; @@ -76,84 +78,6 @@ const zhixingBlocks = computed(() => { return [info.industry, info.area].filter(Boolean).join(' ') || undefined; }); -// ---------- 筹码峰(cyq 截面,跟随 日/周/月K 的当前周期 + 十字线) ---------- -const showChips = ref(false); -const chipsExt = ref(null); -let chipsToken = 0; // 请求序号:切股/切口径瞬间乱序返回的旧截面不能回填 -let chipsKey = ''; // 已请求去重键(code|adjust|参考日) - -function toChipsExt(res: ChipsResponse, refClose: number): ChipsExt { - return { - rows: res.rows.map((r) => ({ price: r.price, percent: r.percent })), - date: res.trade_date ?? undefined, - refClose, - winner: res.winner_rate, - avg: res.weight_avg, - costLow: res.cost_5pct, - costHigh: res.cost_95pct, - error: res.error ?? undefined, - }; -} - -/** 筹码锚点(同花顺式):返回 参考日 + 锚定 bar 收盘价(获利/套牢的分界价)。 - * 日K=bar 当日;周/月/年K 的 bar ts 是周期首日,参考日=下一根 bar 首日的前一天 - * (最后一根=今天,后端再吸附到 <=参考日 的最近有数据交易日)。 - * idx 缺省=窗口最后一根(跟随当前可视周期);十字线跟随时传落点下标。 - * 注:翻页前插的更早 bar 不在 data.candles 里,十字线落到那些 bar 上时沿用上一截面。 */ -function chipsAnchor(idx?: number): { ref: string; close: number } | undefined { - const bars = data.value?.candles; - if (!bars?.length) return undefined; - const i = idx != null && idx >= 0 && idx < bars.length ? idx : bars.length - 1; - if (timeframe.value === '1d') return { ref: bars[i].ts.slice(0, 10), close: bars[i].close }; - const nextTs = i + 1 < bars.length ? new Date(bars[i + 1].ts).getTime() - 86400000 : Date.now(); - return { ref: fmtDashDate(Math.min(nextTs, Date.now())), close: bars[i].close }; -} - -async function loadChips(anchorIdx?: number) { - if (!showChips.value) return; - const anchor = chipsAnchor(anchorIdx); - if (!anchor) return; - const key = `${active.value}|${adjust.value}|${anchor.ref}`; - if (key === chipsKey) return; // 同股同口径同参考日不重复请求(后端有 Redis 缓存,前端也省一次往返) - chipsKey = key; - const token = ++chipsToken; - try { - const res = await getStockChips(active.value, { date: anchor.ref, adjust: adjust.value }); - if (token === chipsToken) chipsExt.value = toChipsExt(res, anchor.close); - } catch { - if (token === chipsToken) chipsExt.value = { rows: [], refClose: anchor.close, error: '筹码数据获取失败' }; - } -} - -/** K线数据就绪/切换(切股、复权、周期、日期跳转都经 load() 换 data):筹码重置到新窗口末根 */ -watch(data, () => { - chipsKey = ''; - chipHoverTs = null; - chipsExt.value = null; - void loadChips(); -}); - -/** 十字线跟随(防抖 350ms):落在哪根K线就取那个周期的截面;离开图表保留最后截面(通达信同款) */ -let chipHoverTimer: ReturnType | null = null; -let chipHoverTs: number | null = null; -function onCrosshairDate(ts: number) { - if (!showChips.value || chipHoverTs === ts) return; - chipHoverTs = ts; - if (chipHoverTimer) clearTimeout(chipHoverTimer); - chipHoverTimer = setTimeout(() => { - chipHoverTimer = null; - const bars = data.value?.candles; - if (!bars) return; - const idx = bars.findIndex((b) => new Date(b.ts).getTime() === ts); - if (idx >= 0) void loadChips(idx); - }, 350); -} - -function toggleChips() { - showChips.value = !showChips.value; - if (showChips.value) { chipsKey = ''; chipHoverTs = null; void loadChips(); } -} - function toggleSub(key: string) { const cur = subPanes.value; settings.setChartLayout({ @@ -664,7 +588,7 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0, class="rounded-md border px-2.5 py-1 text-[13px] transition-colors" :class="showBoll ? 'border-purple-500 bg-purple-500 text-white' : 'border-[#26272E] bg-[#101014] text-[#9BA3AE]'" title="主图叠加布林带" - @click="showBoll = !showBoll" + @click="toggleBoll" >BOLL - -