提交
This commit is contained in:
@@ -3,8 +3,8 @@
|
|||||||
GET /api/health 健康检查
|
GET /api/health 健康检查
|
||||||
GET /api/candles/{sym} 取 K 线(支持 1d/1w/1M/1y 周期,日线为基底聚合)
|
GET /api/candles/{sym} 取 K 线(支持 1d/1w/1M/1y 周期,日线为基底聚合)
|
||||||
GET /api/stocks 全市场股票列表(基本信息 + 最新行情 + 缓存条数)
|
GET /api/stocks 全市场股票列表(基本信息 + 最新行情 + 缓存条数)
|
||||||
GET /api/stock/{code}/chips 个股筹码峰(cyq_chips/cyq_perf,按复权口径换算)
|
|
||||||
GET /api/market/overview 主页大盘总览(A 股/港美指数 + 两市市值成交统计)
|
GET /api/market/overview 主页大盘总览(A 股/港美指数 + 两市市值成交统计)
|
||||||
|
GET /api/market/index-candles 上证指数全量 K 线(1d/1w/1M/1y 聚合)
|
||||||
POST /api/backtest 跑回测,返回 K线+指标+买卖点+净值+绩效
|
POST /api/backtest 跑回测,返回 K线+指标+买卖点+净值+绩效
|
||||||
POST /api/screener/run 智能选股:自然语言 -> 条件 -> 全市场筛选
|
POST /api/screener/run 智能选股:自然语言 -> 条件 -> 全市场筛选
|
||||||
POST /api/screener/sync 启动全市场数据同步(后台任务)
|
POST /api/screener/sync 启动全市场数据同步(后台任务)
|
||||||
@@ -18,6 +18,7 @@ import json
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from pydantic import TypeAdapter
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy import delete, func, select, text
|
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.events import EventEngineError, run_event_backtest
|
||||||
from .backtest.strategies import build_strategy
|
from .backtest.strategies import build_strategy
|
||||||
from .config import settings
|
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.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.market_overview import MarketOverviewError, fetch_overview
|
||||||
from .data.symbols import plain_code
|
from .data.symbols import plain_code
|
||||||
from .db import async_session, get_session
|
from .db import async_session, get_session
|
||||||
@@ -53,8 +55,6 @@ from .schemas import (
|
|||||||
BacktestRequest,
|
BacktestRequest,
|
||||||
BacktestResponse,
|
BacktestResponse,
|
||||||
CandleOut,
|
CandleOut,
|
||||||
ChipRowOut,
|
|
||||||
ChipsResponse,
|
|
||||||
EquityPoint,
|
EquityPoint,
|
||||||
EventBacktestRequest,
|
EventBacktestRequest,
|
||||||
EventBacktestResponse,
|
EventBacktestResponse,
|
||||||
@@ -133,6 +133,8 @@ def _rows_to_bars(rows) -> list[Bar]:
|
|||||||
_ADJUST_MODES = ("bfq", "qfq", "hfq")
|
_ADJUST_MODES = ("bfq", "qfq", "hfq")
|
||||||
# MA 全量集合(前端已改为本地计算 MA,后端始终返回此集合以保证缓存一致)
|
# MA 全量集合(前端已改为本地计算 MA,后端始终返回此集合以保证缓存一致)
|
||||||
_FULL_MA_SET = (5, 10, 20, 30, 60, 120, 250)
|
_FULL_MA_SET = (5, 10, 20, 30, 60, 120, 250)
|
||||||
|
# 指数 K 线支持的周期(日线基底聚合)
|
||||||
|
_INDEX_TIMEFRAMES = ("1d", "1w", "1M", "1y")
|
||||||
|
|
||||||
# 信息卡一条 SQL 拿全:stock_basic 基本信息 + 「优先与行情同日、缺则最新日」的 daily_snapshot
|
# 信息卡一条 SQL 拿全:stock_basic 基本信息 + 「优先与行情同日、缺则最新日」的 daily_snapshot
|
||||||
# (LATERAL 单条替换原两条查询,语义不变:target 为 NULL 时全按最新日兜底)
|
# (LATERAL 单条替换原两条查询,语义不变:target 为 NULL 时全按最新日兜底)
|
||||||
@@ -409,6 +411,32 @@ async def get_market_overview(session: AsyncSession = Depends(get_session)) -> M
|
|||||||
return MarketOverviewResponse(**data)
|
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)
|
@router.post("/backtest", response_model=BacktestResponse)
|
||||||
async def backtest(
|
async def backtest(
|
||||||
req: BacktestRequest,
|
req: BacktestRequest,
|
||||||
@@ -1112,84 +1140,3 @@ async def screener_preview(
|
|||||||
cache.local_set(f"pvj:{cache_key}", raw, ttl=120)
|
cache.local_set(f"pvj:{cache_key}", raw, ttl=120)
|
||||||
asyncio.create_task(cache.cache_set(f"pvj:{cache_key}", raw, ttl=600))
|
asyncio.create_task(cache.cache_set(f"pvj:{cache_key}", raw, ttl=600))
|
||||||
return Response(content=raw, media_type="application/json")
|
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")
|
|
||||||
|
|||||||
@@ -66,32 +66,3 @@ def fetch_daily(code: str, start: str = "20200101", end: str | None = None,
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return bars
|
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
|
|
||||||
|
|||||||
@@ -413,25 +413,3 @@ class MarketOverviewResponse(BaseModel):
|
|||||||
amount_history: list[AmountBarOut] = [] # 近 N 交易日两市成交额(旧 -> 新,末根可能盘中)
|
amount_history: list[AmountBarOut] = [] # 近 N 交易日两市成交额(旧 -> 新,末根可能盘中)
|
||||||
errors: list[str] = [] # 部分来源失败的说明(透明但不阻塞展示)
|
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 前无数据等)
|
|
||||||
|
|||||||
204
backend_run.log
204
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: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: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: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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
AdjustMode,
|
AdjustMode,
|
||||||
BacktestRequest,
|
BacktestRequest,
|
||||||
BacktestResponse,
|
BacktestResponse,
|
||||||
ChipsResponse,
|
Candle,
|
||||||
CurrentUser,
|
CurrentUser,
|
||||||
EventBacktestRequest,
|
EventBacktestRequest,
|
||||||
EventBacktestResponse,
|
EventBacktestResponse,
|
||||||
@@ -191,22 +191,6 @@ export async function getStockPreview(
|
|||||||
return (await res.json()) as PreviewResponse;
|
return (await res.json()) as PreviewResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 筹码分布(个股筹码峰) ----------
|
|
||||||
/** 获取某参考日的筹码截面:date 缺省取最新;周/月 K 线由调用方换算成周期末日传入,
|
|
||||||
* 后端吸附到 <=date 的最近有数据交易日。价格与成本均已按 adjust 口径换算。 */
|
|
||||||
export async function getStockChips(
|
|
||||||
tsCode: string,
|
|
||||||
opts: { date?: string; adjust?: AdjustMode } = {},
|
|
||||||
): Promise<ChipsResponse> {
|
|
||||||
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<MarketOverview> {
|
export async function getMarketOverview(): Promise<MarketOverview> {
|
||||||
const res = await apiFetch('/api/market/overview');
|
const res = await apiFetch('/api/market/overview');
|
||||||
@@ -214,6 +198,13 @@ export async function getMarketOverview(): Promise<MarketOverview> {
|
|||||||
return (await res.json()) as MarketOverview;
|
return (await res.json()) as MarketOverview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 上证指数全量 K 线(日线基底聚合到目标周期;收盘口径) */
|
||||||
|
export async function getIndexCandles(timeframe: Timeframe): Promise<Candle[]> {
|
||||||
|
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: {
|
export async function getStocks(params: {
|
||||||
search?: string;
|
search?: string;
|
||||||
market?: string;
|
market?: string;
|
||||||
|
|||||||
@@ -260,6 +260,9 @@ export interface ChartLayoutPrefs {
|
|||||||
subHeights: Record<string, number>; // 面板高度 px
|
subHeights: Record<string, number>; // 面板高度 px
|
||||||
timeframe?: Timeframe;
|
timeframe?: Timeframe;
|
||||||
tooltipFields?: TooltipField[]; // 浮层显示的指标(顺序即行序;空数组=仅日期头)
|
tooltipFields?: TooltipField[]; // 浮层显示的指标(顺序即行序;空数组=仅日期头)
|
||||||
|
indexTimeframe?: Timeframe; // 首页上证指数 K 线周期
|
||||||
|
showBoll?: boolean; // 主图叠加 BOLL
|
||||||
|
showZhixing?: boolean; // 主图叠加知行指标(趋势线+多空线)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScreenerQueryItem {
|
export interface ScreenerQueryItem {
|
||||||
@@ -357,29 +360,6 @@ export interface EventBacktestResponse {
|
|||||||
total: number;
|
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 {
|
export interface IndexQuote {
|
||||||
code: string; // 000001.SH / HKTECH / DJI
|
code: string; // 000001.SH / HKTECH / DJI
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
threeWaves, triangle, xabcd,
|
threeWaves, triangle, xabcd,
|
||||||
} from '@klinecharts/extension';
|
} from '@klinecharts/extension';
|
||||||
import { useSettingsStore, TOOLTIP_FIELDS, DEFAULT_TOOLTIP_FIELDS } from '@/stores/settings';
|
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';
|
import type { Candle, TooltipField } from '@/api/types';
|
||||||
|
|
||||||
// 通达信导入的自定义主图指标注册表;知行序列走 PV 管道按时间戳取预计算值。
|
// 通达信导入的自定义主图指标注册表;知行序列走 PV 管道按时间戳取预计算值。
|
||||||
@@ -95,10 +96,6 @@ const props = defineProps<{
|
|||||||
showZhixing: boolean;
|
showZhixing: boolean;
|
||||||
/** 知行指标图例行的板块文本(通达信 HYBLOCK+DYBLOCK 的近似,行业+地域) */
|
/** 知行指标图例行的板块文本(通达信 HYBLOCK+DYBLOCK 的近似,行业+地域) */
|
||||||
zhixingBlocks?: string;
|
zhixingBlocks?: string;
|
||||||
/** 主图是否叠加筹码峰(cyq 截面横向直方图,纯 draw 绘制) */
|
|
||||||
showChips: boolean;
|
|
||||||
/** 筹码峰截面数据(父组件按 当前窗口末根/十字线 所在周期取数;null=暂无) */
|
|
||||||
chips?: ChipsExt | null;
|
|
||||||
/** K线周期标签(仅用于 MA 指标名缓存 key) */
|
/** K线周期标签(仅用于 MA 指标名缓存 key) */
|
||||||
timeframe: string;
|
timeframe: string;
|
||||||
/** 浮层显示的指标(可选;缺省=目录全开,空数组=仅日期头) */
|
/** 浮层显示的指标(可选;缺省=目录全开,空数组=仅日期头) */
|
||||||
@@ -114,8 +111,6 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
/** 日期跳转锚点在本次数据窗口里找不到(早于上市/晚于最后一根):请父组件回退到最新行情并提示 */
|
/** 日期跳转锚点在本次数据窗口里找不到(早于上市/晚于最后一根):请父组件回退到最新行情并提示 */
|
||||||
(e: 'centerMiss', ts: number): void;
|
(e: 'centerMiss', ts: number): void;
|
||||||
/** 十字线落在某根K线上(开启筹码峰时逐根上报,父组件防抖后取该周期筹码截面) */
|
|
||||||
(e: 'crosshairDate', ts: number): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// A股语义色(黑底高对比);UP/DOWN 跟随设置中的涨跌配色
|
// A股语义色(黑底高对比);UP/DOWN 跟随设置中的涨跌配色
|
||||||
@@ -327,35 +322,9 @@ function maybePrefetch(myEpoch: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function darkStyles() {
|
function darkStyles() {
|
||||||
return {
|
// 共享暗色主题(chartStyles.ts);UP/DOWN 在 build() 里随 settings 刷新,
|
||||||
grid: { horizontal: { color: '#1C1E24' }, vertical: { color: '#1C1E24' } },
|
// 故切「绿涨红跌」重建后同样生效
|
||||||
// 内建 VOL 等指标柱的涨跌色缺省是库默认绿涨红跌,与全站语义相反;
|
return sharedDarkStyles(UP, DOWN);
|
||||||
// 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)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 副图默认高度
|
// 副图默认高度
|
||||||
@@ -437,8 +406,6 @@ function bindCrosshair(ch: Chart) {
|
|||||||
const idx = ts != null ? idxOfTs(ts) : -1; // 全量数组与指标序列按索引对齐
|
const idx = ts != null ? idxOfTs(ts) : -1; // 全量数组与指标序列按索引对齐
|
||||||
if (idx < 0) { hover.value = null; return; }
|
if (idx < 0) { hover.value = null; return; }
|
||||||
const k = allData[idx];
|
const k = allData[idx];
|
||||||
// 筹码峰跟随:十字线逐根上报(父组件防抖取截面);不开启时不上报,避免无谓事件
|
|
||||||
if (props.showChips) emit('crosshairDate', k.timestamp);
|
|
||||||
const prev = idx > 0 ? allData[idx - 1] : null;
|
const prev = idx > 0 ? allData[idx - 1] : null;
|
||||||
const diff = prev ? k.close - prev.close : null;
|
const diff = prev ? k.close - prev.close : null;
|
||||||
const chg = prev ? (diff! / prev.close) * 100 : 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 };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 筹码峰指标同步(不重建图表:createIndicator 不支持原地改 extendData,
|
// 右侧固定留白(价格轴与末根K之间的呼吸空间;scrollToRealTime 以它为锚点)
|
||||||
// 先删后建即可;数据/开关变化走这里,保留滚动位置与用户画线) ----------
|
|
||||||
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 且改变滚动边缘行为。
|
|
||||||
const BASE_RIGHT_PX = 28;
|
const BASE_RIGHT_PX = 28;
|
||||||
let syncingRightOffset = false; // 重入守卫:set 会同步派发 onVisibleRangeChange
|
|
||||||
let scrollReanchorTimer: ReturnType<typeof setTimeout> | 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() {
|
function build() {
|
||||||
if (!container.value || props.candles.length === 0) return;
|
if (!container.value || props.candles.length === 0) return;
|
||||||
@@ -819,7 +724,6 @@ function build() {
|
|||||||
calcParams: DKX_PERIODS, extendData: { blocks: props.zhixingBlocks },
|
calcParams: DKX_PERIODS, extendData: { blocks: props.zhixingBlocks },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (props.showChips) syncChipsIndicator(); // 筹码峰(数据未到时 draw 直接空转,到货后由 watch 补建)
|
|
||||||
|
|
||||||
// 副图按用户顺序创建,并设置用户高度;主图吃剩余高度。
|
// 副图按用户顺序创建,并设置用户高度;主图吃剩余高度。
|
||||||
// minHeight 交给库在分隔条拖拽时强制执行(与 subH 的 40px 下限一致)
|
// minHeight 交给库在分隔条拖拽时强制执行(与 subH 的 40px 下限一致)
|
||||||
@@ -852,23 +756,8 @@ function build() {
|
|||||||
const from = (payload as { data?: { from?: unknown } }).data?.from;
|
const from = (payload as { data?: { from?: unknown } }).data?.from;
|
||||||
if (typeof from === 'number' && from < 200) maybePrefetch(myEpoch);
|
if (typeof from === 'number' && from < 200) maybePrefetch(myEpoch);
|
||||||
});
|
});
|
||||||
// 右侧留白:开筹码时按竖列宽度预留(scrollToRealTime 以它为锚点,先后顺序不能换)
|
// 右侧留白(scrollToRealTime 以它为锚点,先后顺序不能换)
|
||||||
ch.setOffsetRightDistance(props.showChips ? targetRightPx() : BASE_RIGHT_PX);
|
ch.setOffsetRightDistance(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);
|
|
||||||
});
|
|
||||||
ch.scrollToRealTime();
|
ch.scrollToRealTime();
|
||||||
// 日期跳转:build 尾部的 scrollToRealTime 会把视口重置到最新一根,居中必须放在它之后
|
// 日期跳转:build 尾部的 scrollToRealTime 会把视口重置到最新一根,居中必须放在它之后
|
||||||
//(init 数据在 setPeriod 时已同步落入图表,这里可直接定位)。
|
//(init 数据在 setPeriod 时已同步落入图表,这里可直接定位)。
|
||||||
@@ -881,8 +770,6 @@ function build() {
|
|||||||
|
|
||||||
function teardown() {
|
function teardown() {
|
||||||
if (subHTimer) { clearTimeout(subHTimer); subHTimer = null; }
|
if (subHTimer) { clearTimeout(subHTimer); subHTimer = null; }
|
||||||
if (scrollReanchorTimer) { clearTimeout(scrollReanchorTimer); scrollReanchorTimer = null; }
|
|
||||||
managedRightPx = 0;
|
|
||||||
if (container.value) dispose(container.value);
|
if (container.value) dispose(container.value);
|
||||||
chart = null;
|
chart = null;
|
||||||
hover.value = null;
|
hover.value = null;
|
||||||
@@ -893,9 +780,6 @@ function teardown() {
|
|||||||
onMounted(build);
|
onMounted(build);
|
||||||
onBeforeUnmount(teardown);
|
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.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 });
|
watch(() => props.tradeMarkers, renderTradeMarkers, { deep: true });
|
||||||
// 涨跌配色切换:重建图表以应用新颜色
|
// 涨跌配色切换:重建图表以应用新颜色
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getMarketOverview } from '@/api/client';
|
|||||||
import type { IndexQuote, MarketOverview as Overview } from '@/api/types';
|
import type { IndexQuote, MarketOverview as Overview } from '@/api/types';
|
||||||
|
|
||||||
import AmountHistoryChart from '@/components/AmountHistoryChart.vue';
|
import AmountHistoryChart from '@/components/AmountHistoryChart.vue';
|
||||||
|
import IndexKLine from '@/components/IndexKLine.vue';
|
||||||
|
|
||||||
// 主页大盘总览:A 股 + 港美指数最近收盘(收盘口径,标注交易日),沪深两市市值/成交统计。
|
// 主页大盘总览:A 股 + 港美指数最近收盘(收盘口径,标注交易日),沪深两市市值/成交统计。
|
||||||
// 数据为 EOD 口径,进页面拉一次 + 手动刷新即可,不做轮询。
|
// 数据为 EOD 口径,进页面拉一次 + 手动刷新即可,不做轮询。
|
||||||
@@ -226,6 +227,9 @@ function hoverText(it: IndexQuote): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 上证指数 K 线(日/周/月/年,周期随用户偏好持久化) -->
|
||||||
|
<IndexKLine class="mt-4" />
|
||||||
|
|
||||||
<!-- 两市统计:daily_info 沪 SH_MARKET(主板A+科创+B) + 深 SZ_MARKET 汇总 -->
|
<!-- 两市统计:daily_info 沪 SH_MARKET(主板A+科创+B) + 深 SZ_MARKET 汇总 -->
|
||||||
<div
|
<div
|
||||||
v-if="overview.stats"
|
v-if="overview.stats"
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import {
|
import {
|
||||||
addWatchlist, clearTrades, getStockChips, getStockPreview, getTrades, getWatchlist as getWatchlistApi,
|
addWatchlist, clearTrades, getStockPreview, getTrades, getWatchlist as getWatchlistApi,
|
||||||
importTrades, removeWatchlist,
|
importTrades, removeWatchlist,
|
||||||
} from '@/api/client';
|
} from '@/api/client';
|
||||||
import type {
|
import type {
|
||||||
ChartLayoutPrefs, ChipsResponse, PreviewResponse, ScreenerItemOut, Timeframe, TooltipField,
|
ChartLayoutPrefs, PreviewResponse, ScreenerItemOut, Timeframe, TooltipField,
|
||||||
TradesImportResponse, UserTrade,
|
TradesImportResponse, UserTrade,
|
||||||
} from '@/api/types';
|
} from '@/api/types';
|
||||||
import { useSettingsStore, DEFAULT_TOOLTIP_FIELDS, TOOLTIP_FIELDS, type PriceAdjust } from '@/stores/settings';
|
import { useSettingsStore, DEFAULT_TOOLTIP_FIELDS, TOOLTIP_FIELDS, type PriceAdjust } from '@/stores/settings';
|
||||||
import type { ChipsExt } from '@/indicators';
|
|
||||||
import DetailKLine from './DetailKLine.vue';
|
import DetailKLine from './DetailKLine.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -67,8 +66,11 @@ const subPanes = computed<string[]>(() => layout.value.subPanes);
|
|||||||
const maPeriods = computed<number[]>(() => layout.value.maPeriods);
|
const maPeriods = computed<number[]>(() => layout.value.maPeriods);
|
||||||
const subHeights = computed(() => layout.value.subHeights);
|
const subHeights = computed(() => layout.value.subHeights);
|
||||||
const tooltipFields = computed<TooltipField[]>(() => layout.value.tooltipFields ?? DEFAULT_TOOLTIP_FIELDS);
|
const tooltipFields = computed<TooltipField[]>(() => layout.value.tooltipFields ?? DEFAULT_TOOLTIP_FIELDS);
|
||||||
const showBoll = ref(false);
|
// 主图可选叠加(BOLL / 知行):随用户偏好持久化,跨会话/跨设备记忆开关
|
||||||
const showZhixing = ref(false);
|
const showBoll = computed<boolean>(() => layout.value.showBoll === true);
|
||||||
|
const showZhixing = computed<boolean>(() => layout.value.showZhixing === true);
|
||||||
|
function toggleBoll() { settings.setChartLayout({ showBoll: !showBoll.value }); }
|
||||||
|
function toggleZhixing() { settings.setChartLayout({ showZhixing: !showZhixing.value }); }
|
||||||
// 知行指标图例行的板块文本(通达信 HYBLOCK+' '+DYBLOCK 的近似;GNBLOCK 概念板块无数据源)
|
// 知行指标图例行的板块文本(通达信 HYBLOCK+' '+DYBLOCK 的近似;GNBLOCK 概念板块无数据源)
|
||||||
const zhixingBlocks = computed(() => {
|
const zhixingBlocks = computed(() => {
|
||||||
const info = data.value?.info;
|
const info = data.value?.info;
|
||||||
@@ -76,84 +78,6 @@ const zhixingBlocks = computed(() => {
|
|||||||
return [info.industry, info.area].filter(Boolean).join(' ') || undefined;
|
return [info.industry, info.area].filter(Boolean).join(' ') || undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- 筹码峰(cyq 截面,跟随 日/周/月K 的当前周期 + 十字线) ----------
|
|
||||||
const showChips = ref(false);
|
|
||||||
const chipsExt = ref<ChipsExt | null>(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<typeof setTimeout> | 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) {
|
function toggleSub(key: string) {
|
||||||
const cur = subPanes.value;
|
const cur = subPanes.value;
|
||||||
settings.setChartLayout({
|
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="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]'"
|
:class="showBoll ? 'border-purple-500 bg-purple-500 text-white' : 'border-[#26272E] bg-[#101014] text-[#9BA3AE]'"
|
||||||
title="主图叠加布林带"
|
title="主图叠加布林带"
|
||||||
@click="showBoll = !showBoll"
|
@click="toggleBoll"
|
||||||
>BOLL</button>
|
>BOLL</button>
|
||||||
<!-- 知行:通达信导入的自定义主图指标(趋势线+多空线) -->
|
<!-- 知行:通达信导入的自定义主图指标(趋势线+多空线) -->
|
||||||
<button
|
<button
|
||||||
@@ -672,16 +596,8 @@ 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="rounded-md border px-2.5 py-1 text-[13px] transition-colors"
|
||||||
:class="showZhixing ? 'border-[#E61851] bg-[#E61851] text-white' : 'border-[#26272E] bg-[#101014] text-[#9BA3AE]'"
|
:class="showZhixing ? 'border-[#E61851] bg-[#E61851] text-white' : 'border-[#26272E] bg-[#101014] text-[#9BA3AE]'"
|
||||||
title="知行指标:短期趋势线 EMA(EMA(C,10),10) + 多空线 (MA14+MA28+MA57+MA114)/4"
|
title="知行指标:短期趋势线 EMA(EMA(C,10),10) + 多空线 (MA14+MA28+MA57+MA114)/4"
|
||||||
@click="showZhixing = !showZhixing"
|
@click="toggleZhixing"
|
||||||
>知行</button>
|
>知行</button>
|
||||||
<!-- 筹码峰:右侧横向线状直方图(选中K线下方获利盘红/上方套牢盘蓝+平均成本虚线) -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="rounded-md border px-2.5 py-1 text-[13px] transition-colors"
|
|
||||||
:class="showChips ? 'border-cyan-400 bg-cyan-400 text-black' : 'border-[#26272E] bg-[#101014] text-[#9BA3AE]'"
|
|
||||||
title="筹码峰:各价位持仓占比(选中K线分界:下方获利盘红/上方套牢盘蓝),随日/周/月K与十字线切换截面"
|
|
||||||
@click="toggleChips"
|
|
||||||
>筹码峰</button>
|
|
||||||
<!-- 实盘交易点:交割单导入的买卖标记 -->
|
<!-- 实盘交易点:交割单导入的买卖标记 -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -812,9 +728,6 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
|
|||||||
:show-boll="showBoll"
|
:show-boll="showBoll"
|
||||||
:show-zhixing="showZhixing"
|
:show-zhixing="showZhixing"
|
||||||
:zhixing-blocks="zhixingBlocks"
|
:zhixing-blocks="zhixingBlocks"
|
||||||
:show-chips="showChips"
|
|
||||||
:chips="chipsExt"
|
|
||||||
@crosshair-date="onCrosshairDate"
|
|
||||||
:timeframe="timeframe"
|
:timeframe="timeframe"
|
||||||
:tooltip-fields="tooltipFields"
|
:tooltip-fields="tooltipFields"
|
||||||
:center-ts="jumpTs"
|
:center-ts="jumpTs"
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
// 个股筹码峰(Tushare cyq_chips / cyq_perf,同花顺火焰山式右侧专用竖列)
|
|
||||||
//
|
|
||||||
// 数据不在图表窗口上计算,而是一次性截面(某交易日各价位筹码占比 + 获利比例/平均成本),
|
|
||||||
// 经 extendData 注入、纯 draw 绘制:
|
|
||||||
// - 主图绘图区右缘、价格轴左侧一条【不透明底色专用竖列】,K线不透出(同花顺同款)
|
|
||||||
// - 每个价位一根横条,在列内从右缘向左伸展,宽度 ∝ 占比/最大占比(火焰山形态)
|
|
||||||
// - 分界价 = 选中K线的收盘价(refClose):下方为获利盘(红)、上方为套牢盘(蓝)
|
|
||||||
// - 平均成本画黄色虚线、末根收盘画现价细实线;列内顶部小字:截面日/获利比例/平均成本/90%成本
|
|
||||||
// DetailKLine 依据 CHIPS_COL_PX 在开启筹码时加大右侧留白,保证最新K线不被竖列盖住。
|
|
||||||
// figures 为空、calc 返回空行:指标不产生序列值,也不影响主图价格刻度(筹码是覆盖层)。
|
|
||||||
import type { IndicatorTemplate } from 'klinecharts';
|
|
||||||
|
|
||||||
export const CHIPS_NAME = 'pv-chips';
|
|
||||||
|
|
||||||
/** 火焰山竖列基准宽度(px):DetailKLine 据此预留右侧留白,draw 据此画列 */
|
|
||||||
export const CHIPS_COL_PX = 120;
|
|
||||||
|
|
||||||
/** 经 extendData 注入的筹码截面(父组件按 当前窗口末根/十字线 所在周期取数) */
|
|
||||||
export interface ChipsExt {
|
|
||||||
rows: { price: number; percent: number }[]; // percent 为 0~1 占比
|
|
||||||
date?: string; // YYYYMMDD 截面日
|
|
||||||
refClose?: number | null; // 获利/套牢分界价:选中K线收盘(缺省时 draw 退回图表末根收盘)
|
|
||||||
winner?: number | null; // 获利比例 %
|
|
||||||
avg?: number | null; // 平均成本
|
|
||||||
costLow?: number | null; // 90% 成本下沿(cost_5pct)
|
|
||||||
costHigh?: number | null; // 90% 成本上沿(cost_95pct)
|
|
||||||
error?: string | null; // 无数据/出错的提示文案(仍要画出来,用户才知道为何空)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同花顺筹码峰配色:获利盘红、套牢盘蓝
|
|
||||||
const UP = '#FE354B';
|
|
||||||
const DOWN = '#2F7BFF';
|
|
||||||
const AVG_COLOR = '#F5C518';
|
|
||||||
const LABEL_COLOR = '#9AA0AA';
|
|
||||||
const VALUE_COLOR = '#E8EAED';
|
|
||||||
|
|
||||||
// 竖列几何:窄图下限 56px,最多占绘图区宽度 35%(实际宽度取两者与基准的钳制值)
|
|
||||||
const COL_MIN_PX = 56;
|
|
||||||
const COL_MAX_RATIO = 0.35;
|
|
||||||
/** 列内左右内边距(横条右缘、统计文字与列缘的呼吸) */
|
|
||||||
const COL_PAD = 5;
|
|
||||||
/** 不透明底色(图表容器纯黑 bg-black,取应用面板色);左缘 1px 分隔线与 y 轴线同色 */
|
|
||||||
const COL_BG = '#101014';
|
|
||||||
const COL_BORDER = '#2A2D34';
|
|
||||||
/** 实心条透明度:不透明列内 0.85 足够“实心”,与底色留一丝层次 */
|
|
||||||
const BAR_ALPHA = 0.85;
|
|
||||||
/** 横条高度上限(px):极度放大时单价位占比条不至于铺满大半个竖列 */
|
|
||||||
const BAR_HEIGHT_CAP = 24;
|
|
||||||
/** 统计文字起始 y(画线工具栏是 HTML 层,占顶部 ~36px,文字块从 44px 起)/ 行高 */
|
|
||||||
const HEADER_TOP = 44;
|
|
||||||
const LINE_H = 13;
|
|
||||||
|
|
||||||
function fmtDate(d?: string): string {
|
|
||||||
return d && d.length === 8 ? `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}` : '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 右对齐数值超宽时截断加省略号(90%成本 “10.50~14.20” 这类长串的兜底) */
|
|
||||||
function fitText(ctx: CanvasRenderingContext2D, text: string, maxW: number): string {
|
|
||||||
if (ctx.measureText(text).width <= maxW) return text;
|
|
||||||
let s = text;
|
|
||||||
while (s.length > 1 && ctx.measureText(`${s}…`).width > maxW) s = s.slice(0, -1);
|
|
||||||
return `${s}…`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const chipsIndicator: IndicatorTemplate<Record<string, never>, unknown, ChipsExt> = {
|
|
||||||
name: CHIPS_NAME,
|
|
||||||
shortName: '筹码',
|
|
||||||
figures: [], // 纯 draw 绘制,无序列图形
|
|
||||||
calc: (d) => d.map(() => ({})),
|
|
||||||
// 返回 true(isCover):跳过框架 figure 渲染(本来也没有 figures),全部自己画
|
|
||||||
draw: ({ ctx, chart, indicator, bounding, yAxis }) => {
|
|
||||||
const ext = indicator.extendData;
|
|
||||||
if (!ext) return true;
|
|
||||||
|
|
||||||
// 竖列几何:bounding.right 即 y 轴左缘,列贴着价格轴(预留逻辑见 DetailKLine 的 syncRightOffset)
|
|
||||||
const colW = Math.max(COL_MIN_PX, Math.min(CHIPS_COL_PX, Math.floor(bounding.width * COL_MAX_RATIO)));
|
|
||||||
const colRight = bounding.right;
|
|
||||||
const colLeft = colRight - colW;
|
|
||||||
|
|
||||||
// 无数据/出错:不画底色列(空面板不遮图),错误文案画在原右上角位置
|
|
||||||
if (!ext.rows.length) {
|
|
||||||
if (ext.error) {
|
|
||||||
ctx.font = '11px sans-serif';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
ctx.textAlign = 'right';
|
|
||||||
ctx.fillStyle = LABEL_COLOR;
|
|
||||||
ctx.fillText(`筹码 ${ext.error}`, bounding.right - 4, bounding.top + HEADER_TOP);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 不透明底色列 + 左缘 1px 分隔线(先画,盖住网格/K线/MA/最新价虚线的右端)
|
|
||||||
ctx.fillStyle = COL_BG;
|
|
||||||
ctx.fillRect(colLeft, bounding.top, colW, bounding.height);
|
|
||||||
ctx.fillStyle = COL_BORDER;
|
|
||||||
ctx.fillRect(colLeft, bounding.top, 1, bounding.height);
|
|
||||||
|
|
||||||
const list = chart.getDataList();
|
|
||||||
// 分界价:选中K线的收盘价(父组件随截面注入;缺省退回图表末根收盘),
|
|
||||||
// <=分界价为获利盘(红)、>分界价为套牢盘(蓝)——同花顺同款
|
|
||||||
const split = ext.refClose ?? (list.length ? list[list.length - 1].close : undefined);
|
|
||||||
|
|
||||||
// 2. 筹码横条:列右缘向左伸展,宽度 ∝ 占比/最大占比
|
|
||||||
// 价位升序,便于求相邻像素间距(直方图的“条高”)
|
|
||||||
const rows = [...ext.rows].sort((a, b) => a.price - b.price);
|
|
||||||
const maxP = rows.reduce((m, r) => Math.max(m, r.percent), 0);
|
|
||||||
if (maxP > 0) {
|
|
||||||
// 相邻价位的中位像素间距 → 条高(连续火焰形态;缩到极小时并成实心轮廓)
|
|
||||||
const pitches: number[] = [];
|
|
||||||
for (let i = 1; i < rows.length; i++) {
|
|
||||||
const p = Math.abs(yAxis.convertToPixel(rows[i].price) - yAxis.convertToPixel(rows[i - 1].price));
|
|
||||||
if (p > 0) pitches.push(p);
|
|
||||||
}
|
|
||||||
pitches.sort((a, b) => a - b);
|
|
||||||
const pitch = pitches.length ? pitches[pitches.length >> 1] : 4;
|
|
||||||
const barH = Math.min(BAR_HEIGHT_CAP, Math.max(1, pitch * 0.92));
|
|
||||||
const maxBarW = colW - COL_PAD * 2;
|
|
||||||
for (const r of rows) {
|
|
||||||
const y = yAxis.convertToPixel(r.price);
|
|
||||||
if (y < bounding.top - barH || y > bounding.bottom + barH) continue; // 视口外跳过
|
|
||||||
const w = Math.max(1, (r.percent / maxP) * maxBarW);
|
|
||||||
ctx.fillStyle = split == null ? 'rgba(154,160,170,0.85)'
|
|
||||||
: r.price <= split ? `rgba(254,53,75,${BAR_ALPHA})` : `rgba(47,123,255,${BAR_ALPHA})`;
|
|
||||||
ctx.fillRect(colRight - COL_PAD - w, y - barH / 2, w, barH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 平均成本虚线(横贯竖列)
|
|
||||||
if (ext.avg != null) {
|
|
||||||
const y = yAxis.convertToPixel(ext.avg);
|
|
||||||
if (y >= bounding.top && y <= bounding.bottom) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.strokeStyle = AVG_COLOR;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.setLineDash([4, 3]);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(colLeft + 1, Math.round(y) + 0.5);
|
|
||||||
ctx.lineTo(colRight, Math.round(y) + 0.5);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 现价细实线(末根收盘,红/蓝随涨跌,与 y 轴最新价标签同源同色;
|
|
||||||
// 分界价本身已是横条红蓝交界,不再重复画线)
|
|
||||||
if (list.length >= 2) {
|
|
||||||
const last = list[list.length - 1];
|
|
||||||
const y = yAxis.convertToPixel(last.close);
|
|
||||||
if (y >= bounding.top && y <= bounding.bottom) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.strokeStyle = last.close >= list[list.length - 2].close ? UP : DOWN;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(colLeft + 1, Math.round(y) + 0.5);
|
|
||||||
ctx.lineTo(colRight, Math.round(y) + 0.5);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 统计小字块:列内顶部,label 左对齐 + value 右对齐,超宽截断
|
|
||||||
//(列宽有限,两列布局比原先的单行右对齐更能放下「90%成本 10.50~14.20」这类长串)
|
|
||||||
const w = ext.winner;
|
|
||||||
const lines: [string, string, string][] = [
|
|
||||||
['筹码', fmtDate(ext.date), VALUE_COLOR],
|
|
||||||
['获利比例', w == null ? '—' : `${w.toFixed(2)}%`, w == null ? VALUE_COLOR : w >= 50 ? UP : DOWN],
|
|
||||||
['平均成本', ext.avg == null ? '—' : ext.avg.toFixed(2), AVG_COLOR],
|
|
||||||
['90%成本', ext.costLow == null || ext.costHigh == null ? '—'
|
|
||||||
: `${ext.costLow.toFixed(2)}~${ext.costHigh.toFixed(2)}`, VALUE_COLOR],
|
|
||||||
];
|
|
||||||
ctx.font = '10px sans-serif';
|
|
||||||
ctx.textBaseline = 'top';
|
|
||||||
const labelX = colLeft + COL_PAD;
|
|
||||||
const valueX = colRight - COL_PAD;
|
|
||||||
const valueMaxW = colW - COL_PAD * 2 - 36; // 36 ≈ 左侧标签区预留宽
|
|
||||||
let ty = bounding.top + HEADER_TOP;
|
|
||||||
for (const [label, value, color] of lines) {
|
|
||||||
ctx.textAlign = 'left';
|
|
||||||
ctx.fillStyle = LABEL_COLOR;
|
|
||||||
ctx.fillText(label, labelX, ty);
|
|
||||||
ctx.textAlign = 'right';
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.fillText(fitText(ctx, value, valueMaxW), valueX, ty);
|
|
||||||
ty += LINE_H;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -2,22 +2,14 @@
|
|||||||
// 依赖预计算序列的指标(知行)不在此处闭包数据,而是导出模板工厂、由 DetailKLine 注入
|
// 依赖预计算序列的指标(知行)不在此处闭包数据,而是导出模板工厂、由 DetailKLine 注入
|
||||||
// 按时间戳取值的访问器(g(),与 pv-boll 等后端序列同款管道)。注意 g 及其闭包的
|
// 按时间戳取值的访问器(g(),与 pv-boll 等后端序列同款管道)。注意 g 及其闭包的
|
||||||
// PV/allData 是组件实例级的(script setup 每次挂载重建),知行模板必须随挂载重注册
|
// PV/allData 是组件实例级的(script setup 每次挂载重建),知行模板必须随挂载重注册
|
||||||
// 换绑到当前实例——否则永远读首挂实例的数据,切股后画的是第一只股票的线;
|
// 换绑到当前实例——否则永远读首挂实例的数据,切股后画的是第一只股票的线。
|
||||||
// 筹码峰等“截面数据 + 纯 draw”指标不闭包实例数据(经 extendData 注入),注册一次即可。
|
|
||||||
import { registerIndicator } from 'klinecharts';
|
import { registerIndicator } from 'klinecharts';
|
||||||
import { chipsIndicator } from './chips';
|
|
||||||
import { makeZhixingIndicator, ZHIXING_NAME, type ZhixingGetter } from './zhixing';
|
import { makeZhixingIndicator, ZHIXING_NAME, type ZhixingGetter } from './zhixing';
|
||||||
|
|
||||||
let staticRegistered = false;
|
|
||||||
|
|
||||||
export function registerCustomIndicators(get: ZhixingGetter) {
|
export function registerCustomIndicators(get: ZhixingGetter) {
|
||||||
// 知行:同名重注册即替换,把 calc 换绑到本次挂载实例的 g(与 DetailKLine 内
|
// 知行:同名重注册即替换,把 calc 换绑到本次挂载实例的 g(与 DetailKLine 内
|
||||||
// pv-boll/pv-macd 每挂必注册同款;幂等闸曾导致知行只认第一只股票)
|
// pv-boll/pv-macd 每挂必注册同款;幂等闸曾导致知行只认第一只股票)
|
||||||
registerIndicator(makeZhixingIndicator(get));
|
registerIndicator(makeZhixingIndicator(get));
|
||||||
if (staticRegistered) return;
|
|
||||||
staticRegistered = true;
|
|
||||||
registerIndicator(chipsIndicator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { CHIPS_NAME, CHIPS_COL_PX, type ChipsExt } from './chips';
|
|
||||||
export { ZHIXING_NAME, ZHIXING_TREND_COLOR, DKX_PERIODS, computeZhixingSeries } from './zhixing';
|
export { ZHIXING_NAME, ZHIXING_TREND_COLOR, DKX_PERIODS, computeZhixingSeries } from './zhixing';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { computed, ref, watchEffect } from 'vue';
|
import { computed, ref, watchEffect } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { getPreferences, putPreferences } from '@/api/client';
|
import { getPreferences, putPreferences } from '@/api/client';
|
||||||
import type { ChartLayoutPrefs, TooltipField } from '@/api/types';
|
import type { ChartLayoutPrefs, Timeframe, TooltipField } from '@/api/types';
|
||||||
|
|
||||||
export type PriceTone = 'red-up' | 'green-up';
|
export type PriceTone = 'red-up' | 'green-up';
|
||||||
export type PriceAdjust = 'bfq' | 'qfq' | 'hfq';
|
export type PriceAdjust = 'bfq' | 'qfq' | 'hfq';
|
||||||
@@ -37,6 +37,12 @@ function normTipFields(v: unknown): TooltipField[] | undefined {
|
|||||||
return v.filter((k): k is TooltipField => typeof k === 'string' && TIP_VALID.has(k as TooltipField));
|
return v.filter((k): k is TooltipField => typeof k === 'string' && TIP_VALID.has(k as TooltipField));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TF_VALID = new Set(['1d', '1w', '1M', '1y']);
|
||||||
|
/** K线周期合法化;非法/缺失返回 undefined(调用方回退默认值) */
|
||||||
|
function normTimeframe(v: unknown): Timeframe | undefined {
|
||||||
|
return typeof v === 'string' && TF_VALID.has(v) ? (v as Timeframe) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function loadTone(): PriceTone {
|
function loadTone(): PriceTone {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
@@ -63,6 +69,9 @@ function loadLayout(): ChartLayoutPrefs {
|
|||||||
...v,
|
...v,
|
||||||
subHeights: v.subHeights ?? {},
|
subHeights: v.subHeights ?? {},
|
||||||
tooltipFields: normTipFields(v.tooltipFields) ?? DEFAULT_TOOLTIP_FIELDS,
|
tooltipFields: normTipFields(v.tooltipFields) ?? DEFAULT_TOOLTIP_FIELDS,
|
||||||
|
showBoll: v.showBoll === true,
|
||||||
|
showZhixing: v.showZhixing === true,
|
||||||
|
indexTimeframe: normTimeframe(v.indexTimeframe),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +174,14 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
if (Array.isArray(v.maPeriods) && Array.isArray(v.subPanes)) {
|
if (Array.isArray(v.maPeriods) && Array.isArray(v.subPanes)) {
|
||||||
// 服务端存档早于浮层配置(无 tooltipFields)时保留本地选择,避免旧档案冲掉
|
// 服务端存档早于浮层配置(无 tooltipFields)时保留本地选择,避免旧档案冲掉
|
||||||
const tip = normTipFields(v.tooltipFields) ?? chartLayout.value.tooltipFields;
|
const tip = normTipFields(v.tooltipFields) ?? chartLayout.value.tooltipFields;
|
||||||
chartLayout.value = { ...v, subHeights: v.subHeights ?? {}, tooltipFields: tip };
|
chartLayout.value = {
|
||||||
|
...v,
|
||||||
|
subHeights: v.subHeights ?? {},
|
||||||
|
tooltipFields: tip,
|
||||||
|
showBoll: v.showBoll === true,
|
||||||
|
showZhixing: v.showZhixing === true,
|
||||||
|
indexTimeframe: normTimeframe(v.indexTimeframe),
|
||||||
|
};
|
||||||
saveLocal(LAYOUT_KEY, JSON.stringify(chartLayout.value));
|
saveLocal(LAYOUT_KEY, JSON.stringify(chartLayout.value));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user