提交
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,3 +48,6 @@ Thumbs.db
|
|||||||
|
|
||||||
# ---------- Claude ----------
|
# ---------- Claude ----------
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# ---------- Logs ----------
|
||||||
|
*.log
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -9,21 +9,24 @@
|
|||||||
|
|
||||||
## 快速启动
|
## 快速启动
|
||||||
|
|
||||||
两个终端分别启动后端与前端:
|
两个终端分别启动后端与前端(命令均从仓库根 `stock/` 执行):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 终端 1 —— 后端 API(http://localhost:8000)
|
# 终端 1 —— 后端 API(http://localhost:8000)
|
||||||
cd backend
|
bash backend/restart_backend.sh
|
||||||
env -u SSLKEYLOGFILE uv run uvicorn app.main:app --reload --port 8000
|
|
||||||
|
|
||||||
# 终端 2 —— 前端开发服务器(http://localhost:5173)
|
# 终端 2 —— 前端开发服务器(http://localhost:5173)
|
||||||
cd frontend
|
cd frontend
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
> `env -u SSLKEYLOGFILE` 是本机(Windows)必需的:用户环境变量 `SSLKEYLOGFILE` 值开头混有不可见控制符,asyncpg 建连即崩。详见下文「快速开始(开发模式)」与「常见问题」。
|
> 后端统一走 `backend/restart_backend.sh`:自动杀旧进程树 + 剔除会崩 asyncpg 的 `SSLKEYLOGFILE` + 带热重载重启 + 日志写 `backend_run.log`。Windows cmd 下等价命令:`backend\restart_backend.cmd`。详见下文「快速开始(开发模式)」。
|
||||||
|
|
||||||
|
**首次运行前**:
|
||||||
|
1. 装依赖:后端 `cd backend && uv sync`、前端 `cd frontend && pnpm install`。
|
||||||
|
2. 后端依赖 **PostgreSQL(16/17)+ Redis 已启动**,并配好 `backend/.env`(复制 `.env.example`)。
|
||||||
|
3. 建库表:`cd backend && uv run alembic upgrade head`。
|
||||||
|
|
||||||
首次运行前先安装依赖:后端 `uv sync`(backend 下)、前端 `pnpm install`(frontend 下)。
|
|
||||||
浏览器打开 http://localhost:5173 即可使用。详细说明见下文「快速开始(开发模式)」。
|
浏览器打开 http://localhost:5173 即可使用。详细说明见下文「快速开始(开发模式)」。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -67,28 +70,36 @@ stock/
|
|||||||
├── TECH_STACK.md # 技术选型与架构决策(必读)
|
├── TECH_STACK.md # 技术选型与架构决策(必读)
|
||||||
├── README.md # 本文件
|
├── README.md # 本文件
|
||||||
├── backend/ # FastAPI + 回测引擎
|
├── backend/ # FastAPI + 回测引擎
|
||||||
│ ├── pyproject.toml # uv 依赖声明
|
│ ├── pyproject.toml # uv 依赖声明(pytest 在 dev 组)
|
||||||
│ ├── .env.example # 配置示例(数据库 / 费率)
|
│ ├── .env.example # 配置示例(数据库 / 费率)
|
||||||
│ ├── smoke_test.py # 后端全链路自检脚本
|
│ ├── smoke_test.py # 后端全链路自检脚本(线上库鉴权+核心 API)
|
||||||
|
│ ├── conftest.py # pytest 根(使 tests/ 可直接 import app.*)
|
||||||
|
│ ├── tests/ # pytest 单元测试(交割单解析/复权换算/缓存/限速/事件回测纯函数)
|
||||||
|
│ ├── scripts/ # 一次性运维脚本(TDX 导入 / 复权因子回补…)
|
||||||
│ └── app/
|
│ └── app/
|
||||||
│ ├── main.py # FastAPI 入口(启动建表)
|
│ ├── main.py # FastAPI 入口(lifespan 拉起夜间调度)
|
||||||
│ ├── config.py # 配置(pydantic-settings)
|
│ ├── config.py # 配置(pydantic-settings)
|
||||||
│ ├── db.py # async SQLAlchemy 引擎/会话
|
│ ├── db.py # async SQLAlchemy 引擎/会话
|
||||||
│ ├── domain.py # 领域契约(Bar/Signal/Fill/Position…)
|
│ ├── domain.py # 领域契约(Bar/Signal/Fill/Position…)
|
||||||
│ ├── models.py # ORM(Candle / BacktestRun)
|
│ ├── models.py # ORM(Candle / StockBasic / 用户数据表…)
|
||||||
│ ├── schemas.py # Pydantic DTO(= OpenAPI 契约)
|
│ ├── schemas.py # Pydantic DTO(= OpenAPI 契约)
|
||||||
│ ├── commission.py # A 股交易成本(已修正、可配置)
|
│ ├── auth.py # Argon2 密码 + 会话(SHA-256 摘要)
|
||||||
|
│ ├── auth_api.py # 登录/登出(含按 IP 限速)
|
||||||
|
│ ├── scheduler.py # 夜间定时任务(收盘后自动同步 + 会话清理)
|
||||||
|
│ ├── cache.py # Redis 读缓存(本地层 + 版本号失效 + 熔断冷却恢复)
|
||||||
│ ├── indicators.py # 指标:MACD/RSI/KDJ/布林/均线(单一事实源)
|
│ ├── indicators.py # 指标:MACD/RSI/KDJ/布林/均线(单一事实源)
|
||||||
│ ├── api.py # 路由:/health /candles /backtest
|
│ ├── api/ # 路由包:stocks / etfs / market / backtest / screener / user + _deps 共享件
|
||||||
│ ├── data/ # DataProvider 适配器(Tushare/AKShare)+ 周期聚合
|
│ ├── data/ # 数据管道(tushare 适配 + 同步 + 懒加载缓存)
|
||||||
│ └── backtest/ # engine / broker(PaperBroker) / metrics / strategies
|
│ ├── screener/ # 智能选股(LLM 解析 + 全市场引擎 + 夜间同步)
|
||||||
|
│ └── backtest/ # engine / events(全市场事件回测) / strategies
|
||||||
└── frontend/ # Vue SPA
|
└── frontend/ # Vue SPA
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── main.ts # PrimeVue(Aura 深色) + Pinia
|
│ ├── main.ts # Vue + Pinia
|
||||||
│ ├── api/ # 类型化客户端 + DTO 镜像
|
│ ├── api/ # 类型化客户端 + DTO 镜像
|
||||||
│ ├── stores/ # Pinia 回测状态
|
│ ├── stores/ # Pinia(鉴权/设置/同步状态)
|
||||||
│ ├── components/ # KLineChart / EquityChart / MetricsPanel / BacktestForm
|
│ ├── composables/ # 组合式工具(防抖 ref / 路由 query 同步)
|
||||||
│ └── views/ # BacktestView
|
│ ├── components/ # StockDetailOverlay / DetailKLine / LimitBoard / MarketOverview…
|
||||||
|
│ └── views/ # Home / Stocks / ETF / Concepts / Indexes / Screener / Backtest
|
||||||
└── vite.config.ts # /api 代理到 :8000
|
└── vite.config.ts # /api 代理到 :8000
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -117,17 +128,16 @@ pip install uv
|
|||||||
|
|
||||||
### 1) 后端
|
### 1) 后端
|
||||||
|
|
||||||
实际启动命令(venv 解释器直启,日志重定向到仓库根 `backend_run.log`,改代码自动热重载):
|
实际启动命令(一键脚本,自动杀旧进程树 + 剔除 SSLKEYLOGFILE + 热重载 + 日志写仓库根 `backend_run.log`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
uv sync # 创建 .venv 并安装依赖(仅首次)
|
||||||
uv sync # 创建 .venv 并安装依赖(仅首次)
|
bash backend/restart_backend.sh # 从仓库根执行(Git Bash);Windows cmd 下等价:backend\restart_backend.cmd
|
||||||
env -u SSLKEYLOGFILE .venv/Scripts/python.exe -m uvicorn app.main:app --reload --port 8000 > ../backend_run.log 2>&1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
看日志:`tail -f backend_run.log`;确认起没起:`curl http://localhost:8000/api/health`。
|
看日志:`tail -f backend_run.log`;确认起没起:`curl http://localhost:8000/api/health`。
|
||||||
|
|
||||||
等价的 `uv run` 写法(不带日志重定向,直接打到当前终端):
|
等价的 `uv run` 写法(不杀旧进程、日志直接打到当前终端):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
@@ -135,13 +145,15 @@ env -u SSLKEYLOGFILE uv run uvicorn app.main:app --reload --port 8000
|
|||||||
```
|
```
|
||||||
|
|
||||||
- **`env -u SSLKEYLOGFILE`(本机 Windows 必需)**:用户环境变量 `SSLKEYLOGFILE` 的值开头混有 U+202A 不可见控制符,asyncpg 建连时执行 `ssl.keylog_filename` 直接抛 `OSError: [Errno 22]`,uvicorn 启动即崩。Git Bash 下用 `env -u` 剔除即可;根治可 `setx SSLKEYLOGFILE "C:\Users\cirry\Desktop\fhzg.log"`(重开终端后不再需要前缀)。验证:`python -c "import os; print(repr(os.environ.get('SSLKEYLOGFILE')))"`。
|
- **`env -u SSLKEYLOGFILE`(本机 Windows 必需)**:用户环境变量 `SSLKEYLOGFILE` 的值开头混有 U+202A 不可见控制符,asyncpg 建连时执行 `ssl.keylog_filename` 直接抛 `OSError: [Errno 22]`,uvicorn 启动即崩。Git Bash 下用 `env -u` 剔除即可;根治可 `setx SSLKEYLOGFILE "C:\Users\cirry\Desktop\fhzg.log"`(重开终端后不再需要前缀)。验证:`python -c "import os; print(repr(os.environ.get('SSLKEYLOGFILE')))"`。
|
||||||
- **Windows `--reload` 僵死**:watcher 偶尔改文件不重载且日志无 Reloading 行,此时只能杀进程树重启——`netstat -ano | grep :8000` 找 PID,`taskkill //PID <pid> //T //F`。
|
- **Windows `--reload` 僵死**:watcher 偶尔改文件不重载且日志无 Reloading 行,此时只能杀进程树重启——一键脚本 `bash backend/restart_backend.sh`(Git Bash)或 `backend\restart_backend.cmd`(cmd):自动杀旧进程树(`--reload` 起 launcher→reloader→worker 三层进程)+ 带 reload 重启 + 日志写 `backend_run.log`。手动:`netstat -ano | grep :8000` 找 PID,`taskkill //PID <pid> //T //F`。
|
||||||
- 数据库结构由 Alembic 管理:首次部署/更新代码后先执行 `uv run alembic upgrade head`(见「初始化登录系统」)。
|
- 数据库结构由 Alembic 管理:首次部署/更新代码后先执行 `uv run alembic upgrade head`(见「初始化登录系统」)。
|
||||||
- 交互式 API 文档:http://localhost:8000/docs
|
- 交互式 API 文档:http://localhost:8000/docs
|
||||||
|
- **夜间自动同步**:后端启动即拉起调度(`app/scheduler.py`),每日 18:05 本地时间自动跑全市场 A 股 + ETF 同步并清理过期会话;进程启动时若已过点且当日数据未落库会补跑。关闭:`.env` 设 `NIGHTLY_SYNC_ENABLED=false`,时间改 `NIGHTLY_SYNC_HOUR`。
|
||||||
|
|
||||||
**自检**(无需起服务器,验证全链路):
|
**测试**:
|
||||||
```bash
|
```bash
|
||||||
uv run --with httpx --directory backend python smoke_test.py
|
uv run --directory backend pytest -q # 单元测试(交割单解析/复权换算/缓存/限速等纯函数,不碰库)
|
||||||
|
uv run --with httpx --directory backend python smoke_test.py # 全链路自检(线上库鉴权+核心 API)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2) 前端
|
### 2) 前端
|
||||||
@@ -152,7 +164,8 @@ pnpm install # 首次
|
|||||||
pnpm dev # http://localhost:5173
|
pnpm dev # http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
前端 `/api` 请求由 Vite 代理到后端 `:8000`(见 `vite.config.ts`),无需处理跨域。
|
- **必须用 pnpm,不要用 npm/yarn**:项目锁定 `pnpm-lock.yaml`,`node_modules` 是 pnpm 的硬链接结构,`npm install` 会写坏依赖,导致装包/构建崩溃。以后装包一律 `pnpm add <pkg>`(不要 `npm install <pkg>`)。
|
||||||
|
- 前端 `/api` 请求由 Vite 代理到后端 `:8000`(见 `vite.config.ts`),无需处理跨域。
|
||||||
|
|
||||||
打开 http://localhost:5173 → 选周期、改参数 → 点「开始回测」。
|
打开 http://localhost:5173 → 选周期、改参数 → 点「开始回测」。
|
||||||
鼠标悬停 K 线可看当日详情弹框;切日线/周线/月线/年线;勾「fast 模式」可对比关闭费用/T+1 的差异。
|
鼠标悬停 K 线可看当日详情弹框;切日线/周线/月线/年线;勾「fast 模式」可对比关闭费用/T+1 的差异。
|
||||||
@@ -229,9 +242,10 @@ EXPOSE_API_DOCS=false
|
|||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| GET | `/api/health` | 健康检查 |
|
| GET | `/api/health` | 健康检查 |
|
||||||
| GET | `/api/candles/{symbol}?timeframe=1d&limit=1000` | 取 K 线(`timeframe`: `1d`/`1w`/`1M`/`1y`) |
|
|
||||||
| POST | `/api/data/sync` | 拉取并缓存某标的日线(Tushare 主 → AKShare 兜底)。body: `{symbol, source?, force?}` |
|
| POST | `/api/data/sync` | 拉取并缓存某标的日线(Tushare 主 → AKShare 兜底)。body: `{symbol, source?, force?}` |
|
||||||
| POST | `/api/backtest` | 跑回测(真实标的首次自动拉取并缓存) |
|
| POST | `/api/backtest` | 跑回测(真实标的首次自动拉取并缓存;前端已改用 `/api/backtest/event` 事件回测,此为旧策略回测 API) |
|
||||||
|
|
||||||
|
个股 K 线统一走 `GET /api/screener/preview/{ts_code}`(含复权/指标预热/翻页,两级缓存)。
|
||||||
|
|
||||||
**回测请求示例**:
|
**回测请求示例**:
|
||||||
```json
|
```json
|
||||||
|
|||||||
16
backend/.env
16
backend/.env
@@ -1,16 +0,0 @@
|
|||||||
DATABASE_URL=postgresql+asyncpg://postgres:Cirry0115@cirry.cn:5432/stock
|
|
||||||
# TUSHARE_TOKEN=22edda0afe44c0609a187ff1ac0bb2a8fc61430f490ec19f7fec8390
|
|
||||||
TUSHARE_TOKEN=2f7dbca732cdb762eb61bf3ca1b58f0c19a12732346ae06b3f60f4f5
|
|
||||||
# 15000 积分档走 quicksync 镜像(官方接口对该 token 返回 40101)
|
|
||||||
TUSHARE_API_URL=http://api.quicksync.cn
|
|
||||||
DATA_ADJUST=qfq
|
|
||||||
DATA_DEFAULT_START=20200101
|
|
||||||
|
|
||||||
# ---- Redis 读缓存(股票列表/筛选项;留空则不缓存直查数据库)----
|
|
||||||
REDIS_URL=redis://default:26d5c71d57344f37b8b4ddb567f2652f0c7ef41c774284ad@cirry.cn:6379
|
|
||||||
|
|
||||||
# ---- LLM(智能选股;DeepSeek,OpenAI 兼容协议;/anthropic 后缀会被 _endpoint 自动归一)----
|
|
||||||
# key 在 https://bigmodel.cn 控制台获取,格式形如 xxxxxxxx.yyyyyyyy(id.secret)
|
|
||||||
LLM_BASE_URL=https://api.deepseek.com
|
|
||||||
LLM_API_KEY=sk-b09acbd6c0ca4f94818b6deb039d6515
|
|
||||||
LLM_MODEL=deepseek-chat
|
|
||||||
1488
backend/app/api.py
1488
backend/app/api.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
"""只登录、不注册的鉴权 API。"""
|
"""只登录、不注册的鉴权 API。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response, status
|
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response, status
|
||||||
@@ -49,6 +51,33 @@ def clear_session_cookie(response: Response) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 登录按 IP 限速(进程内滑动窗口,无外部依赖) ----------
|
||||||
|
# 补充账户级锁定(auth_max_failed_logins):本层拦多 IP 分布爆破,也稀释
|
||||||
|
# 「故意输错 5 次锁死他人账户」的滥用面。反代部署时 host 是代理 IP,需改读 X-Forwarded-For。
|
||||||
|
_LOGIN_WINDOW = 60.0
|
||||||
|
_LOGIN_MAX_PER_WINDOW = 15
|
||||||
|
_login_attempts: dict[str, deque[float]] = {}
|
||||||
|
_login_gc_at = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _login_rate_limited(ip: str) -> bool:
|
||||||
|
"""超限返回 True;未超限记录本次尝试(成功失败都计)。"""
|
||||||
|
global _login_gc_at
|
||||||
|
now = time.monotonic()
|
||||||
|
q = _login_attempts.setdefault(ip, deque())
|
||||||
|
while q and q[0] <= now - _LOGIN_WINDOW:
|
||||||
|
q.popleft()
|
||||||
|
if len(q) >= _LOGIN_MAX_PER_WINDOW:
|
||||||
|
return True
|
||||||
|
q.append(now)
|
||||||
|
if now - _login_gc_at > 3600: # 顺手回收陈旧 entry,防长跑内存增长
|
||||||
|
_login_gc_at = now
|
||||||
|
stale = now - _LOGIN_WINDOW * 10
|
||||||
|
for k in [k for k, v in _login_attempts.items() if not v or v[-1] <= stale]:
|
||||||
|
del _login_attempts[k]
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=LoginResponse)
|
@router.post("/login", response_model=LoginResponse)
|
||||||
async def login(
|
async def login(
|
||||||
payload: LoginRequest,
|
payload: LoginRequest,
|
||||||
@@ -56,6 +85,10 @@ async def login(
|
|||||||
response: Response,
|
response: Response,
|
||||||
db: AsyncSession = Depends(get_session),
|
db: AsyncSession = Depends(get_session),
|
||||||
) -> LoginResponse:
|
) -> LoginResponse:
|
||||||
|
ip = request.client.host if request.client else "?"
|
||||||
|
if _login_rate_limited(ip):
|
||||||
|
raise HTTPException(status_code=429, detail="登录尝试过于频繁,请稍后再试")
|
||||||
|
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
username = payload.username.strip()
|
username = payload.username.strip()
|
||||||
user = (await db.execute(select(User).where(User.username == username))).scalar_one_or_none()
|
user = (await db.execute(select(User).where(User.username == username))).scalar_one_or_none()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ match=all(连续满足)/any(曾经满足),多条件之间取 AND。
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -120,6 +121,69 @@ def _stats_block(trades: list[dict]) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_batch(
|
||||||
|
candle_rows: list,
|
||||||
|
code_by_symbol: dict[str, str],
|
||||||
|
f_map: dict,
|
||||||
|
name_map: dict[str, str],
|
||||||
|
spec: EventBacktestSpec,
|
||||||
|
start_ts: datetime,
|
||||||
|
trades_limit: int,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""纯 CPU:一批 bar -> 交易明细(放线程池跑,不阻塞事件循环)。"""
|
||||||
|
trades: list[dict] = []
|
||||||
|
if not candle_rows:
|
||||||
|
return trades
|
||||||
|
bars = pd.DataFrame(
|
||||||
|
candle_rows, columns=["symbol", "ts", "open", "high", "low", "close"]
|
||||||
|
)
|
||||||
|
for symbol, g in bars.groupby("symbol", sort=False):
|
||||||
|
if len(g) < 30:
|
||||||
|
continue
|
||||||
|
g = g.reset_index(drop=True)
|
||||||
|
ts_code_l = code_by_symbol[symbol]
|
||||||
|
cache: dict = {"_families": set()}
|
||||||
|
mask = _signal_mask(g, spec, cache)
|
||||||
|
if not mask.any():
|
||||||
|
continue
|
||||||
|
for sig_i in np.flatnonzero(mask.to_numpy()):
|
||||||
|
ts_sig = g.at[sig_i, "ts"]
|
||||||
|
# 信号必须落在回测窗口内(buffer 区只用于指标配热)
|
||||||
|
if ts_sig < start_ts:
|
||||||
|
continue
|
||||||
|
ie = _entry_exit_indices(int(sig_i), spec, len(g))
|
||||||
|
if ie is None:
|
||||||
|
continue
|
||||||
|
entry_i, exit_i = ie
|
||||||
|
e_row, x_row = g.iloc[entry_i], g.iloc[exit_i]
|
||||||
|
e_price = _price_at(e_row, "open" if spec.entry_timing == "next_open" else "close")
|
||||||
|
x_price = _price_at(x_row, "open" if spec.exit_timing == "open" else "close")
|
||||||
|
if not e_price or not x_price:
|
||||||
|
continue
|
||||||
|
f_in = f_map.get((ts_code_l, e_row["ts"].date()), 1.0)
|
||||||
|
f_out = f_map.get((ts_code_l, x_row["ts"].date()), 1.0)
|
||||||
|
ret_pct = (x_price * f_out) / (e_price * f_in) * 100 - 100
|
||||||
|
trades.append({
|
||||||
|
"ts_code": ts_code_l,
|
||||||
|
"name": name_map.get(ts_code_l),
|
||||||
|
"entry_date": e_row["ts"], "entry_price": round(e_price, 3),
|
||||||
|
"exit_date": x_row["ts"], "exit_price": round(x_price, 3),
|
||||||
|
"ret_pct": round(float(ret_pct), 3),
|
||||||
|
})
|
||||||
|
if len(trades) >= trades_limit:
|
||||||
|
return trades
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize(trades: list[dict]) -> tuple[dict, list[dict]]:
|
||||||
|
"""纯 CPU:汇总统计 + 最好/最差样本(同样下线程池)。"""
|
||||||
|
stats = _stats_block(trades)
|
||||||
|
# 明细样本:最好 100 + 最差 100(其余统计已覆盖)
|
||||||
|
trades_sorted = sorted(trades, key=lambda t: t["ret_pct"], reverse=True)
|
||||||
|
sample = trades_sorted[:100] + (trades_sorted[-100:] if len(trades_sorted) > 100 else [])
|
||||||
|
return stats, sample
|
||||||
|
|
||||||
|
|
||||||
async def run_event_backtest(
|
async def run_event_backtest(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
spec: EventBacktestSpec,
|
spec: EventBacktestSpec,
|
||||||
@@ -195,53 +259,15 @@ async def run_event_backtest(
|
|||||||
)).all()
|
)).all()
|
||||||
f_map = {(r[0], r[1].date()): float(r[2]) for r in adj_rows if r[2]}
|
f_map = {(r[0], r[1].date()): float(r[2]) for r in adj_rows if r[2]}
|
||||||
|
|
||||||
bars = pd.DataFrame(
|
# pandas 全市场扫描是同步 CPU 重计算,丢线程池跑(await 期间事件循环可服务其他请求)
|
||||||
candle_rows, columns=["symbol", "ts", "open", "high", "low", "close"]
|
trades.extend(await asyncio.to_thread(
|
||||||
)
|
_scan_batch, candle_rows, code_by_symbol, f_map, name_map,
|
||||||
for symbol, g in bars.groupby("symbol", sort=False):
|
spec, start_ts, MAX_TRADES - len(trades),
|
||||||
if len(g) < 30:
|
))
|
||||||
continue
|
|
||||||
g = g.reset_index(drop=True)
|
|
||||||
ts_code_l = code_by_symbol[symbol]
|
|
||||||
cache: dict = {"_families": set()}
|
|
||||||
mask = _signal_mask(g, spec, cache)
|
|
||||||
if not mask.any():
|
|
||||||
continue
|
|
||||||
for sig_i in np.flatnonzero(mask.to_numpy()):
|
|
||||||
ts_sig = g.at[sig_i, "ts"]
|
|
||||||
# 信号必须落在回测窗口内(buffer 区只用于指标配热)
|
|
||||||
if ts_sig < start_ts:
|
|
||||||
continue
|
|
||||||
ie = _entry_exit_indices(int(sig_i), spec, len(g))
|
|
||||||
if ie is None:
|
|
||||||
continue
|
|
||||||
entry_i, exit_i = ie
|
|
||||||
e_row, x_row = g.iloc[entry_i], g.iloc[exit_i]
|
|
||||||
e_price = _price_at(e_row, "open" if spec.entry_timing == "next_open" else "close")
|
|
||||||
x_price = _price_at(x_row, "open" if spec.exit_timing == "open" else "close")
|
|
||||||
if not e_price or not x_price:
|
|
||||||
continue
|
|
||||||
f_in = f_map.get((ts_code_l, e_row["ts"].date()), 1.0)
|
|
||||||
f_out = f_map.get((ts_code_l, x_row["ts"].date()), 1.0)
|
|
||||||
ret_pct = (x_price * f_out) / (e_price * f_in) * 100 - 100
|
|
||||||
trades.append({
|
|
||||||
"ts_code": ts_code_l,
|
|
||||||
"name": name_map.get(ts_code_l),
|
|
||||||
"entry_date": e_row["ts"], "entry_price": round(e_price, 3),
|
|
||||||
"exit_date": x_row["ts"], "exit_price": round(x_price, 3),
|
|
||||||
"ret_pct": round(float(ret_pct), 3),
|
|
||||||
})
|
|
||||||
if len(trades) >= MAX_TRADES:
|
|
||||||
break
|
|
||||||
if len(trades) >= MAX_TRADES:
|
|
||||||
break
|
|
||||||
if len(trades) >= MAX_TRADES:
|
if len(trades) >= MAX_TRADES:
|
||||||
break
|
break
|
||||||
|
|
||||||
stats = _stats_block(trades)
|
stats, sample = await asyncio.to_thread(_summarize, trades)
|
||||||
# 明细样本:最好 100 + 最差 100(其余统计已覆盖)
|
|
||||||
trades_sorted = sorted(trades, key=lambda t: t["ret_pct"], reverse=True)
|
|
||||||
sample = trades_sorted[:100] + (trades_sorted[-100:] if len(trades_sorted) > 100 else [])
|
|
||||||
return {
|
return {
|
||||||
"spec": spec,
|
"spec": spec,
|
||||||
"universe": ts_code or "all",
|
"universe": ts_code or "all",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""Redis 读缓存(可选基础设施)。
|
"""Redis 读缓存(可选基础设施)。
|
||||||
|
|
||||||
- REDIS_URL 留空、连接失败或超时:所有操作静默退化为「无缓存」,接口照常直查数据库,
|
- REDIS_URL 留空、连接失败或超时:所有操作静默退化为「无缓存」,接口照常直查数据库。
|
||||||
且本进程内禁用重试(避免每个请求都陪跑一次连接超时)。
|
故障后熔断 _RECOVERY_SECONDS(期间所有请求直连,不陪跑连接超时),到期自动放行
|
||||||
|
一次探测——成功即完全恢复,仍失败则重新熔断(Redis 属加速件,坏了不能拖慢接口)。
|
||||||
- 失效策略:TTL 自然过期 + 版本号(INCR)作废。自选股增删等写操作只 INCR 版本 key,
|
- 失效策略:TTL 自然过期 + 版本号(INCR)作废。自选股增删等写操作只 INCR 版本 key,
|
||||||
旧缓存 key 里带着旧版本号,无需 SCAN 批量删除。
|
旧缓存 key 里带着旧版本号,无需 SCAN 批量删除。
|
||||||
- 只缓存「读多写少、可容忍短暂陈旧」的聚合数据(股票列表、筛选项等);
|
- 只缓存「读多写少、可容忍短暂陈旧」的聚合数据(股票列表、筛选项等);
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
@@ -23,12 +25,13 @@ import redis.asyncio as aioredis
|
|||||||
from .config import settings
|
from .config import settings
|
||||||
|
|
||||||
_pool: aioredis.ConnectionPool | None = None
|
_pool: aioredis.ConnectionPool | None = None
|
||||||
_disabled = False # 一次失败后本进程禁用(Redis 属加速件,坏了不能拖慢接口)
|
_RECOVERY_SECONDS = 60.0 # 熔断时长:期间直连不试 Redis,到期放行一次探测
|
||||||
|
_disabled_until = 0.0 # 熔断截止的 monotonic 时刻;0 = 未熔断
|
||||||
|
|
||||||
|
|
||||||
def _client() -> aioredis.Redis | None:
|
def _client() -> aioredis.Redis | None:
|
||||||
global _pool, _disabled
|
global _pool
|
||||||
if not settings.redis_url or _disabled:
|
if not settings.redis_url or time.monotonic() < _disabled_until:
|
||||||
return None
|
return None
|
||||||
if _pool is None:
|
if _pool is None:
|
||||||
_pool = aioredis.ConnectionPool.from_url(
|
_pool = aioredis.ConnectionPool.from_url(
|
||||||
@@ -43,8 +46,8 @@ def _client() -> aioredis.Redis | None:
|
|||||||
|
|
||||||
|
|
||||||
def _bail() -> None:
|
def _bail() -> None:
|
||||||
global _disabled
|
global _disabled_until
|
||||||
_disabled = True
|
_disabled_until = time.monotonic() + _RECOVERY_SECONDS
|
||||||
|
|
||||||
|
|
||||||
def digest(*parts: Any) -> str:
|
def digest(*parts: Any) -> str:
|
||||||
@@ -84,7 +87,21 @@ def local_set(key: str, raw: str, ttl: int) -> None:
|
|||||||
_local_store[key] = (time.monotonic() + max(1, min(ttl, 120)), raw)
|
_local_store[key] = (time.monotonic() + max(1, min(ttl, 120)), raw)
|
||||||
_local_bytes += len(raw) + 64 # 连同 dict/tuple 开销粗略计入
|
_local_bytes += len(raw) + 64 # 连同 dict/tuple 开销粗略计入
|
||||||
while _local_store and (len(_local_store) > _LOCAL_MAX_ENTRIES or _local_bytes > _LOCAL_MAX_BYTES):
|
while _local_store and (len(_local_store) > _LOCAL_MAX_ENTRIES or _local_bytes > _LOCAL_MAX_BYTES):
|
||||||
_local_bytes -= len(_local_store.popitem(last=False)[1][1]) + 64
|
# dict 是插入序:next(iter(...)) 即最旧键(dict.popitem 不支持参数,别写成 OrderedDict 的写法)
|
||||||
|
oldest = next(iter(_local_store))
|
||||||
|
_local_bytes -= len(_local_store.pop(oldest)[1]) + 64
|
||||||
|
|
||||||
|
|
||||||
|
# --- 后台写(fire-and-forget)------------------------------------------------
|
||||||
|
# 读路径拿到响应后异步写 Redis、不阻塞返回。统一入口:任务挂全局集合防 GC,
|
||||||
|
# cache_set 内部自带异常静默(缓存层尽力而为),优雅停机丢最后一次写无害(TTL 兜底)。
|
||||||
|
_bg_tasks: set[asyncio.Task] = set()
|
||||||
|
|
||||||
|
|
||||||
|
def set_bg(key: str, value: Any, ttl: int) -> None:
|
||||||
|
t = asyncio.create_task(cache_set(key, value, ttl))
|
||||||
|
_bg_tasks.add(t)
|
||||||
|
t.add_done_callback(_bg_tasks.discard)
|
||||||
|
|
||||||
|
|
||||||
# --- 版本号本地缓存:热请求连 Redis GET ver:xx 都省掉 ------------------------
|
# --- 版本号本地缓存:热请求连 Redis GET ver:xx 都省掉 ------------------------
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class Settings(BaseSettings):
|
|||||||
screener_default_limit: int = 200 # 选股结果条数上限
|
screener_default_limit: int = 200 # 选股结果条数上限
|
||||||
screener_sync_interval: float = 0.35 # 全市场批量调用间隔(秒),Tushare 控频
|
screener_sync_interval: float = 0.35 # 全市场批量调用间隔(秒),Tushare 控频
|
||||||
|
|
||||||
|
# ---- 夜间定时任务(收盘后自动同步 + 会话清理;见 app/scheduler.py)----
|
||||||
|
nightly_sync_enabled: bool = True
|
||||||
|
nightly_sync_hour: int = 18 # 本地时间整点,触发在 :05(收盘后日线已生成)
|
||||||
|
|
||||||
# A股交易成本(基准日 2026-08)——做成可配置参数,便于将来按生效日期版本化
|
# A股交易成本(基准日 2026-08)——做成可配置参数,便于将来按生效日期版本化
|
||||||
stamp_duty_rate: float = 0.0005 # 印花税 0.05%,单边卖出(2023-08-28 减半)
|
stamp_duty_rate: float = 0.0005 # 印花税 0.05%,单边卖出(2023-08-28 减半)
|
||||||
transfer_fee_rate: float = 0.00001 # 过户费 0.001%,沪深双边(2022 调整)
|
transfer_fee_rate: float = 0.00001 # 过户费 0.001%,沪深双边(2022 调整)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
@@ -18,12 +18,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..db import async_session
|
from ..db import async_session
|
||||||
from ..models import StockCompany
|
from ..models import StockCompany
|
||||||
|
from .sync_utils import call_retry, f_clean, fresh, get_pro_lazy, s_clean, utcnow
|
||||||
|
|
||||||
_REFRESH_DAYS = 30
|
_REFRESH_DAYS = 30
|
||||||
|
|
||||||
# 频率超限特征(等待 62s 重试一次;与 screener.market_sync / data.etf_sync._call_retry 同款语义)
|
|
||||||
_RATE_MARKS = ("频率超限", "每分钟")
|
|
||||||
|
|
||||||
# 显式列出全部字段:introduction/office/main_business/business_scope 文档标注默认不显示,
|
# 显式列出全部字段:introduction/office/main_business/business_scope 文档标注默认不显示,
|
||||||
# 不传 fields 时 tushare 不返回这四列(实测 000001.SZ)
|
# 不传 fields 时 tushare 不返回这四列(实测 000001.SZ)
|
||||||
_FIELDS = (
|
_FIELDS = (
|
||||||
@@ -32,87 +30,38 @@ _FIELDS = (
|
|||||||
"employees,main_business,business_scope"
|
"employees,main_business,business_scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
_pro = None # 惰性单例(get_pro 每次都 ts.set_token 写文件,没必要重复)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pro():
|
|
||||||
if not settings.tushare_token:
|
|
||||||
raise RuntimeError("未配置 TUSHARE_TOKEN,无法拉取公司简介(backend/.env)")
|
|
||||||
global _pro
|
|
||||||
if _pro is None:
|
|
||||||
from .tushare_provider import get_pro
|
|
||||||
|
|
||||||
_pro = get_pro()
|
|
||||||
return _pro
|
|
||||||
|
|
||||||
|
|
||||||
def _call_retry(fn, *args, **kwargs):
|
|
||||||
"""同步调用 tushare 接口;「每分钟」级频率超限等 62s 重试一次。"""
|
|
||||||
try:
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
msg = str(e)
|
|
||||||
if any(m in msg for m in _RATE_MARKS) and "小时" not in msg:
|
|
||||||
time.sleep(62)
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def _utcnow() -> datetime:
|
|
||||||
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
||||||
|
|
||||||
|
|
||||||
def _fresh(updated_at: datetime | None) -> bool:
|
|
||||||
return updated_at is not None and updated_at >= _utcnow() - timedelta(days=_REFRESH_DAYS)
|
|
||||||
|
|
||||||
|
|
||||||
def _s(v) -> str | None:
|
|
||||||
"""pandas NaN / 空串 / None -> None,其余 strip。"""
|
|
||||||
if v is None or (isinstance(v, float) and v != v):
|
|
||||||
return None
|
|
||||||
s = str(v).strip()
|
|
||||||
return s or None
|
|
||||||
|
|
||||||
|
|
||||||
def _f(v) -> float | None:
|
|
||||||
try:
|
|
||||||
f = float(v)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
return None if f != f else f # NaN -> None
|
|
||||||
|
|
||||||
|
|
||||||
def _i(v) -> int | None:
|
def _i(v) -> int | None:
|
||||||
f = _f(v)
|
f = f_clean(v)
|
||||||
return None if f is None else int(f)
|
return None if f is None else int(f)
|
||||||
|
|
||||||
|
|
||||||
def fetch_company_sync(ts_code: str) -> dict | None:
|
def fetch_company_sync(ts_code: str) -> dict | None:
|
||||||
"""同步拉单只公司简介(需在 to_thread 里跑);返回行 dict,无此股返回 None。"""
|
"""同步拉单只公司简介(需在 to_thread 里跑);返回行 dict,无此股返回 None。"""
|
||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
df = _call_retry(_get_pro().stock_company, ts_code=ts_code, fields=_FIELDS)
|
df = call_retry(get_pro_lazy().stock_company, ts_code=ts_code, fields=_FIELDS)
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return None
|
return None
|
||||||
r = df.iloc[0]
|
r = df.iloc[0]
|
||||||
return {
|
return {
|
||||||
"ts_code": ts_code,
|
"ts_code": ts_code,
|
||||||
"com_name": _s(r.get("com_name")),
|
"com_name": s_clean(r.get("com_name")),
|
||||||
"com_id": _s(r.get("com_id")),
|
"com_id": s_clean(r.get("com_id")),
|
||||||
"chairman": _s(r.get("chairman")),
|
"chairman": s_clean(r.get("chairman")),
|
||||||
"manager": _s(r.get("manager")),
|
"manager": s_clean(r.get("manager")),
|
||||||
"secretary": _s(r.get("secretary")),
|
"secretary": s_clean(r.get("secretary")),
|
||||||
"reg_capital": _f(r.get("reg_capital")),
|
"reg_capital": f_clean(r.get("reg_capital")),
|
||||||
"setup_date": _s(r.get("setup_date")),
|
"setup_date": s_clean(r.get("setup_date")),
|
||||||
"province": _s(r.get("province")),
|
"province": s_clean(r.get("province")),
|
||||||
"city": _s(r.get("city")),
|
"city": s_clean(r.get("city")),
|
||||||
"introduction": _s(r.get("introduction")),
|
"introduction": s_clean(r.get("introduction")),
|
||||||
"website": _s(r.get("website")),
|
"website": s_clean(r.get("website")),
|
||||||
"email": _s(r.get("email")),
|
"email": s_clean(r.get("email")),
|
||||||
"office": _s(r.get("office")),
|
"office": s_clean(r.get("office")),
|
||||||
"employees": _i(r.get("employees")),
|
"employees": _i(r.get("employees")),
|
||||||
"main_business": _s(r.get("main_business")),
|
"main_business": s_clean(r.get("main_business")),
|
||||||
"business_scope": _s(r.get("business_scope")),
|
"business_scope": s_clean(r.get("business_scope")),
|
||||||
"updated_at": _utcnow(),
|
"updated_at": utcnow(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -151,7 +100,7 @@ async def get_company(session: AsyncSession, ts_code: str) -> dict | None:
|
|||||||
"""
|
"""
|
||||||
row = (await session.execute(
|
row = (await session.execute(
|
||||||
select(StockCompany).where(StockCompany.ts_code == ts_code))).scalar_one_or_none()
|
select(StockCompany).where(StockCompany.ts_code == ts_code))).scalar_one_or_none()
|
||||||
if row is not None and _fresh(row.updated_at):
|
if row is not None and fresh(row.updated_at, _REFRESH_DAYS):
|
||||||
return _row_dict(row) if row.com_name is not None else None # 墓碑 -> None
|
return _row_dict(row) if row.com_name is not None else None # 墓碑 -> None
|
||||||
# 释放请求会话持有的连接:后面可能隔着 1-2s 的 tushare 调用,别长占连接池。
|
# 释放请求会话持有的连接:后面可能隔着 1-2s 的 tushare 调用,别长占连接池。
|
||||||
# 用 close() 而非 rollback():rollback 会把会话身份映射里的实例全部 expire——
|
# 用 close() 而非 rollback():rollback 会把会话身份映射里的实例全部 expire——
|
||||||
@@ -165,7 +114,7 @@ async def get_company(session: AsyncSession, ts_code: str) -> dict | None:
|
|||||||
async with async_session() as s2: # 锁内重读 + 写入走新会话
|
async with async_session() as s2: # 锁内重读 + 写入走新会话
|
||||||
row = (await s2.execute(
|
row = (await s2.execute(
|
||||||
select(StockCompany).where(StockCompany.ts_code == ts_code))).scalar_one_or_none()
|
select(StockCompany).where(StockCompany.ts_code == ts_code))).scalar_one_or_none()
|
||||||
if row is not None and _fresh(row.updated_at):
|
if row is not None and fresh(row.updated_at, _REFRESH_DAYS):
|
||||||
return _row_dict(row) if row.com_name is not None else None
|
return _row_dict(row) if row.com_name is not None else None
|
||||||
try:
|
try:
|
||||||
fetched = await asyncio.to_thread(fetch_company_sync, ts_code)
|
fetched = await asyncio.to_thread(fetch_company_sync, ts_code)
|
||||||
@@ -174,5 +123,5 @@ async def get_company(session: AsyncSession, ts_code: str) -> dict | None:
|
|||||||
if row is not None and row.com_name is not None:
|
if row is not None and row.com_name is not None:
|
||||||
return _row_dict(row)
|
return _row_dict(row)
|
||||||
raise
|
raise
|
||||||
await _upsert(s2, fetched or {"ts_code": ts_code, "updated_at": _utcnow()})
|
await _upsert(s2, fetched or {"ts_code": ts_code, "updated_at": utcnow()})
|
||||||
return fetched
|
return fetched
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from sqlalchemy import delete, func, select, text
|
from sqlalchemy import delete, func, select, text
|
||||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
@@ -29,6 +29,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from .. import cache
|
from .. import cache
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from . import etf_provider
|
from . import etf_provider
|
||||||
|
from .sync_utils import call_retry, get_pro_lazy, utcnow
|
||||||
|
|
||||||
# 进程内单例任务状态(uvicorn 单进程场景够用)
|
# 进程内单例任务状态(uvicorn 单进程场景够用)
|
||||||
_state: dict = {
|
_state: dict = {
|
||||||
@@ -46,34 +47,6 @@ _lock = asyncio.Lock()
|
|||||||
_BATCH = 3000 # upsert 分批行数(asyncpg 单语句参数上限 32766,10 列/行)
|
_BATCH = 3000 # upsert 分批行数(asyncpg 单语句参数上限 32766,10 列/行)
|
||||||
# fund_daily 返回全市场基金 ~2100 行,一天一批远小于上限
|
# fund_daily 返回全市场基金 ~2100 行,一天一批远小于上限
|
||||||
|
|
||||||
# 频率超限特征(等待 62s 重试一次;与 screener.market_sync._call_retry 同款语义)
|
|
||||||
_RATE_MARKS = ("频率超限", "每分钟")
|
|
||||||
|
|
||||||
|
|
||||||
def _call_retry(fn, *args, **kwargs):
|
|
||||||
"""同步调用 tushare 接口;「每分钟」级频率超限等 62s 重试一次。"""
|
|
||||||
try:
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
msg = str(e)
|
|
||||||
if any(m in msg for m in _RATE_MARKS) and "小时" not in msg:
|
|
||||||
time.sleep(62)
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pro():
|
|
||||||
"""token 检查 + 返回 pro api 客户端(同步对象,调用需 to_thread 包裹)。"""
|
|
||||||
if not settings.tushare_token:
|
|
||||||
raise RuntimeError("未配置 TUSHARE_TOKEN,无法同步 ETF 日线(backend/.env)")
|
|
||||||
from .tushare_provider import get_pro
|
|
||||||
|
|
||||||
return get_pro()
|
|
||||||
|
|
||||||
|
|
||||||
def _utcnow() -> datetime:
|
|
||||||
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_d(s: str) -> datetime:
|
def _parse_d(s: str) -> datetime:
|
||||||
return datetime.strptime(str(s), "%Y%m%d")
|
return datetime.strptime(str(s), "%Y%m%d")
|
||||||
@@ -85,7 +58,7 @@ async def _sync_spot(session: AsyncSession) -> int:
|
|||||||
|
|
||||||
async with etf_provider.new_client() as client:
|
async with etf_provider.new_client() as client:
|
||||||
rows = await etf_provider.fetch_etf_spot(client)
|
rows = await etf_provider.fetch_etf_spot(client)
|
||||||
now = _utcnow()
|
now = utcnow()
|
||||||
stmt = pg_insert(EtfBasic).values([{**r, "updated_at": now} for r in rows])
|
stmt = pg_insert(EtfBasic).values([{**r, "updated_at": now} for r in rows])
|
||||||
stmt = stmt.on_conflict_do_update(
|
stmt = stmt.on_conflict_do_update(
|
||||||
index_elements=["ts_code"],
|
index_elements=["ts_code"],
|
||||||
@@ -108,7 +81,7 @@ async def _sync_spot(session: AsyncSession) -> int:
|
|||||||
def _fetch_day_sync(pro, d: str) -> list[dict]:
|
def _fetch_day_sync(pro, d: str) -> list[dict]:
|
||||||
"""拉某交易日全市场场内基金日线(fund_daily;未生成的日期返回空)。"""
|
"""拉某交易日全市场场内基金日线(fund_daily;未生成的日期返回空)。"""
|
||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
df = _call_retry(pro.fund_daily, trade_date=d)
|
df = call_retry(pro.fund_daily, trade_date=d)
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return []
|
return []
|
||||||
rows = []
|
rows = []
|
||||||
@@ -128,7 +101,7 @@ def _fetch_day_sync(pro, d: str) -> list[dict]:
|
|||||||
def _fetch_symbol_sync(pro, ts_code: str, start: str | None, end: str | None) -> list[dict]:
|
def _fetch_symbol_sync(pro, ts_code: str, start: str | None, end: str | None) -> list[dict]:
|
||||||
"""按 ts_code 增量/全量拉单只 ETF 日线(start=None 即上市以来全量)。"""
|
"""按 ts_code 增量/全量拉单只 ETF 日线(start=None 即上市以来全量)。"""
|
||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
df = _call_retry(pro.fund_daily, ts_code=ts_code, start_date=start, end_date=end)
|
df = call_retry(pro.fund_daily, ts_code=ts_code, start_date=start, end_date=end)
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return []
|
return []
|
||||||
df = df.sort_values("trade_date")
|
df = df.sort_values("trade_date")
|
||||||
@@ -209,7 +182,7 @@ async def _run_sync(full: bool) -> None:
|
|||||||
from ..models import Candle, EtfBasic, TradeCalendar
|
from ..models import Candle, EtfBasic, TradeCalendar
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pro = await asyncio.to_thread(_get_pro)
|
pro = await asyncio.to_thread(get_pro_lazy)
|
||||||
|
|
||||||
# 1) 快照 -> etf_basic
|
# 1) 快照 -> etf_basic
|
||||||
_state["step"] = "正在拉取 ETF 列表"
|
_state["step"] = "正在拉取 ETF 列表"
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ async def sync_symbol(
|
|||||||
)
|
)
|
||||||
await session.execute(stmt)
|
await session.execute(stmt)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
# 作废 candles 相关读缓存(preview 等)——不 bump 的话旧版本号的缓存要等 TTL 自然过期
|
||||||
|
from .. import cache
|
||||||
|
await cache.bump_version("candles")
|
||||||
return {"symbol": code, "bars": len(bars), "source": used}
|
return {"symbol": code, "bars": len(bars), "source": used}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import math
|
|
||||||
import time
|
import time
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
from .. import cache
|
from .. import cache
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..domain import Bar
|
from ..domain import Bar
|
||||||
|
from .sync_utils import d8_iso, f_clean
|
||||||
|
|
||||||
# ---- 静态元数据表(tushare index_global 支持的全部 21 个指数,展示顺序即文档顺序)----
|
# ---- 静态元数据表(tushare index_global 支持的全部 21 个指数,展示顺序即文档顺序)----
|
||||||
# region: americas 美洲 / europe 欧洲 / asia 亚太(含港股与富时A50)
|
# region: americas 美洲 / europe 欧洲 / asia 亚太(含港股与富时A50)
|
||||||
@@ -82,22 +82,6 @@ class GlobalIndexError(RuntimeError):
|
|||||||
"""全部国际指数都拉不到(token/网络故障)——接口层转 503。"""
|
"""全部国际指数都拉不到(token/网络故障)——接口层转 503。"""
|
||||||
|
|
||||||
|
|
||||||
def _f(v) -> float | None:
|
|
||||||
"""pandas 值 -> float;NaN/None -> None。"""
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
f = float(v)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
return None if math.isnan(f) else f
|
|
||||||
|
|
||||||
|
|
||||||
def _d(v) -> str | None:
|
|
||||||
"""YYYYMMDD -> 'YYYY-MM-DD'(字符串便于 JSON 缓存)。"""
|
|
||||||
return datetime.strptime(str(v), "%Y%m%d").date().isoformat() if v else None
|
|
||||||
|
|
||||||
|
|
||||||
def is_cn_index(code: str) -> bool:
|
def is_cn_index(code: str) -> bool:
|
||||||
return "." in code
|
return "." in code
|
||||||
|
|
||||||
@@ -128,14 +112,14 @@ def _fetch_quote_sync(pro, ts_code: str) -> dict:
|
|||||||
tail = df.tail(_SPARK_DAYS)
|
tail = df.tail(_SPARK_DAYS)
|
||||||
last = df.iloc[-1]
|
last = df.iloc[-1]
|
||||||
return {
|
return {
|
||||||
"close": _f(last["close"]),
|
"close": f_clean(last["close"]),
|
||||||
"change": _f(last.get("change")),
|
"change": f_clean(last.get("change")),
|
||||||
"pct_chg": _f(last.get("pct_chg")),
|
"pct_chg": f_clean(last.get("pct_chg")),
|
||||||
"open": _f(last.get("open")),
|
"open": f_clean(last.get("open")),
|
||||||
"high": _f(last.get("high")),
|
"high": f_clean(last.get("high")),
|
||||||
"low": _f(last.get("low")),
|
"low": f_clean(last.get("low")),
|
||||||
"pre_close": _f(last.get("pre_close")),
|
"pre_close": f_clean(last.get("pre_close")),
|
||||||
"trade_date": _d(last["trade_date"]),
|
"trade_date": d8_iso(last["trade_date"]),
|
||||||
"spark": [round(float(c), 4) for c in tail["close"]],
|
"spark": [round(float(c), 4) for c in tail["close"]],
|
||||||
"spark_dates": [str(d) for d in tail["trade_date"]],
|
"spark_dates": [str(d) for d in tail["trade_date"]],
|
||||||
}
|
}
|
||||||
@@ -249,8 +233,8 @@ def _fetch_global_bars_sync(ts_code: str) -> list[Bar]:
|
|||||||
df = pd.concat(frames).drop_duplicates(subset="trade_date").sort_values("trade_date")
|
df = pd.concat(frames).drop_duplicates(subset="trade_date").sort_values("trade_date")
|
||||||
bars: list[Bar] = []
|
bars: list[Bar] = []
|
||||||
for _, r in df.iterrows():
|
for _, r in df.iterrows():
|
||||||
vol = _f(r.get("vol"))
|
vol = f_clean(r.get("vol"))
|
||||||
amt = _f(r.get("amount"))
|
amt = f_clean(r.get("amount"))
|
||||||
bars.append(
|
bars.append(
|
||||||
Bar(
|
Bar(
|
||||||
ts=datetime.strptime(str(r["trade_date"]), "%Y%m%d"),
|
ts=datetime.strptime(str(r["trade_date"]), "%Y%m%d"),
|
||||||
@@ -318,9 +302,9 @@ def _fetch_basic_sync(ts_code: str) -> dict:
|
|||||||
"market": r.get("market"),
|
"market": r.get("market"),
|
||||||
"publisher": r.get("publisher"),
|
"publisher": r.get("publisher"),
|
||||||
"category": r.get("category"),
|
"category": r.get("category"),
|
||||||
"base_date": _d(r.get("base_date")),
|
"base_date": d8_iso(r.get("base_date")),
|
||||||
"base_point": _f(r.get("base_point")),
|
"base_point": f_clean(r.get("base_point")),
|
||||||
"list_date": _d(r.get("list_date")),
|
"list_date": d8_iso(r.get("list_date")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -354,10 +338,10 @@ def _fetch_valuation_sync(ts_code: str, days: int) -> list[dict]:
|
|||||||
rows = []
|
rows = []
|
||||||
for _, r in df.sort_values("trade_date").iterrows():
|
for _, r in df.sort_values("trade_date").iterrows():
|
||||||
rows.append({
|
rows.append({
|
||||||
"trade_date": _d(r["trade_date"]),
|
"trade_date": d8_iso(r["trade_date"]),
|
||||||
"pe": _f(r.get("pe")), "pe_ttm": _f(r.get("pe_ttm")), "pb": _f(r.get("pb")),
|
"pe": f_clean(r.get("pe")), "pe_ttm": f_clean(r.get("pe_ttm")), "pb": f_clean(r.get("pb")),
|
||||||
"turnover_rate": _f(r.get("turnover_rate")),
|
"turnover_rate": f_clean(r.get("turnover_rate")),
|
||||||
"total_mv": _f(r.get("total_mv")), "float_mv": _f(r.get("float_mv")),
|
"total_mv": f_clean(r.get("total_mv")), "float_mv": f_clean(r.get("float_mv")),
|
||||||
})
|
})
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@@ -395,7 +379,7 @@ def _fetch_weights_sync(ts_code: str) -> dict | None:
|
|||||||
latest_date = df.iloc[0]["trade_date"]
|
latest_date = df.iloc[0]["trade_date"]
|
||||||
rows = df[df["trade_date"] == latest_date]
|
rows = df[df["trade_date"] == latest_date]
|
||||||
return {
|
return {
|
||||||
"trade_date": _d(latest_date),
|
"trade_date": d8_iso(latest_date),
|
||||||
"total": int(len(rows)),
|
"total": int(len(rows)),
|
||||||
"items": [
|
"items": [
|
||||||
{"con_code": str(r["con_code"]), "weight": round(float(r["weight"]), 4)}
|
{"con_code": str(r["con_code"]), "weight": round(float(r["weight"]), 4)}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import math
|
|
||||||
import time
|
import time
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
@@ -26,6 +25,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from .. import cache
|
from .. import cache
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
|
from .sync_utils import d8_iso, f_clean
|
||||||
|
|
||||||
# (tushare代码, 名称, 地区, 腾讯符号) —— 展示顺序即列表顺序
|
# (tushare代码, 名称, 地区, 腾讯符号) —— 展示顺序即列表顺序
|
||||||
# 首页聚焦中美(港股/国际指数在 /indexes 国际指数页);标普500 腾讯符号是 s_usINX(不是 s_usSPX)
|
# 首页聚焦中美(港股/国际指数在 /indexes 国际指数页);标普500 腾讯符号是 s_usINX(不是 s_usSPX)
|
||||||
@@ -57,22 +57,6 @@ class MarketOverviewError(RuntimeError):
|
|||||||
"""所有指数都拉不到(token/网络故障)——接口层转 503。"""
|
"""所有指数都拉不到(token/网络故障)——接口层转 503。"""
|
||||||
|
|
||||||
|
|
||||||
def _f(v) -> float | None:
|
|
||||||
"""pandas 值 -> float;NaN/None -> None(否则 JSON 里会出现 NaN)。"""
|
|
||||||
if v is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
f = float(v)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
return None if math.isnan(f) else f
|
|
||||||
|
|
||||||
|
|
||||||
def _d(v) -> str | None:
|
|
||||||
"""YYYYMMDD -> 'YYYY-MM-DD'(字符串便于 JSON 缓存;pydantic 响应模型自动 coerce)。"""
|
|
||||||
return datetime.strptime(str(v), "%Y%m%d").date().isoformat() if v else None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pro():
|
def _get_pro():
|
||||||
if not settings.tushare_token:
|
if not settings.tushare_token:
|
||||||
raise MarketOverviewError("未配置 TUSHARE_TOKEN,无法获取大盘行情(backend/.env)")
|
raise MarketOverviewError("未配置 TUSHARE_TOKEN,无法获取大盘行情(backend/.env)")
|
||||||
@@ -165,10 +149,10 @@ def _quote_from_df(df: pd.DataFrame) -> dict | None:
|
|||||||
tail = df.tail(_SPARK_DAYS)
|
tail = df.tail(_SPARK_DAYS)
|
||||||
last = df.iloc[-1]
|
last = df.iloc[-1]
|
||||||
return {
|
return {
|
||||||
"close": _f(last["close"]),
|
"close": f_clean(last["close"]),
|
||||||
"change": _f(last.get("change")),
|
"change": f_clean(last.get("change")),
|
||||||
"pct_chg": _f(last.get("pct_chg")),
|
"pct_chg": f_clean(last.get("pct_chg")),
|
||||||
"trade_date": _d(last["trade_date"]),
|
"trade_date": d8_iso(last["trade_date"]),
|
||||||
"spark": [round(float(c), 4) for c in tail["close"]],
|
"spark": [round(float(c), 4) for c in tail["close"]],
|
||||||
"spark_dates": [str(d) for d in tail["trade_date"]],
|
"spark_dates": [str(d) for d in tail["trade_date"]],
|
||||||
}
|
}
|
||||||
@@ -193,10 +177,10 @@ def _fetch_stats_sync(pro) -> dict | None:
|
|||||||
if sh_m is None or sz_m is None:
|
if sh_m is None or sz_m is None:
|
||||||
return None
|
return None
|
||||||
# 两边各自取最新,日期不一致时以较旧一天为准凑齐口径(罕见,通常同日)
|
# 两边各自取最新,日期不一致时以较旧一天为准凑齐口径(罕见,通常同日)
|
||||||
d = min(_d(sh_m["trade_date"]), _d(sz_m["trade_date"]))
|
d = min(d8_iso(sh_m["trade_date"]), d8_iso(sz_m["trade_date"]))
|
||||||
|
|
||||||
def _sum(col: str) -> float | None:
|
def _sum(col: str) -> float | None:
|
||||||
a, b = _f(sh_m.get(col)), _f(sz_m.get(col))
|
a, b = f_clean(sh_m.get(col)), f_clean(sz_m.get(col))
|
||||||
return None if a is None or b is None else round(a + b, 2)
|
return None if a is None or b is None else round(a + b, 2)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -204,7 +188,7 @@ def _fetch_stats_sync(pro) -> dict | None:
|
|||||||
"total_mv": _sum("total_mv"),
|
"total_mv": _sum("total_mv"),
|
||||||
"float_mv": _sum("float_mv"),
|
"float_mv": _sum("float_mv"),
|
||||||
"amount": _sum("amount"),
|
"amount": _sum("amount"),
|
||||||
"turnover": _f(sh_m.get("tr")), # 换手率仅沪市有,展示口径注明沪市
|
"turnover": f_clean(sh_m.get("tr")), # 换手率仅沪市有,展示口径注明沪市
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -222,7 +206,7 @@ def _fetch_amount_history_sync(pro) -> list[dict]:
|
|||||||
if len(common) == 0:
|
if len(common) == 0:
|
||||||
return []
|
return []
|
||||||
total = (sh_m[common] + sz_m[common]).sort_index()
|
total = (sh_m[common] + sz_m[common]).sort_index()
|
||||||
return [{"date": _d(d), "amount": round(float(v), 2)} for d, v in total.tail(_AMOUNT_HIST_BARS).items()]
|
return [{"date": d8_iso(d), "amount": round(float(v), 2)} for d, v in total.tail(_AMOUNT_HIST_BARS).items()]
|
||||||
|
|
||||||
|
|
||||||
# ---- EOD 的 SWR(stale-while-revalidate):新鲜期内直返;过期先返旧值后台刷新 ----
|
# ---- EOD 的 SWR(stale-while-revalidate):新鲜期内直返;过期先返旧值后台刷新 ----
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import calendar
|
import calendar
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -29,6 +30,8 @@ from ..db import async_session
|
|||||||
from ..models import StockReference
|
from ..models import StockReference
|
||||||
from .sync_utils import call_retry, f_clean, fresh, get_pro_lazy, read_sync_state, s_clean, upsert_sync_state, utcnow
|
from .sync_utils import call_retry, f_clean, fresh, get_pro_lazy, read_sync_state, s_clean, upsert_sync_state, utcnow
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
_REFRESH_DAYS = 7
|
_REFRESH_DAYS = 7
|
||||||
|
|
||||||
|
|
||||||
@@ -394,12 +397,12 @@ async def _sync_repurchase_locked(only_current: bool) -> dict[str, list[dict]]:
|
|||||||
|
|
||||||
|
|
||||||
async def _repurchase_backfill() -> None:
|
async def _repurchase_backfill() -> None:
|
||||||
"""后台全量回填(近 24 个月);失败静默——下次触发重试。"""
|
"""后台全量回填(近 24 个月);失败记录日志——下次触发重试。"""
|
||||||
try:
|
try:
|
||||||
async with _repurchase_lock:
|
async with _repurchase_lock:
|
||||||
await _sync_repurchase_locked(only_current=False)
|
await _sync_repurchase_locked(only_current=False)
|
||||||
except Exception: # noqa: BLE001 后台任务无人接异常
|
except Exception: # noqa: BLE001 后台任务无人接异常,至少留痕
|
||||||
pass
|
log.warning("回购数据后台回填失败(下次触发重试)", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
def _spawn_repurchase_backfill() -> None:
|
def _spawn_repurchase_backfill() -> None:
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ def f_clean(v) -> float | None:
|
|||||||
return None if f != f else f # NaN -> None
|
return None if f != f else f # NaN -> None
|
||||||
|
|
||||||
|
|
||||||
|
def d8_iso(v) -> str | None:
|
||||||
|
"""tushare YYYYMMDD -> 'YYYY-MM-DD'(字符串便于 JSON 缓存;pydantic 自动 coerce)。"""
|
||||||
|
return datetime.strptime(str(v), "%Y%m%d").date().isoformat() if v else None
|
||||||
|
|
||||||
|
|
||||||
async def read_sync_state(session: AsyncSession, ts_code: str, kind: str) -> StockSyncState | None:
|
async def read_sync_state(session: AsyncSession, ts_code: str, kind: str) -> StockSyncState | None:
|
||||||
return (await session.execute(
|
return (await session.execute(
|
||||||
select(StockSyncState).where(
|
select(StockSyncState).where(
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""FastAPI 入口。数据库结构统一由 Alembic 管理。"""
|
"""FastAPI 入口。数据库结构统一由 Alembic 管理。"""
|
||||||
from contextlib import asynccontextmanager
|
import asyncio
|
||||||
|
from contextlib import asynccontextmanager, suppress
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from . import cache
|
from . import cache, scheduler
|
||||||
from .api import router
|
from .api import router
|
||||||
from .auth_api import router as auth_router
|
from .auth_api import router as auth_router
|
||||||
from .config import settings
|
from .config import settings
|
||||||
@@ -16,7 +17,11 @@ from .db import engine
|
|||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
async with engine.connect() as conn:
|
async with engine.connect() as conn:
|
||||||
await conn.execute(text("SELECT 1"))
|
await conn.execute(text("SELECT 1"))
|
||||||
|
nightly = asyncio.create_task(scheduler.run_nightly_loop())
|
||||||
yield
|
yield
|
||||||
|
nightly.cancel()
|
||||||
|
with suppress(asyncio.CancelledError):
|
||||||
|
await nightly
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
await cache.aclose() # 释放 Redis 连接池(未启用时是 no-op)
|
await cache.aclose() # 释放 Redis 连接池(未启用时是 no-op)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ Candle 表设计与 TimescaleDB hypertable 完全兼容:将来在目标 PG 库
|
|||||||
SELECT create_hypertable('candles', 'ts');
|
SELECT create_hypertable('candles', 'ts');
|
||||||
即可升级为时序表 + Continuous Aggregates 多周期预聚合,无需改表结构。
|
即可升级为时序表 + Continuous Aggregates 多周期预聚合,无需改表结构。
|
||||||
|
|
||||||
智能选股三表(stock_basic / market_daily / daily_snapshot)与回测 candles(qfq)
|
智能选股直接读 candles 不复权底座(market_daily 已退役);
|
||||||
完全隔离:选股用未复权日线按 trade_date 全市场批量落地,避免污染回测复权缓存。
|
daily_snapshot 存每日指标快照(估值/市值,选股过滤用)。
|
||||||
"""
|
"""
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
@@ -236,29 +236,8 @@ class StockReference(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MarketDaily(Base):
|
# market_daily(全市场未复权日线)已于选股改读 candles 后退役:
|
||||||
"""全市场未复权日线(选股专用,与回测 candles(qfq) 隔离)。
|
# ORM 模型已删,物理表暂留库中作冷备,确认无用后可手动 DROP TABLE market_daily。
|
||||||
|
|
||||||
单位沿用 Tushare 原始:vol 手、amount 千元。
|
|
||||||
"""
|
|
||||||
__tablename__ = "market_daily"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
||||||
trade_date: Mapped[datetime] = mapped_column(DateTime, index=True)
|
|
||||||
ts_code: Mapped[str] = mapped_column(String(12), index=True)
|
|
||||||
open: Mapped[float] = mapped_column(Float)
|
|
||||||
high: Mapped[float] = mapped_column(Float)
|
|
||||||
low: Mapped[float] = mapped_column(Float)
|
|
||||||
close: Mapped[float] = mapped_column(Float)
|
|
||||||
pre_close: Mapped[float] = mapped_column(Float)
|
|
||||||
change: Mapped[float | None] = mapped_column(Float)
|
|
||||||
pct_chg: Mapped[float | None] = mapped_column(Float) # 日涨跌幅 %
|
|
||||||
vol: Mapped[float] = mapped_column(Float) # 手
|
|
||||||
amount: Mapped[float] = mapped_column(Float) # 千元
|
|
||||||
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint("ts_code", "trade_date", name="uq_mkt_code_date"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DailySnapshot(Base):
|
class DailySnapshot(Base):
|
||||||
|
|||||||
@@ -352,6 +352,93 @@ class StockReferenceOut(BaseModel):
|
|||||||
records: list[dict[str, str | float | None]] = Field(default_factory=list)
|
records: list[dict[str, str | float | None]] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 打板专题(主页,同花顺口径) ----------
|
||||||
|
class LimitStockOut(BaseModel):
|
||||||
|
"""涨跌停榜单行:三池共用,涨停池字段最全(炸板/跌停池仅价格类字段)。"""
|
||||||
|
ts_code: str
|
||||||
|
name: str | None = None
|
||||||
|
price: float | None = None # 收盘价(元)
|
||||||
|
pct_chg: float | None = None # 涨跌幅 %
|
||||||
|
tag: str | None = None # 涨停标签:首板 / 2天2板(仅涨停池)
|
||||||
|
status: str | None = None # 涨停状态:一字板 / 换手板 / N连板(仅涨停池)
|
||||||
|
lu_desc: str | None = None # 涨停原因(仅涨停池)
|
||||||
|
open_num: float | None = None # 打开次数
|
||||||
|
limit_amount_yi: float | None = None # 封单额(亿元,仅涨停池)
|
||||||
|
turnover_yi: float | None = None # 成交额(亿元,仅涨停池)
|
||||||
|
first_lu_time: str | None = None # 首次涨停时间
|
||||||
|
last_lu_time: str | None = None # 最后涨停时间(仅炸板池)
|
||||||
|
limit_up_suc_rate: float | None = None # 近一年封板率 %(仅涨停池)
|
||||||
|
|
||||||
|
|
||||||
|
class LimitLadderOut(BaseModel):
|
||||||
|
ts_code: str
|
||||||
|
name: str | None = None
|
||||||
|
nums: int # 连板数
|
||||||
|
|
||||||
|
|
||||||
|
class LimitBlockOut(BaseModel):
|
||||||
|
name: str | None = None # 同花顺概念板块名
|
||||||
|
days: float | None = None # 板块连涨天数
|
||||||
|
up_stat: str | None = None # 如「6天3板」
|
||||||
|
cons_nums: float | None = None # 连板家数
|
||||||
|
up_nums: float | None = None # 涨停家数
|
||||||
|
pct_chg: float | None = None # 板块涨跌 %
|
||||||
|
|
||||||
|
|
||||||
|
class LimitSummaryOut(BaseModel):
|
||||||
|
up_count: int = 0
|
||||||
|
broken_count: int = 0
|
||||||
|
down_count: int = 0
|
||||||
|
first_board_count: int = 0
|
||||||
|
max_ladder: LimitLadderOut | None = None
|
||||||
|
ladder_dist: list[dict[str, int]] = Field(default_factory=list) # [{nums, count}] 升序(2板起)
|
||||||
|
|
||||||
|
|
||||||
|
class LimitBoardResponse(BaseModel):
|
||||||
|
trade_date: str # YYYY-MM-DD
|
||||||
|
updated_at: str
|
||||||
|
summary: LimitSummaryOut
|
||||||
|
up: list[LimitStockOut] = Field(default_factory=list) # 涨停池(按封单额降序)
|
||||||
|
broken: list[LimitStockOut] = Field(default_factory=list) # 炸板池
|
||||||
|
down: list[LimitStockOut] = Field(default_factory=list) # 跌停池
|
||||||
|
ladder: list[LimitLadderOut] = Field(default_factory=list) # 连板天梯(连板数降序)
|
||||||
|
blocks: list[LimitBlockOut] = Field(default_factory=list) # 涨停最强板块
|
||||||
|
errors: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 概念板块(THS:ths_index 列表 + ths_daily 行情 + ths_member 成分) ----------
|
||||||
|
class ThsBoardOut(BaseModel):
|
||||||
|
ts_code: str # 885835.TI / 700001.TI
|
||||||
|
name: str | None = None
|
||||||
|
type: str | None = None # N概念 I行业 TH主题 S特色 R地域 BB宽基 ST风格
|
||||||
|
count: float | None = None # 成分个数
|
||||||
|
list_date: str | None = None # YYYYMMDD
|
||||||
|
close: float | None = None # 板块指数收盘(当日快照)
|
||||||
|
pct_change: float | None = None # 涨跌幅 %
|
||||||
|
vol: float | None = None # 成交量(手)
|
||||||
|
turnover_rate: float | None = None # 换手率 %
|
||||||
|
|
||||||
|
|
||||||
|
class ThsBoardListResponse(BaseModel):
|
||||||
|
trade_date: str | None = None
|
||||||
|
updated_at: str | None = None
|
||||||
|
boards: list[ThsBoardOut] = Field(default_factory=list)
|
||||||
|
errors: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class ThsMemberOut(BaseModel):
|
||||||
|
con_code: str # 成分股代码 000016.SZ
|
||||||
|
con_name: str | None = None
|
||||||
|
close: float | None = None # 现价(candles 最新,北交所等无底座为空)
|
||||||
|
pct_chg: float | None = None # 涨跌幅 %(最新收盘 / 前收 - 1)
|
||||||
|
|
||||||
|
|
||||||
|
class ThsBoardMembersResponse(BaseModel):
|
||||||
|
code: str
|
||||||
|
name: str | None = None
|
||||||
|
members: list[ThsMemberOut] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
# ---------- Auth ----------
|
# ---------- Auth ----------
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
username: str = Field(min_length=1, max_length=64)
|
username: str = Field(min_length=1, max_length=64)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ daily 与 daily_basic 分步独立落库:daily_basic 积分不足时快照仍
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -20,9 +21,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from .. import cache
|
from .. import cache
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..data.symbols import plain_code
|
from ..data.symbols import plain_code
|
||||||
|
from ..data.sync_utils import call_retry, get_pro_lazy
|
||||||
from ..models import AdjFactor, Candle, DailySnapshot, StockBasic, TradeCalendar
|
from ..models import AdjFactor, Candle, DailySnapshot, StockBasic, TradeCalendar
|
||||||
from .llm import ScreenerError
|
from .llm import ScreenerError
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 进程内单例任务状态(uvicorn --reload 单进程场景够用)
|
# 进程内单例任务状态(uvicorn --reload 单进程场景够用)
|
||||||
_sync_state: dict = {
|
_sync_state: dict = {
|
||||||
"running": False,
|
"running": False,
|
||||||
@@ -40,32 +44,6 @@ _BATCH = 5000 # executemany 分批行数
|
|||||||
|
|
||||||
# Tushare 积分/权限不足的特征文案(daily_basic 常见门槛)
|
# Tushare 积分/权限不足的特征文案(daily_basic 常见门槛)
|
||||||
_PERM_MARKS = ("抱歉,您没有访问该项目权限", "积分", "权限")
|
_PERM_MARKS = ("抱歉,您没有访问该项目权限", "积分", "权限")
|
||||||
# 频率超限特征(等待 62s 重试一次)
|
|
||||||
_RATE_MARKS = ("频率超限", "每分钟")
|
|
||||||
|
|
||||||
|
|
||||||
def _call_retry(fn, *args, **kwargs):
|
|
||||||
"""同步调用 tushare 接口;「每分钟」级频率超限等 62s 重试一次(小时级限频直接抛)。"""
|
|
||||||
try:
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
msg = str(e)
|
|
||||||
if any(m in msg for m in _RATE_MARKS) and "小时" not in msg:
|
|
||||||
time.sleep(62)
|
|
||||||
return fn(*args, **kwargs)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pro():
|
|
||||||
"""token 检查 + 返回 pro api 客户端(同步对象,调用需 to_thread 包裹)。
|
|
||||||
|
|
||||||
经 tushare_provider.get_pro 统一走镜像补丁(15000 积分档 token 只认 quicksync)。
|
|
||||||
"""
|
|
||||||
if not settings.tushare_token:
|
|
||||||
raise ScreenerError("未配置 TUSHARE_TOKEN,无法同步全市场数据(backend/.env)")
|
|
||||||
from ..data.tushare_provider import get_pro
|
|
||||||
|
|
||||||
return get_pro()
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_d(s: str) -> datetime:
|
def _parse_d(s: str) -> datetime:
|
||||||
@@ -77,7 +55,7 @@ def _fetch_calendar_sync(pro) -> list[str]:
|
|||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
end = (datetime.now() + timedelta(days=90)).strftime("%Y%m%d")
|
end = (datetime.now() + timedelta(days=90)).strftime("%Y%m%d")
|
||||||
start = (datetime.now() - timedelta(days=550)).strftime("%Y%m%d")
|
start = (datetime.now() - timedelta(days=550)).strftime("%Y%m%d")
|
||||||
cal = _call_retry(pro.trade_cal, exchange="SSE", start_date=start, end_date=end, is_open="1")
|
cal = call_retry(pro.trade_cal, exchange="SSE", start_date=start, end_date=end, is_open="1")
|
||||||
return sorted(cal["cal_date"].tolist())
|
return sorted(cal["cal_date"].tolist())
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +90,7 @@ async def _recent_trade_dates(session: AsyncSession, pro, days: int) -> list[str
|
|||||||
def _fetch_daily(pro, d: str) -> list[dict]:
|
def _fetch_daily(pro, d: str) -> list[dict]:
|
||||||
"""拉取某交易日全市场日线(未复权)。当日数据未生成(盘前/盘中)返回空。"""
|
"""拉取某交易日全市场日线(未复权)。当日数据未生成(盘前/盘中)返回空。"""
|
||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
df = _call_retry(pro.daily, trade_date=d)
|
df = call_retry(pro.daily, trade_date=d)
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return []
|
return []
|
||||||
rows = []
|
rows = []
|
||||||
@@ -138,7 +116,7 @@ def _fetch_basic(pro, d: str) -> list[dict]:
|
|||||||
"""
|
"""
|
||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
try:
|
try:
|
||||||
df = _call_retry(pro.daily_basic, trade_date=d)
|
df = call_retry(pro.daily_basic, trade_date=d)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
msg = str(e)
|
msg = str(e)
|
||||||
if any(m in msg for m in _PERM_MARKS):
|
if any(m in msg for m in _PERM_MARKS):
|
||||||
@@ -169,7 +147,7 @@ def _fetch_basic(pro, d: str) -> list[dict]:
|
|||||||
def _fetch_adj_factor(pro, d: str) -> list[dict]:
|
def _fetch_adj_factor(pro, d: str) -> list[dict]:
|
||||||
"""拉取某交易日全市场复权因子(K线 bfq->qfq/hfq 本地换算的底座)。"""
|
"""拉取某交易日全市场复权因子(K线 bfq->qfq/hfq 本地换算的底座)。"""
|
||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
df = _call_retry(pro.adj_factor, trade_date=d)
|
df = call_retry(pro.adj_factor, trade_date=d)
|
||||||
if df is None or df.empty:
|
if df is None or df.empty:
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
@@ -181,7 +159,7 @@ def _fetch_adj_factor(pro, d: str) -> list[dict]:
|
|||||||
def _sync_stock_list_sync(pro) -> list[dict]:
|
def _sync_stock_list_sync(pro) -> list[dict]:
|
||||||
"""拉取在市股票列表。"""
|
"""拉取在市股票列表。"""
|
||||||
time.sleep(settings.screener_sync_interval)
|
time.sleep(settings.screener_sync_interval)
|
||||||
df = _call_retry(pro.stock_basic, exchange="", list_status="L",
|
df = call_retry(pro.stock_basic, exchange="", list_status="L",
|
||||||
fields="ts_code,symbol,name,area,industry,market,exchange,list_status,list_date,delist_date")
|
fields="ts_code,symbol,name,area,industry,market,exchange,list_status,list_date,delist_date")
|
||||||
rows = []
|
rows = []
|
||||||
for _, r in df.iterrows():
|
for _, r in df.iterrows():
|
||||||
@@ -232,6 +210,27 @@ async def _existing_candle_dates(session: AsyncSession) -> set[str]:
|
|||||||
return {r[0].strftime("%Y%m%d") for r in res if r[0] is not None}
|
return {r[0].strftime("%Y%m%d") for r in res if r[0] is not None}
|
||||||
|
|
||||||
|
|
||||||
|
_UPSERT_CHUNK = 3000 # 单语句行数(asyncpg 参数上限拆批)
|
||||||
|
|
||||||
|
|
||||||
|
async def _recent_day_counts(session: AsyncSession, dates: list[str]) -> dict[str, int]:
|
||||||
|
"""指定交易日在市股票的 candles 行数(半日数据自愈用)。
|
||||||
|
|
||||||
|
单条 GROUP BY 走 ts 索引范围扫,窗口 ≤15 日、代价可忽略。
|
||||||
|
"""
|
||||||
|
if not dates:
|
||||||
|
return {}
|
||||||
|
lo = _parse_d(min(dates))
|
||||||
|
hi = _parse_d(max(dates)) + timedelta(days=1)
|
||||||
|
rows = (await session.execute(
|
||||||
|
select(func.date(Candle.ts), func.count())
|
||||||
|
.where(Candle.timeframe == "1d", Candle.ts >= lo, Candle.ts < hi,
|
||||||
|
Candle.symbol.in_(select(StockBasic.symbol).where(StockBasic.list_status == "L")))
|
||||||
|
.group_by(func.date(Candle.ts))
|
||||||
|
)).all()
|
||||||
|
return {r[0].strftime("%Y%m%d"): int(r[1]) for r in rows if r[0] is not None}
|
||||||
|
|
||||||
|
|
||||||
async def _upsert_candle_day(session: AsyncSession, rows: list[dict], listed: set[str], d_str: str) -> None:
|
async def _upsert_candle_day(session: AsyncSession, rows: list[dict], listed: set[str], d_str: str) -> None:
|
||||||
"""把某交易日全市场日线 upsert 进 candles(不复权底座,幂等)。
|
"""把某交易日全市场日线 upsert 进 candles(不复权底座,幂等)。
|
||||||
|
|
||||||
@@ -253,9 +252,10 @@ async def _upsert_candle_day(session: AsyncSession, rows: list[dict], listed: se
|
|||||||
if not batch:
|
if not batch:
|
||||||
return
|
return
|
||||||
# on_conflict 语句整批渲染为占位符(非 executemany),asyncpg 单语句参数上限 32766,
|
# on_conflict 语句整批渲染为占位符(非 executemany),asyncpg 单语句参数上限 32766,
|
||||||
# 10 列 x 3000 行 = 30000 参数留出余量
|
# 10 列 x 3000 行 = 30000 参数留出余量。分批只拆语句,commit 在循环外 ——
|
||||||
for i in range(0, len(batch), 3000):
|
# 单日一事务:写一半崩溃整日回滚,该日期语义上「未同步」,下次自然重拉(不留半日数据)
|
||||||
stmt = pg_insert(Candle).values(batch[i : i + 3000])
|
for i in range(0, len(batch), _UPSERT_CHUNK):
|
||||||
|
stmt = pg_insert(Candle).values(batch[i : i + _UPSERT_CHUNK])
|
||||||
stmt = stmt.on_conflict_do_update(
|
stmt = stmt.on_conflict_do_update(
|
||||||
index_elements=["symbol", "timeframe", "ts"],
|
index_elements=["symbol", "timeframe", "ts"],
|
||||||
set_={
|
set_={
|
||||||
@@ -266,7 +266,7 @@ async def _upsert_candle_day(session: AsyncSession, rows: list[dict], listed: se
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
await session.execute(stmt)
|
await session.execute(stmt)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def _run_sync(days: int, force: bool) -> None:
|
async def _run_sync(days: int, force: bool) -> None:
|
||||||
@@ -277,7 +277,7 @@ async def _run_sync(days: int, force: bool) -> None:
|
|||||||
from ..db import async_session # 延迟导入避免循环
|
from ..db import async_session # 延迟导入避免循环
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pro = await asyncio.to_thread(_get_pro)
|
pro = await asyncio.to_thread(get_pro_lazy)
|
||||||
|
|
||||||
# 1) 股票列表(已有数据则跳过——stock_basic 低积分版限频 1 次/小时)
|
# 1) 股票列表(已有数据则跳过——stock_basic 低积分版限频 1 次/小时)
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
@@ -307,6 +307,15 @@ async def _run_sync(days: int, force: bool) -> None:
|
|||||||
select(StockBasic.symbol).where(StockBasic.list_status == "L")
|
select(StockBasic.symbol).where(StockBasic.list_status == "L")
|
||||||
)).scalars()
|
)).scalars()
|
||||||
)
|
)
|
||||||
|
if not force:
|
||||||
|
# 半日数据自愈(修单日原子化之前的历史残留):写入中途崩溃的日期
|
||||||
|
# 行数 ≈ 1 批(3000),显著低于完整日(~5300)。只查最近 15 个交易日
|
||||||
|
# ——实际风险区且窗口内上市数变化 <2%,0.7 阈值安全;更老的日期不查
|
||||||
|
# (上市数变化会误判,历史缺口本就由 TDX 底座兜底)。
|
||||||
|
counts = await _recent_day_counts(session, [d for d in dates[:15] if d in have_daily])
|
||||||
|
if counts:
|
||||||
|
floor = max(_UPSERT_CHUNK + 1, int(max(counts.values()) * 0.7))
|
||||||
|
have_daily -= {d for d, n in counts.items() if n < floor}
|
||||||
todo = [d for d in dates if d not in have_daily]
|
todo = [d for d in dates if d not in have_daily]
|
||||||
_sync_state["total_days"] = len(todo)
|
_sync_state["total_days"] = len(todo)
|
||||||
_sync_state["done_days"] = 0
|
_sync_state["done_days"] = 0
|
||||||
@@ -352,7 +361,7 @@ async def _run_sync(days: int, force: bool) -> None:
|
|||||||
try:
|
try:
|
||||||
await _refresh_stats(await cache.get_version("candles"))
|
await _refresh_stats(await cache.get_version("candles"))
|
||||||
except Exception: # noqa: BLE001 —— 预热失败只影响统计数字的新鲜度
|
except Exception: # noqa: BLE001 —— 预热失败只影响统计数字的新鲜度
|
||||||
pass
|
log.warning("统计缓存预热失败(下轮轮询会 SWR 重算)", exc_info=True)
|
||||||
_sync_state["step"] = "同步完成"
|
_sync_state["step"] = "同步完成"
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
_sync_state["error"] = f"同步失败:{str(e)[:300]}"
|
_sync_state["error"] = f"同步失败:{str(e)[:300]}"
|
||||||
@@ -414,16 +423,9 @@ async def _heavy_stats(session: AsyncSession) -> dict:
|
|||||||
|
|
||||||
async def _store_stats(ver: int, data: dict) -> None:
|
async def _store_stats(ver: int, data: dict) -> None:
|
||||||
_status_stats_cache.update(at=time.time(), ver=ver, data=data)
|
_status_stats_cache.update(at=time.time(), ver=ver, data=data)
|
||||||
# 写 Redis 后台执行,失败由 cache 层静默降级,不拖慢调用方
|
# 写 Redis 后台执行(cache.set_bg 挂全局集合防 GC),失败由 cache 层静默降级
|
||||||
tasks = [
|
cache.set_bg(f"syncstats:v{ver}", data, ttl=settings.sync_stats_redis_ttl)
|
||||||
asyncio.create_task(cache.cache_set(
|
cache.set_bg(_STATS_LAST_KEY, data, ttl=settings.sync_stats_redis_ttl)
|
||||||
f"syncstats:v{ver}", data, ttl=settings.sync_stats_redis_ttl)),
|
|
||||||
asyncio.create_task(cache.cache_set(
|
|
||||||
_STATS_LAST_KEY, data, ttl=settings.sync_stats_redis_ttl)),
|
|
||||||
]
|
|
||||||
_stats_bg_tasks.update(tasks)
|
|
||||||
for t in tasks:
|
|
||||||
t.add_done_callback(_stats_bg_tasks.discard)
|
|
||||||
|
|
||||||
|
|
||||||
async def _refresh_stats(ver: int) -> None:
|
async def _refresh_stats(ver: int) -> None:
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ dependencies = [
|
|||||||
"openpyxl>=3.1.5",
|
"openpyxl>=3.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pytest>=8"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
# 应用型项目(非库):不把自身打包安装,只管理依赖到 .venv
|
# 应用型项目(非库):不把自身打包安装,只管理依赖到 .venv
|
||||||
package = false
|
package = false
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
"""交割单解析器离线自测:不碰数据库,直接调 app.trades.parse_statement。
|
|
||||||
|
|
||||||
覆盖四类真实导出格式 + 边界行(转账/配号/利息跳过、费用合计列去重、日期多格式)。
|
|
||||||
运行:uv run python scripts/test_trades_parser.py
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
||||||
|
|
||||||
from app.trades import parse_statement # noqa: E402
|
|
||||||
|
|
||||||
FAIL: list[str] = []
|
|
||||||
|
|
||||||
|
|
||||||
def check(name: str, cond: bool, detail: str = "") -> None:
|
|
||||||
mark = "ok " if cond else "FAIL"
|
|
||||||
print(f"[{mark}] {name}{(' — ' + detail) if detail and not cond else ''}")
|
|
||||||
if not cond:
|
|
||||||
FAIL.append(name)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- 1) 通达信式:GBK + 制表符 + 标题行在前 ----------
|
|
||||||
tdx = (
|
|
||||||
"交割单\n"
|
|
||||||
"股东账号: A123456789 起始日期: 20240102 终止日期: 20240105 币种: 人民币\n"
|
|
||||||
"\t交割日期\t业务名称\t证券代码\t证券名称\t成交价格\t成交数量\t成交金额\t手续费\t印花税\t过户费\t发生金额\t资金余额\t合同号\n"
|
|
||||||
"\t20240102\t证券买入\t600519\t贵州茅台\t1680.00\t100\t168000.00\t5.00\t0.00\t1.68\t-168006.68\t200000.00\t1000001\n"
|
|
||||||
"\t20240102\t银行转存\t\t\t\t\t\t\t\t\t50000.00\t250000.00\t\n"
|
|
||||||
"\t20240103\t证券卖出\t600519\t贵州茅台\t1700.50\t100\t170050.00\t5.00\t170.05\t1.70\t169873.25\t419873.25\t1000002\n"
|
|
||||||
"\t20240105\t利息归本\t\t\t\t\t\t\t\t\t1.25\t419874.50\t\n"
|
|
||||||
)
|
|
||||||
r = parse_statement(tdx.encode("gbk"), "交割单.txt")
|
|
||||||
check("tdx: 2 笔成交", len(r.trades) == 2, f"got {len(r.trades)}")
|
|
||||||
check("tdx: 跳过 2 行非交易", r.skipped_other == 2, f"got {r.skipped_other}")
|
|
||||||
t0, t1 = r.trades[0], r.trades[1]
|
|
||||||
check("tdx: 日期/代码/后缀", (t0.trade_date.isoformat(), t0.ts_code) == ("2024-01-02", "600519.SH"), f"{t0.trade_date} {t0.ts_code}")
|
|
||||||
check("tdx: 买入方向+费用合计", t0.direction == "buy" and abs(t0.fee - 6.68) < 1e-9, f"{t0.direction} fee={t0.fee}")
|
|
||||||
check("tdx: 卖出费用含印花税", t1.direction == "sell" and abs(t1.fee - 176.75) < 1e-9, f"fee={t1.fee}")
|
|
||||||
check("tdx: 金额取绝对值", t0.amount == 168000.0, f"amount={t0.amount}")
|
|
||||||
|
|
||||||
# ---------- 2) 恒生柜台式:UTF-8 CSV,交收日期/交易类别/费用合计 ----------
|
|
||||||
hs = (
|
|
||||||
"序号,交收日期,证券代码,证券名称,交易类别,成交价格,成交数量,证券余额,成交金额,资金发生数,资金余额,流水序号,业务标志,业务名称,发生金额,后资金额,货币类别,费用合计,净佣金,规费,印花税,过户费,合同号\n"
|
|
||||||
"1,2024-06-07,000858,五粮液,证券买入,132.50,200,200,26500.00,-26505.80,73494.20,1,0101,证券买入,-26505.80,73494.20,人民币,5.80,4.20,1.60,0.00,0.00,66778001\n"
|
|
||||||
"2,2024-06-07,,,\t,,,,5120.00,78614.20,2,2041,银行转存,5120.00,78614.20,人民币,0,0,0,0,0,\n"
|
|
||||||
"3,2024-06-10,000858,五粮液,证券卖出,135.00,200,0,27000.00,26975.30,105589.50,3,0102,证券卖出,26975.30,105589.50,人民币,24.70,4.20,1.60,18.90,0.00,66779001\n"
|
|
||||||
)
|
|
||||||
r2 = parse_statement(hs.encode("utf-8"), "hsi.csv")
|
|
||||||
check("hs: 2 笔成交", len(r2.trades) == 2, f"got {len(r2.trades)}")
|
|
||||||
check("hs: 费用合计不重复累加", abs(r2.trades[1].fee - 24.70) < 1e-9, f"fee={r2.trades[1].fee}")
|
|
||||||
check("hs: 深市后缀", r2.trades[0].ts_code == "000858.SZ", r2.trades[0].ts_code)
|
|
||||||
check("hs: 日期 YYYY-MM-DD", r2.trades[0].trade_date.isoformat() == "2024-06-07")
|
|
||||||
|
|
||||||
# ---------- 3) HTML 伪 .xls(同花顺导出常见真身) ----------
|
|
||||||
html = """<html><head><meta charset="gbk"></head><body>
|
|
||||||
<table>
|
|
||||||
<tr><td>客户姓名</td><td>测试</td></tr>
|
|
||||||
<tr><td>成交日期</td><td>业务名称</td><td>证券代码</td><td>证券名称</td><td>成交价格</td><td>成交数量</td><td>成交金额</td><td>手续费</td></tr>
|
|
||||||
<tr><td>2024/03/15</td><td>证券买入</td><td>300750</td><td>宁德时代</td><td>182.30</td><td>300</td><td>54,690.00</td><td>16.41</td></tr>
|
|
||||||
<tr><td>2024/03/18</td><td>证券卖出</td><td>300750</td><td>宁德时代</td><td>185.00</td><td>300</td><td>55,500.00</td><td>5.55</td></tr>
|
|
||||||
</table></body></html>"""
|
|
||||||
r3 = parse_statement(html.encode("gbk"), "jiaogedan.xls")
|
|
||||||
check("html: 2 笔成交", len(r3.trades) == 2, f"got {len(r3.trades)}")
|
|
||||||
check("html: 千分位金额", r3.trades[0].amount == 54690.0, f"{r3.trades[0].amount}")
|
|
||||||
check("html: 创业板后缀", r3.trades[0].ts_code == "300750.SZ", r3.trades[0].ts_code)
|
|
||||||
check("html: 斜杠日期", r3.trades[1].trade_date.isoformat() == "2024-03-18")
|
|
||||||
|
|
||||||
# ---------- 4) 无业务名称列:发生金额正负判方向(招商式) ----------
|
|
||||||
zh = (
|
|
||||||
"证券名称,成交日期,成交价格,成交数量,发生金额,资金余额,合同编号\n"
|
|
||||||
"贵州茅台,20240102,1680.00,100,-168005.00,200000.00,SZ1000001\n"
|
|
||||||
"贵州茅台,20240103,1700.50,100,170049.50,370049.50,SZ1000002\n"
|
|
||||||
)
|
|
||||||
r4 = parse_statement(zh.encode("utf-8"), "zszs.csv")
|
|
||||||
check("sign: 2 笔成交", len(r4.trades) == 2, f"got {len(r4.trades)}")
|
|
||||||
check("sign: 负金额=买入", (r4.trades[0].direction, r4.trades[1].direction) == ("buy", "sell"),
|
|
||||||
f"{r4.trades[0].direction}/{r4.trades[1].direction}")
|
|
||||||
|
|
||||||
# ---------- 5) xlsx(openpyxl 内存构造) ----------
|
|
||||||
import io # noqa: E402
|
|
||||||
from openpyxl import Workbook # noqa: E402
|
|
||||||
|
|
||||||
wb = Workbook()
|
|
||||||
ws = wb.active
|
|
||||||
ws.append(["对账单", None, None])
|
|
||||||
ws.append(["成交日期", "业务名称", "证券代码", "证券名称", "成交均价", "成交股数", "成交金额", "佣金", "过户费"])
|
|
||||||
from datetime import datetime as dt # noqa: E402
|
|
||||||
ws.append([dt(2024, 2, 28, 14, 35, 0), "证券买入", "688981", "中芯国际", 52.80, 200, 10560.00, 2.50, 1.06])
|
|
||||||
ws.append([dt(2024, 3, 1, 9, 31, 0), "证券卖出", "688981", "中芯国际", 54.10, 200, 10820.00, 2.50, 1.06])
|
|
||||||
buf = io.BytesIO()
|
|
||||||
wb.save(buf)
|
|
||||||
r5 = parse_statement(buf.getvalue(), "sm.xlsx")
|
|
||||||
check("xlsx: 2 笔成交", len(r5.trades) == 2, f"got {len(r5.trades)}")
|
|
||||||
check("xlsx: datetime 日期", r5.trades[0].trade_date.isoformat() == "2024-02-28")
|
|
||||||
check("xlsx: 科创板后缀", r5.trades[0].ts_code == "688981.SH", r5.trades[0].ts_code)
|
|
||||||
check("xlsx: 佣金+过户费", abs(r5.trades[0].fee - 3.56) < 1e-9, f"fee={r5.trades[0].fee}")
|
|
||||||
|
|
||||||
# ---------- 6) 错误分支 ----------
|
|
||||||
from fastapi import HTTPException # noqa: E402
|
|
||||||
try:
|
|
||||||
parse_statement("随便一串不是交割单的文字,1,2,3".encode("utf-8"), "x.csv")
|
|
||||||
check("garbage: 应 422", False)
|
|
||||||
except HTTPException as e:
|
|
||||||
check("garbage: 422", e.status_code == 422)
|
|
||||||
|
|
||||||
print()
|
|
||||||
if FAIL:
|
|
||||||
print(f"FAIL {len(FAIL)}: {FAIL}")
|
|
||||||
sys.exit(1)
|
|
||||||
print("PASS: 交割单解析器全部用例通过")
|
|
||||||
60
backend/uv.lock
generated
60
backend/uv.lock
generated
@@ -501,6 +501,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lxml"
|
name = "lxml"
|
||||||
version = "6.1.1"
|
version = "6.1.1"
|
||||||
@@ -719,6 +728,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pandas"
|
name = "pandas"
|
||||||
version = "3.0.5"
|
version = "3.0.5"
|
||||||
@@ -765,6 +783,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "3.0"
|
version = "3.0"
|
||||||
@@ -878,6 +905,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
|
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@@ -1125,6 +1177,11 @@ dependencies = [
|
|||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "alembic", specifier = ">=1.19.1" },
|
{ name = "alembic", specifier = ">=1.19.1" },
|
||||||
@@ -1144,6 +1201,9 @@ requires-dist = [
|
|||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.30" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "pytest", specifier = ">=8" }]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tqdm"
|
name = "tqdm"
|
||||||
version = "4.70.0"
|
version = "4.70.0"
|
||||||
|
|||||||
862
backend_run.log
862
backend_run.log
@@ -1,862 +0,0 @@
|
|||||||
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 [28028] using WatchFiles
|
|
||||||
INFO: Started server process [16456]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: 127.0.0.1:44639 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44642 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44662 - "GET /api/stocks/600519.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44669 - "GET /api/stocks/600519.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44671 - "GET /api/stocks/510300.SH/company HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:44673 - "GET /api/stocks/999999.SZ/company HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:44683 - "GET /api/stocks/000002/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44686 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 401 Unauthorized
|
|
||||||
INFO: 127.0.0.1:44701 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44707 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44704 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44700 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:44703 - "GET /api/stocks/601318.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45455 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45461 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45464 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45463 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45472 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45469 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45471 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45476 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:45474 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
WARNING: WatchFiles detected changes in 'app\data\index_global.py'. Reloading...
|
|
||||||
INFO: 127.0.0.1:46980 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46985 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46982 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46984 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46990 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46992 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46991 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46998 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:46996 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47008 - "GET /openapi.json HTTP/1.1" 200 OK
|
|
||||||
INFO: Will watch for changes in these directories: ['D:\\Project\\stock\\backend']
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: Started reloader process [29204] using WatchFiles
|
|
||||||
INFO: Started server process [28644]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: 127.0.0.1:47041 - "GET /openapi.json HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47042 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
WARNING: WatchFiles detected changes in 'scripts\_ui_drive_tmp.py'. Reloading...
|
|
||||||
INFO: 127.0.0.1:47137 - "POST /api/auth/login HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47183 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47185 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47188 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47194 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47206 - "PUT /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47202 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47227 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47233 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47235 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47269 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47272 - "GET /api/market/indexes/SPX HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47270 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47278 - "GET /api/market/indexes/SPX/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47193 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47307 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47308 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47310 - "GET /api/market/indexes/000300.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47319 - "GET /api/market/indexes/000300.SH/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:47317 - "GET /api/market/indexes/000300.SH/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48863 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
|
||||||
INFO: 127.0.0.1:48877 - "POST /api/auth/login HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48878 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48880 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48879 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48886 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48881 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48895 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48904 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48912 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:48917 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49244 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49252 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49254 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49253 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49260 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49258 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49259 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49264 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49262 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49269 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49266 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49273 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49270 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49365 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49369 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49367 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49374 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49378 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49380 - "GET /api/market/indexes/399001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49388 - "GET /api/market/indexes/399001.SZ/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49382 - "GET /api/market/indexes/399001.SZ/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49393 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49395 - "GET /api/market/indexes/IXIC HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49397 - "GET /api/market/indexes/IXIC/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49408 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49410 - "GET /api/market/indexes/RUT HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49412 - "GET /api/market/indexes/RUT/candles?timeframe=1d HTTP/1.1" 502 Bad Gateway
|
|
||||||
INFO: 127.0.0.1:49425 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49427 - "GET /api/market/indexes/SPTSX HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49429 - "GET /api/market/indexes/SPTSX/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49437 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49441 - "GET /api/market/indexes/IBOVESPA HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49442 - "GET /api/market/indexes/IBOVESPA/candles?timeframe=1d HTTP/1.1" 502 Bad Gateway
|
|
||||||
INFO: 127.0.0.1:49447 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49455 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:49457 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64126 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64123 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64132 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64131 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64155 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64149 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64124 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64374 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64377 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64376 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64375 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64380 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64395 - "GET /api/market/indexes/SPTSX HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64397 - "GET /api/market/indexes/SPTSX/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64459 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64464 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64461 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64469 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64465 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64482 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64489 - "GET /api/market/indexes/KS11/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64487 - "GET /api/market/indexes/KS11 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64497 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64494 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64500 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64498 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64507 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64506 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64517 - "GET /api/trades?ts_code=000010.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64519 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64518 - "GET /api/screener/preview/000010.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64521 - "GET /api/screener/preview/000010.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-13 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64523 - "GET /api/stocks/000010.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64558 - "GET /api/screener/preview/000010.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-13 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64560 - "PUT /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64562 - "GET /api/screener/preview/000010.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-13 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64564 - "PUT /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64802 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64799 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64808 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64803 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64816 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64817 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64818 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64825 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64828 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64826 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64865 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64871 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64869 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64876 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64917 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64918 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64914 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64921 - "GET /api/market/index-candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64940 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64942 - "GET /api/market/indexes/DJI HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64944 - "GET /api/market/indexes/DJI/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64951 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64956 - "GET /api/market/indexes/000001.SH/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64954 - "GET /api/market/indexes/000001.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64961 - "GET /api/market/indexes/000001.SH/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64972 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64976 - "GET /api/market/indexes/399001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64984 - "GET /api/market/indexes/399001.SZ/weights?limit=50 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64978 - "GET /api/market/indexes/399001.SZ/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:64990 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10165 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10163 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10167 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10252 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10249 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10253 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10732 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10731 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10733 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10746 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10751 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10759 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10760 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10764 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10770 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:10768 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: Started server process [25400]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:12480 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12505 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12524 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 500 Internal Server Error
|
|
||||||
ERROR: Exception in ASGI application
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 422, in run_asgi
|
|
||||||
result = await app( # type: ignore[func-returns-value]
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
self.scope, self.receive, self.send
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
)
|
|
||||||
^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 63, in __call__
|
|
||||||
return await self.app(scope, receive, send)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\applications.py", line 1163, in __call__
|
|
||||||
await super().__call__(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\applications.py", line 90, in __call__
|
|
||||||
await self.middleware_stack(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__
|
|
||||||
await self.app(scope, receive, _send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__
|
|
||||||
await self.app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__
|
|
||||||
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
|
|
||||||
await app(scope, receive, sender)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
|
|
||||||
await self.app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__
|
|
||||||
await self.middleware_stack(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2734, in app
|
|
||||||
await route.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1780, in handle
|
|
||||||
await self.original_router.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2789, in handle
|
|
||||||
await included_router._handle_selected(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1800, in _handle_selected
|
|
||||||
await original_route.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1279, in handle
|
|
||||||
await app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 158, in app
|
|
||||||
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
|
|
||||||
await app(scope, receive, sender)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 144, in app
|
|
||||||
response = await f(request)
|
|
||||||
^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 481, in app
|
|
||||||
solved_result = await solve_dependencies(
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
...<6 lines>...
|
|
||||||
)
|
|
||||||
^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\dependencies\utils.py", line 674, in solve_dependencies
|
|
||||||
solved = await call(**solved_result.values)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\app\auth.py", line 127, in require_user
|
|
||||||
if expires_mono > time.monotonic() and sess_expires > utcnow() and user.is_active:
|
|
||||||
^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 569, in __get__
|
|
||||||
return self.impl.get(state, dict_) # type: ignore[no-any-return]
|
|
||||||
~~~~~~~~~~~~~^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1096, in get
|
|
||||||
value = self._fire_loader_callables(state, key, passive)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1126, in _fire_loader_callables
|
|
||||||
return state._load_expired(state, passive)
|
|
||||||
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\state.py", line 828, in _load_expired
|
|
||||||
self.manager.expired_attribute_loader(self, toload, passive)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\loading.py", line 1607, in load_scalar_attributes
|
|
||||||
raise orm_exc.DetachedInstanceError(
|
|
||||||
...<2 lines>...
|
|
||||||
)
|
|
||||||
sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x21243a634d0> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)
|
|
||||||
INFO: 127.0.0.1:12525 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 500 Internal Server Error
|
|
||||||
ERROR: Exception in ASGI application
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 422, in run_asgi
|
|
||||||
result = await app( # type: ignore[func-returns-value]
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
self.scope, self.receive, self.send
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
)
|
|
||||||
^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 63, in __call__
|
|
||||||
return await self.app(scope, receive, send)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\applications.py", line 1163, in __call__
|
|
||||||
await super().__call__(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\applications.py", line 90, in __call__
|
|
||||||
await self.middleware_stack(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__
|
|
||||||
await self.app(scope, receive, _send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__
|
|
||||||
await self.app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__
|
|
||||||
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
|
|
||||||
await app(scope, receive, sender)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
|
|
||||||
await self.app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__
|
|
||||||
await self.middleware_stack(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2734, in app
|
|
||||||
await route.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1780, in handle
|
|
||||||
await self.original_router.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2789, in handle
|
|
||||||
await included_router._handle_selected(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1800, in _handle_selected
|
|
||||||
await original_route.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1279, in handle
|
|
||||||
await app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 158, in app
|
|
||||||
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
|
|
||||||
await app(scope, receive, sender)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 144, in app
|
|
||||||
response = await f(request)
|
|
||||||
^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 481, in app
|
|
||||||
solved_result = await solve_dependencies(
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
...<6 lines>...
|
|
||||||
)
|
|
||||||
^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\dependencies\utils.py", line 674, in solve_dependencies
|
|
||||||
solved = await call(**solved_result.values)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\app\auth.py", line 127, in require_user
|
|
||||||
if expires_mono > time.monotonic() and sess_expires > utcnow() and user.is_active:
|
|
||||||
^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 569, in __get__
|
|
||||||
return self.impl.get(state, dict_) # type: ignore[no-any-return]
|
|
||||||
~~~~~~~~~~~~~^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1096, in get
|
|
||||||
value = self._fire_loader_callables(state, key, passive)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1126, in _fire_loader_callables
|
|
||||||
return state._load_expired(state, passive)
|
|
||||||
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\state.py", line 828, in _load_expired
|
|
||||||
self.manager.expired_attribute_loader(self, toload, passive)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\loading.py", line 1607, in load_scalar_attributes
|
|
||||||
raise orm_exc.DetachedInstanceError(
|
|
||||||
...<2 lines>...
|
|
||||||
)
|
|
||||||
sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x21243a634d0> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)
|
|
||||||
INFO: 127.0.0.1:12530 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 500 Internal Server Error
|
|
||||||
ERROR: Exception in ASGI application
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 422, in run_asgi
|
|
||||||
result = await app( # type: ignore[func-returns-value]
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
self.scope, self.receive, self.send
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
)
|
|
||||||
^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 63, in __call__
|
|
||||||
return await self.app(scope, receive, send)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\applications.py", line 1163, in __call__
|
|
||||||
await super().__call__(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\applications.py", line 90, in __call__
|
|
||||||
await self.middleware_stack(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 186, in __call__
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\errors.py", line 164, in __call__
|
|
||||||
await self.app(scope, receive, _send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\cors.py", line 88, in __call__
|
|
||||||
await self.app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\middleware\exceptions.py", line 63, in __call__
|
|
||||||
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
|
|
||||||
await app(scope, receive, sender)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__
|
|
||||||
await self.app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\routing.py", line 660, in __call__
|
|
||||||
await self.middleware_stack(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2734, in app
|
|
||||||
await route.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1780, in handle
|
|
||||||
await self.original_router.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 2789, in handle
|
|
||||||
await included_router._handle_selected(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1800, in _handle_selected
|
|
||||||
await original_route.handle(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 1279, in handle
|
|
||||||
await app(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 158, in app
|
|
||||||
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 53, in wrapped_app
|
|
||||||
raise exc
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\starlette\_exception_handler.py", line 42, in wrapped_app
|
|
||||||
await app(scope, receive, sender)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 144, in app
|
|
||||||
response = await f(request)
|
|
||||||
^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\routing.py", line 481, in app
|
|
||||||
solved_result = await solve_dependencies(
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
...<6 lines>...
|
|
||||||
)
|
|
||||||
^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\fastapi\dependencies\utils.py", line 674, in solve_dependencies
|
|
||||||
solved = await call(**solved_result.values)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\app\auth.py", line 127, in require_user
|
|
||||||
if expires_mono > time.monotonic() and sess_expires > utcnow() and user.is_active:
|
|
||||||
^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 569, in __get__
|
|
||||||
return self.impl.get(state, dict_) # type: ignore[no-any-return]
|
|
||||||
~~~~~~~~~~~~~^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1096, in get
|
|
||||||
value = self._fire_loader_callables(state, key, passive)
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1126, in _fire_loader_callables
|
|
||||||
return state._load_expired(state, passive)
|
|
||||||
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\state.py", line 828, in _load_expired
|
|
||||||
self.manager.expired_attribute_loader(self, toload, passive)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\Project\stock\backend\.venv\Lib\site-packages\sqlalchemy\orm\loading.py", line 1607, in load_scalar_attributes
|
|
||||||
raise orm_exc.DetachedInstanceError(
|
|
||||||
...<2 lines>...
|
|
||||||
)
|
|
||||||
sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x21243a634d0> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)
|
|
||||||
INFO: 127.0.0.1:12740 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12741 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12747 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12748 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12750 - "GET /api/stocks/000010.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: Started server process [20424]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:12839 - "POST /api/auth/login HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12841 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12846 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12851 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12855 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12862 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12861 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12857 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12874 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12870 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12873 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12886 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12887 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12888 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12889 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12893 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12894 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12971 - "GET /api/trades?ts_code=000002.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12973 - "GET /api/screener/preview/000002.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12979 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12980 - "GET /api/stocks/510300.SH/finance HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:12975 - "GET /api/screener/preview/000002.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12972 - "GET /api/stocks/000002.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12984 - "GET /api/stocks/000002.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12983 - "GET /api/stocks/000002.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: Started server process [19680]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:13034 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13037 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13046 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13045 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13044 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13058 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13060 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13059 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13074 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13151 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13152 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13155 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13153 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13229 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:13154 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: Started server process [13624]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:26668 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26677 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26680 - "GET /api/stocks/000001.SZ/reference/top10_floatholders HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26682 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26684 - "GET /api/stocks/000014.SZ/reference/pledge_detail HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26686 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26688 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26693 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26696 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 503 Service Unavailable
|
|
||||||
INFO: 127.0.0.1:26698 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26700 - "GET /api/stocks/000001.SZ/reference/high_shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26705 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26707 - "GET /api/stocks/000001.SZ/reference/bogus HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:26708 - "GET /api/stocks/510300.SH/reference/top10_holders HTTP/1.1" 404 Not Found
|
|
||||||
INFO: Started server process [23460]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:26776 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26780 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26783 - "GET /api/stocks/000001.SZ/reference/top10_floatholders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26785 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26791 - "GET /api/stocks/000014.SZ/reference/pledge_detail HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26793 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26795 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26798 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26800 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26802 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26803 - "GET /api/stocks/000001.SZ/reference/high_shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26805 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26806 - "GET /api/stocks/000001.SZ/reference/bogus HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:26807 - "GET /api/stocks/510300.SH/reference/top10_holders HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:26819 - "GET /api/stocks/688433.SH/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26821 - "GET /api/stocks/001260.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26826 - "GET /api/stocks/000792.SZ/reference/pledge_detail HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26828 - "GET /api/stocks/000408.SZ/reference/pledge_detail HTTP/1.1" 200 OK
|
|
||||||
INFO: Started server process [8048]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:26862 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26866 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26869 - "GET /api/stocks/000001.SZ/reference/top10_floatholders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26871 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26873 - "GET /api/stocks/000014.SZ/reference/pledge_detail HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26893 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26898 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26900 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26910 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26915 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26917 - "GET /api/stocks/000001.SZ/reference/high_shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26919 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26921 - "GET /api/stocks/000001.SZ/reference/bogus HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:26922 - "GET /api/stocks/510300.SH/reference/top10_holders HTTP/1.1" 404 Not Found
|
|
||||||
INFO: 127.0.0.1:26972 - "GET /api/stocks/688433.SH/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26975 - "GET /api/stocks/001260.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26977 - "GET /api/stocks/000792.SZ/reference/pledge_detail HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:26981 - "GET /api/stocks/000408.SZ/reference/pledge_detail HTTP/1.1" 200 OK
|
|
||||||
INFO: Started server process [22596]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:27093 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27102 - "GET /api/stocks/688433.SH/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27105 - "GET /api/stocks/001260.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27107 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27109 - "GET /api/stocks/000792.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27154 - "GET /api/stocks/000792.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27165 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27169 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27170 - "GET /api/stocks/000792.SZ/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27173 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27174 - "GET /api/stocks/000792.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27175 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27938 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27949 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27955 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27954 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27959 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27960 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27966 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27965 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27981 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27980 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:27977 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28003 - "GET /api/stocks/002648.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28008 - "GET /api/stocks/002648.SZ/reference/top10_floatholders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28016 - "GET /api/stocks/002648.SZ/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28021 - "GET /api/stocks/002648.SZ/reference/pledge_detail HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28024 - "GET /api/stocks/002648.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28031 - "GET /api/stocks/002648.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28034 - "GET /api/stocks/002648.SZ/reference/block_trade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28039 - "GET /api/stocks/002648.SZ/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28042 - "GET /api/stocks/002648.SZ/reference/holdertrade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28093 - "GET /api/stocks/002648.SZ/reference/shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28105 - "GET /api/stocks/002648.SZ/reference/high_shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28989 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28995 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28993 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:28994 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29003 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29006 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29001 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29005 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29015 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29016 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29012 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29030 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29032 - "GET /api/stocks/000001.SZ/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29037 - "GET /api/stocks/000001.SZ/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29041 - "GET /api/stocks/000001.SZ/reference/block_trade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29045 - "GET /api/stocks/000001.SZ/reference/holdertrade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29077 - "GET /api/stocks/000001.SZ/reference/shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29080 - "GET /api/stocks/000001.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:29085 - "GET /api/stocks/000001.SZ/reference/share_float HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30638 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30689 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30659 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30688 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30700 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30702 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30717 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30716 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30729 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30728 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:30725 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59133 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
|
||||||
INFO: 127.0.0.1:59380 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
|
||||||
INFO: 127.0.0.1:59474 - "GET /api/health HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59497 - "GET /api/auth/me HTTP/1.1" 401 Unauthorized
|
|
||||||
INFO: 127.0.0.1:59569 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59575 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59578 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59577 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59587 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59581 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59584 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59586 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59597 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59596 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59593 - "GET /api/screener/preview/000001.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59708 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59715 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59716 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59709 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59719 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59723 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59721 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59722 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59733 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59730 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59734 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59918 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59923 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59922 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59928 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59921 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59929 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59925 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59933 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59926 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59939 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59937 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59936 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59946 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59950 - "GET /api/stocks/600848.SH/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59965 - "GET /api/stocks/600848.SH/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:59973 - "GET /api/stocks/600848.SH/reference/high_shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60101 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60107 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60106 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60112 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60102 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60113 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60109 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60111 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60116 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60122 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60120 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60119 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60128 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60136 - "GET /api/stocks/600848.SH/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60153 - "GET /api/stocks/600848.SH/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60159 - "GET /api/stocks/600848.SH/reference/high_shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60237 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60243 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60242 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60240 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60248 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60246 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60249 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60257 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60247 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60255 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60256 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60272 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60508 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60515 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60514 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60510 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60523 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60521 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60524 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60522 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60540 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60542 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60541 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60565 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60731 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60738 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60737 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60743 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60735 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60740 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60744 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60742 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60753 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60752 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60749 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60761 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60755 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60848 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60852 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60853 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60849 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60858 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d&mas=5%2C10%2C20%2C60 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60859 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60854 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60856 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60862 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60866 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60865 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60874 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60990 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60993 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60995 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:60996 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61001 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61002 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61004 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61003 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61017 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61016 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61013 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61021 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61113 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61114 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61117 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61118 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61123 - "GET /api/screener/preview/600848.SH?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61124 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61122 - "GET /api/trades?ts_code=600848.SH HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61120 - "GET /api/stocks/600848.SH/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61128 - "GET /api/screener/preview/600848.SH?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61131 - "GET /api/stocks/600848.SH/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61130 - "GET /api/stocks/600848.SH/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61144 - "GET /api/stocks/600848.SH/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61154 - "GET /api/stocks/600848.SH/reference/pledge_stat HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61159 - "GET /api/stocks/600848.SH/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61164 - "GET /api/stocks/600848.SH/reference/high_shock HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61735 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61743 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61747 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61746 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61751 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61753 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61757 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61756 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61770 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61769 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61766 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61795 - "GET /api/stocks/002648.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61807 - "GET /api/stocks/002648.SZ/reference/block_trade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61813 - "GET /api/stocks/002648.SZ/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61843 - "GET /api/stocks/002648.SZ/reference/holdertrade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:61870 - "GET /api/stocks/002648.SZ/reference/repurchase HTTP/1.1" 200 OK
|
|
||||||
INFO: Started server process [43184]
|
|
||||||
INFO: Waiting for application startup.
|
|
||||||
INFO: Application startup complete.
|
|
||||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
|
||||||
INFO: 127.0.0.1:62984 - "GET /api/stocks/000001.SZ/reference/moneyflow HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:62991 - "GET /api/stocks/000001.SZ/reference/moneyflow HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63245 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63246 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63250 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63249 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63260 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63256 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63258 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63259 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63276 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63275 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63279 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:63272 - "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:63281 - "GET /api/stocks/000001.SZ/reference/moneyflow HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11920 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11937 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11933 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11936 - "GET /api/stocks?search=%E5%8D%AB%E6%98%9F%E5%8C%96%E5%AD%A6&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11942 - "GET /api/stocks/002648.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11941 - "GET /api/trades?ts_code=002648.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11943 - "GET /api/screener/preview/002648.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11949 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11967 - "GET /api/stocks/002648.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11964 - "GET /api/stocks/002648.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:11963 - "GET /api/screener/preview/002648.SZ?limit=800&adjust=qfq&timeframe=1d&end=2024-08-14 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12029 - "GET /api/stocks/002648.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12088 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12093 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12094 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12139 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12155 - "GET /api/market/indexes/DJI HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12160 - "GET /api/market/indexes/DJI/candles?timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12223 - "GET /api/market/global-indexes HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12288 - "GET /api/market/overview HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12293 - "GET /api/screener/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12294 - "GET /api/etf/sync/status HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12303 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12302 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12309 - "GET /api/stocks?watched_only=true&sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12314 - "GET /api/trades?ts_code=002100.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12326 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12325 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12324 - "GET /api/stocks/002100.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12333 - "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:12335 - "GET /api/stocks/002100.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12334 - "GET /api/stocks/002100.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12344 - "GET /api/stocks/002100.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12353 - "GET /api/stocks/002100.SZ/reference/block_trade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12528 - "GET /api/stocks/002100.SZ/reference/holdertrade HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12543 - "GET /api/stocks/002100.SZ/reference/holdernumber HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12573 - "GET /api/auth/me HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12580 - "GET /api/stocks/facets HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12577 - "GET /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12579 - "GET /api/stocks?sort=symbol&order=asc&limit=100&offset=0 HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12585 - "GET /api/watchlist HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12582 - "GET /api/trades?ts_code=000001.SZ HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12583 - "GET /api/stocks/000001.SZ/dividends HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12584 - "GET /api/screener/preview/000001.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12595 - "GET /api/stocks/000001.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12594 - "GET /api/stocks/000001.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12591 - "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:12599 - "GET /api/stocks/000001.SZ/reference/top10_holders HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12604 - "GET /api/stocks/000001.SZ/reference/moneyflow HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12607 - "GET /api/stocks/002100.SZ/reference/moneyflow HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12676 - "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:12826 - "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:12831 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1w HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12833 - "PUT /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12841 - "GET /api/stocks/002100.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12842 - "GET /api/stocks/002100.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12838 - "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:12880 - "GET /api/screener/preview/002100.SZ?limit=500&adjust=qfq&timeframe=1d HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12886 - "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:12890 - "GET /api/stocks/002100.SZ/finance HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12891 - "GET /api/stocks/002100.SZ/company HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12892 - "PUT /api/preferences HTTP/1.1" 200 OK
|
|
||||||
INFO: 127.0.0.1:12896 - "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:12901 - "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:12942 - "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:12944 - "PUT /api/preferences HTTP/1.1" 200 OK
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
AdjustMode,
|
AdjustMode,
|
||||||
BacktestRequest,
|
|
||||||
BacktestResponse,
|
|
||||||
Candle,
|
Candle,
|
||||||
CurrentUser,
|
CurrentUser,
|
||||||
EventBacktestRequest,
|
EventBacktestRequest,
|
||||||
@@ -11,6 +9,9 @@ import type {
|
|||||||
GlobalIndexList,
|
GlobalIndexList,
|
||||||
IndexDetail,
|
IndexDetail,
|
||||||
IndexWeights,
|
IndexWeights,
|
||||||
|
LimitBoard,
|
||||||
|
ThsBoardList,
|
||||||
|
ThsBoardMembers,
|
||||||
LoginRequest,
|
LoginRequest,
|
||||||
LoginResponse,
|
LoginResponse,
|
||||||
MarketOverview,
|
MarketOverview,
|
||||||
@@ -27,8 +28,6 @@ import type {
|
|||||||
StockFinanceOut,
|
StockFinanceOut,
|
||||||
StockReferenceOut,
|
StockReferenceOut,
|
||||||
StockListResponse,
|
StockListResponse,
|
||||||
SyncRequest,
|
|
||||||
SyncResponse,
|
|
||||||
Timeframe,
|
Timeframe,
|
||||||
TradesClearResponse,
|
TradesClearResponse,
|
||||||
TradesImportResponse,
|
TradesImportResponse,
|
||||||
@@ -96,18 +95,6 @@ export async function logout(): Promise<void> {
|
|||||||
if (!res.ok && res.status !== 401) throw new ApiError(await readError(res, '退出登录失败'), res.status);
|
if (!res.ok && res.status !== 401) throw new ApiError(await readError(res, '退出登录失败'), res.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postBacktest(req: BacktestRequest): Promise<BacktestResponse> {
|
|
||||||
const res = await apiFetch('/api/backtest', { method: 'POST', body: JSON.stringify(req) });
|
|
||||||
if (!res.ok) throw new ApiError(`回测请求失败 (HTTP ${res.status}): ${await res.text()}`, res.status);
|
|
||||||
return (await res.json()) as BacktestResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function syncData(req: SyncRequest): Promise<SyncResponse> {
|
|
||||||
const res = await apiFetch('/api/data/sync', { method: 'POST', body: JSON.stringify(req) });
|
|
||||||
if (!res.ok) throw new ApiError(`数据拉取失败 (HTTP ${res.status}): ${await res.text()}`, res.status);
|
|
||||||
return (await res.json()) as SyncResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 自然语言事件回测。全市场扫描较慢,timeout 放宽到 15 分钟。 */
|
/** 自然语言事件回测。全市场扫描较慢,timeout 放宽到 15 分钟。 */
|
||||||
export async function postEventBacktest(req: EventBacktestRequest): Promise<EventBacktestResponse> {
|
export async function postEventBacktest(req: EventBacktestRequest): Promise<EventBacktestResponse> {
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
@@ -172,7 +159,7 @@ export async function startScreenerSync(req: ScreenerSyncRequest = {}): Promise<
|
|||||||
|
|
||||||
export async function getScreenerSyncStatus(): Promise<ScreenerSyncStatus> {
|
export async function getScreenerSyncStatus(): Promise<ScreenerSyncStatus> {
|
||||||
const res = await apiFetch('/api/screener/sync/status');
|
const res = await apiFetch('/api/screener/sync/status');
|
||||||
if (!res.ok) throw new ApiError(`获取同步状态失败 (HTTP ${res.status})`, res.status);
|
if (!res.ok) throw new ApiError(await readError(res, `获取同步状态失败 (HTTP ${res.status})`), res.status);
|
||||||
return (await res.json()) as ScreenerSyncStatus;
|
return (await res.json()) as ScreenerSyncStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +172,10 @@ export async function getStockPreview(
|
|||||||
mas?: number[];
|
mas?: number[];
|
||||||
/** 向前翻页:返回该日期(不含)之前的 limit 根 K 线 + 预热好的指标 */
|
/** 向前翻页:返回该日期(不含)之前的 limit 根 K 线 + 预热好的指标 */
|
||||||
end?: string;
|
end?: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<PreviewResponse> {
|
): Promise<PreviewResponse> {
|
||||||
const { limit = 500, adjust = 'qfq', timeframe = '1d', mas, end } = opts;
|
const { limit = 500, adjust = 'qfq', timeframe = '1d', mas, end, signal } = opts;
|
||||||
const q = new URLSearchParams({
|
const q = new URLSearchParams({
|
||||||
limit: String(limit),
|
limit: String(limit),
|
||||||
adjust,
|
adjust,
|
||||||
@@ -195,7 +183,7 @@ export async function getStockPreview(
|
|||||||
...(mas?.length ? { mas: mas.join(',') } : {}),
|
...(mas?.length ? { mas: mas.join(',') } : {}),
|
||||||
...(end ? { end } : {}),
|
...(end ? { end } : {}),
|
||||||
});
|
});
|
||||||
const res = await apiFetch(`/api/screener/preview/${encodeURIComponent(tsCode)}?${q.toString()}`);
|
const res = await apiFetch(`/api/screener/preview/${encodeURIComponent(tsCode)}?${q.toString()}`, { signal });
|
||||||
if (!res.ok) throw new ApiError(await readError(res, `获取个股详情失败 (HTTP ${res.status})`), res.status);
|
if (!res.ok) throw new ApiError(await readError(res, `获取个股详情失败 (HTTP ${res.status})`), res.status);
|
||||||
return (await res.json()) as PreviewResponse;
|
return (await res.json()) as PreviewResponse;
|
||||||
}
|
}
|
||||||
@@ -235,6 +223,26 @@ export async function getMarketOverview(): Promise<MarketOverview> {
|
|||||||
return (await res.json()) as MarketOverview;
|
return (await res.json()) as MarketOverview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 打板专题(主页) ----------
|
||||||
|
export async function getLimitBoard(): Promise<LimitBoard> {
|
||||||
|
const res = await apiFetch('/api/market/limit-board');
|
||||||
|
if (!res.ok) throw new ApiError(await readError(res, '获取打板数据失败'), res.status);
|
||||||
|
return (await res.json()) as LimitBoard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 概念板块(THS) ----------
|
||||||
|
export async function getThsBoards(opts: { signal?: AbortSignal } = {}): Promise<ThsBoardList> {
|
||||||
|
const res = await apiFetch('/api/market/boards', { signal: opts.signal });
|
||||||
|
if (!res.ok) throw new ApiError(await readError(res, '获取板块列表失败'), res.status);
|
||||||
|
return (await res.json()) as ThsBoardList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getThsBoardMembers(code: string, opts: { signal?: AbortSignal } = {}): Promise<ThsBoardMembers> {
|
||||||
|
const res = await apiFetch(`/api/market/boards/${encodeURIComponent(code)}/members`, { signal: opts.signal });
|
||||||
|
if (!res.ok) throw new ApiError(await readError(res, '获取板块成分失败'), res.status);
|
||||||
|
return (await res.json()) as ThsBoardMembers;
|
||||||
|
}
|
||||||
|
|
||||||
/** 上证指数全量 K 线(日线基底聚合到目标周期;收盘口径) */
|
/** 上证指数全量 K 线(日线基底聚合到目标周期;收盘口径) */
|
||||||
export async function getIndexCandles(timeframe: Timeframe): Promise<Candle[]> {
|
export async function getIndexCandles(timeframe: Timeframe): Promise<Candle[]> {
|
||||||
const res = await apiFetch(`/api/market/index-candles?timeframe=${encodeURIComponent(timeframe)}`);
|
const res = await apiFetch(`/api/market/index-candles?timeframe=${encodeURIComponent(timeframe)}`);
|
||||||
@@ -256,9 +264,10 @@ export async function getIndexDetail(code: string): Promise<IndexDetail> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 白名单指数全量 K 线(国内 index_daily / 国际 index_global,日线基底聚合) */
|
/** 白名单指数全量 K 线(国内 index_daily / 国际 index_global,日线基底聚合) */
|
||||||
export async function getAnyIndexCandles(code: string, timeframe: Timeframe): Promise<Candle[]> {
|
export async function getAnyIndexCandles(code: string, timeframe: Timeframe, opts: { signal?: AbortSignal } = {}): Promise<Candle[]> {
|
||||||
const res = await apiFetch(
|
const res = await apiFetch(
|
||||||
`/api/market/indexes/${encodeURIComponent(code)}/candles?timeframe=${encodeURIComponent(timeframe)}`,
|
`/api/market/indexes/${encodeURIComponent(code)}/candles?timeframe=${encodeURIComponent(timeframe)}`,
|
||||||
|
{ signal: opts.signal },
|
||||||
);
|
);
|
||||||
if (!res.ok) throw new ApiError(await readError(res, `获取指数K线失败 (HTTP ${res.status})`), res.status);
|
if (!res.ok) throw new ApiError(await readError(res, `获取指数K线失败 (HTTP ${res.status})`), res.status);
|
||||||
return (await res.json()) as Candle[];
|
return (await res.json()) as Candle[];
|
||||||
@@ -283,6 +292,7 @@ export async function getStocks(params: {
|
|||||||
order?: 'asc' | 'desc';
|
order?: 'asc' | 'desc';
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
signal?: AbortSignal;
|
||||||
}): Promise<StockListResponse> {
|
}): Promise<StockListResponse> {
|
||||||
const q = new URLSearchParams();
|
const q = new URLSearchParams();
|
||||||
if (params.search) q.set('search', params.search);
|
if (params.search) q.set('search', params.search);
|
||||||
@@ -294,7 +304,7 @@ export async function getStocks(params: {
|
|||||||
if (params.order) q.set('order', params.order);
|
if (params.order) q.set('order', params.order);
|
||||||
q.set('limit', String(params.limit ?? 100));
|
q.set('limit', String(params.limit ?? 100));
|
||||||
q.set('offset', String(params.offset ?? 0));
|
q.set('offset', String(params.offset ?? 0));
|
||||||
const res = await apiFetch(`/api/stocks?${q.toString()}`);
|
const res = await apiFetch(`/api/stocks?${q.toString()}`, { signal: params.signal });
|
||||||
if (!res.ok) throw new ApiError(await readError(res, `获取股票列表失败 (HTTP ${res.status})`), res.status);
|
if (!res.ok) throw new ApiError(await readError(res, `获取股票列表失败 (HTTP ${res.status})`), res.status);
|
||||||
return (await res.json()) as StockListResponse;
|
return (await res.json()) as StockListResponse;
|
||||||
}
|
}
|
||||||
@@ -314,6 +324,7 @@ export async function getEtfs(params: {
|
|||||||
order?: 'asc' | 'desc';
|
order?: 'asc' | 'desc';
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
signal?: AbortSignal;
|
||||||
}): Promise<EtfListResponse> {
|
}): Promise<EtfListResponse> {
|
||||||
const q = new URLSearchParams();
|
const q = new URLSearchParams();
|
||||||
if (params.search) q.set('search', params.search);
|
if (params.search) q.set('search', params.search);
|
||||||
@@ -323,7 +334,7 @@ export async function getEtfs(params: {
|
|||||||
if (params.order) q.set('order', params.order);
|
if (params.order) q.set('order', params.order);
|
||||||
q.set('limit', String(params.limit ?? 100));
|
q.set('limit', String(params.limit ?? 100));
|
||||||
q.set('offset', String(params.offset ?? 0));
|
q.set('offset', String(params.offset ?? 0));
|
||||||
const res = await apiFetch(`/api/etfs?${q.toString()}`);
|
const res = await apiFetch(`/api/etfs?${q.toString()}`, { signal: params.signal });
|
||||||
if (!res.ok) throw new ApiError(await readError(res, `获取 ETF 列表失败 (HTTP ${res.status})`), res.status);
|
if (!res.ok) throw new ApiError(await readError(res, `获取 ETF 列表失败 (HTTP ${res.status})`), res.status);
|
||||||
return (await res.json()) as EtfListResponse;
|
return (await res.json()) as EtfListResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,71 +27,6 @@ export interface Candle {
|
|||||||
turnover?: number | null; // 换手率(%);daily_basic 缺失时为 null
|
turnover?: number | null; // 换手率(%);daily_basic 缺失时为 null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BacktestRequest {
|
|
||||||
symbol: string;
|
|
||||||
timeframe: string;
|
|
||||||
strategy: string;
|
|
||||||
params: Record<string, number>;
|
|
||||||
initial_cash: number;
|
|
||||||
fast_mode: boolean;
|
|
||||||
start?: string;
|
|
||||||
end?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SignalOut {
|
|
||||||
ts: string;
|
|
||||||
side: 'buy' | 'sell';
|
|
||||||
price: number;
|
|
||||||
qty: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EquityPoint {
|
|
||||||
ts: string;
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IndicatorOut {
|
|
||||||
strategy: string;
|
|
||||||
data: Record<string, (number | null)[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MetricsOut {
|
|
||||||
total_return: number;
|
|
||||||
max_drawdown: number;
|
|
||||||
sharpe: number;
|
|
||||||
volatility: number;
|
|
||||||
num_trades: number;
|
|
||||||
win_rate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BacktestResponse {
|
|
||||||
symbol: string;
|
|
||||||
timeframe: string;
|
|
||||||
strategy: string;
|
|
||||||
candles: Candle[];
|
|
||||||
indicators: IndicatorOut;
|
|
||||||
signals: SignalOut[];
|
|
||||||
equity: EquityPoint[];
|
|
||||||
metrics: MetricsOut;
|
|
||||||
final_cash: number;
|
|
||||||
final_position: number;
|
|
||||||
initial_cash: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncRequest {
|
|
||||||
symbol: string;
|
|
||||||
start?: string;
|
|
||||||
end?: string;
|
|
||||||
source?: string; // auto | tushare | akshare
|
|
||||||
force?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncResponse {
|
|
||||||
symbol: string;
|
|
||||||
bars: number;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 智能选股(镜像 app/schemas.py) ----------
|
// ---------- 智能选股(镜像 app/schemas.py) ----------
|
||||||
export type Op = 'gt' | 'ge' | 'lt' | 'le' | 'between';
|
export type Op = 'gt' | 'ge' | 'lt' | 'le' | 'between';
|
||||||
|
|
||||||
@@ -532,6 +467,94 @@ export interface MarketOverview {
|
|||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 打板专题(主页,同花顺口径) ----------
|
||||||
|
export interface LimitStock {
|
||||||
|
ts_code: string;
|
||||||
|
name?: string | null;
|
||||||
|
price?: number | null; // 收盘价(元)
|
||||||
|
pct_chg?: number | null; // 涨跌幅 %
|
||||||
|
tag?: string | null; // 首板 / 2天2板(仅涨停池)
|
||||||
|
status?: string | null; // 一字板 / 换手板(仅涨停池)
|
||||||
|
lu_desc?: string | null; // 涨停原因(仅涨停池)
|
||||||
|
open_num?: number | null; // 打开次数
|
||||||
|
limit_amount_yi?: number | null; // 封单额(亿元,仅涨停池)
|
||||||
|
turnover_yi?: number | null; // 成交额(亿元,仅涨停池)
|
||||||
|
first_lu_time?: string | null; // 首次涨停时间
|
||||||
|
last_lu_time?: string | null; // 最后涨停时间(仅炸板池)
|
||||||
|
limit_up_suc_rate?: number | null; // 近一年封板率 %(仅涨停池)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LimitLadder {
|
||||||
|
ts_code: string;
|
||||||
|
name?: string | null;
|
||||||
|
nums: number; // 连板数
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LimitBlock {
|
||||||
|
name?: string | null; // 同花顺概念板块名
|
||||||
|
days?: number | null;
|
||||||
|
up_stat?: string | null; // 如「6天3板」
|
||||||
|
cons_nums?: number | null; // 连板家数
|
||||||
|
up_nums?: number | null; // 涨停家数
|
||||||
|
pct_chg?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LimitBoardSummary {
|
||||||
|
up_count: number;
|
||||||
|
broken_count: number;
|
||||||
|
down_count: number;
|
||||||
|
first_board_count: number;
|
||||||
|
max_ladder?: LimitLadder | null;
|
||||||
|
ladder_dist: { nums: number; count: number }[]; // 2板起升序
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LimitBoard {
|
||||||
|
trade_date: string; // YYYY-MM-DD
|
||||||
|
updated_at: string;
|
||||||
|
summary: LimitBoardSummary;
|
||||||
|
up: LimitStock[]; // 涨停池(封单额降序)
|
||||||
|
broken: LimitStock[];
|
||||||
|
down: LimitStock[];
|
||||||
|
ladder: LimitLadder[]; // 连板数降序
|
||||||
|
blocks: LimitBlock[];
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 概念板块(THS:ths_index + ths_daily + ths_member) ----------
|
||||||
|
export type ThsBoardType = 'N' | 'I' | 'TH' | 'S' | 'R' | 'BB' | 'ST'; // 概念/行业/主题/特色/地域/宽基/风格
|
||||||
|
|
||||||
|
export interface ThsBoard {
|
||||||
|
ts_code: string; // 885835.TI / 700001.TI
|
||||||
|
name?: string | null;
|
||||||
|
type?: ThsBoardType | string | null;
|
||||||
|
count?: number | null; // 成分个数
|
||||||
|
list_date?: string | null;
|
||||||
|
close?: number | null; // 板块指数收盘(当日快照)
|
||||||
|
pct_change?: number | null; // 涨跌幅 %
|
||||||
|
vol?: number | null; // 成交量(手)
|
||||||
|
turnover_rate?: number | null; // 换手率 %
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThsBoardList {
|
||||||
|
trade_date?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
boards: ThsBoard[];
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThsMember {
|
||||||
|
con_code: string; // 000016.SZ
|
||||||
|
con_name?: string | null;
|
||||||
|
close?: number | null; // 现价(北交所等无底座为空)
|
||||||
|
pct_chg?: number | null; // 涨跌幅 %
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThsBoardMembers {
|
||||||
|
code: string;
|
||||||
|
name?: string | null;
|
||||||
|
members: ThsMember[];
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 指数专题(国际指数卡片 + 指数详情) ----------
|
// ---------- 指数专题(国际指数卡片 + 指数详情) ----------
|
||||||
export type GlobalRegion = 'americas' | 'europe' | 'asia';
|
export type GlobalRegion = 'americas' | 'europe' | 'asia';
|
||||||
|
|
||||||
|
|||||||
@@ -790,9 +790,11 @@ 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 即可:父组件对 data 是整体替换(新数组引用),props 引用变化必触发;
|
||||||
|
// deep 反而每次深遍历几百根 K 线的嵌套数组(父组件从无原地改写)
|
||||||
|
watch(() => [props.candles, props.indicators, props.subPanes, props.showBoll, props.showZhixing, props.zhixingBlocks, props.maPeriods, props.timeframe], () => { teardown(); build(); });
|
||||||
// 事件标记数据变化(导入/清空/开关显示/分红数据到达):只重画标记,不重建图表(保留滚动位置与用户画线)
|
// 事件标记数据变化(导入/清空/开关显示/分红数据到达):只重画标记,不重建图表(保留滚动位置与用户画线)
|
||||||
watch(() => [props.tradeMarkers, props.dividendMarkers], renderMarkers, { deep: true });
|
watch(() => [props.tradeMarkers, props.dividendMarkers], renderMarkers);
|
||||||
// 涨跌配色切换:重建图表以应用新颜色
|
// 涨跌配色切换:重建图表以应用新颜色
|
||||||
watch(() => settings.priceTone, () => { teardown(); build(); });
|
watch(() => settings.priceTone, () => { teardown(); build(); });
|
||||||
// 副图高度变化:仅调 pane 高度,不重建(保留滚动/画线状态)
|
// 副图高度变化:仅调 pane 高度,不重建(保留滚动/画线状态)
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
||||||
import { dispose, init, type Chart, type KLineData } from 'klinecharts';
|
|
||||||
|
|
||||||
import { getIndexCandles } from '@/api/client';
|
|
||||||
import type { Candle, Timeframe } from '@/api/types';
|
|
||||||
import { useSettingsStore } from '@/stores/settings';
|
|
||||||
import { darkStyles } from '@/chartStyles';
|
|
||||||
|
|
||||||
// 首页上证指数 K 线图:轻量版(无翻页/画线/副图配置)。
|
|
||||||
// v10 无 applyNewData,数据只进 dataLoader——每次到新数据整图重建(切周期/换配色同款,
|
|
||||||
// 与详情页 teardown+build 模式一致;全量 ≤9000 根,init 开销毫秒级)。
|
|
||||||
// 周期随用户偏好持久化(chartLayout.indexTimeframe)。
|
|
||||||
const settings = useSettingsStore();
|
|
||||||
|
|
||||||
const PERIODS: { key: Timeframe; label: string }[] = [
|
|
||||||
{ key: '1d', label: '日K' },
|
|
||||||
{ key: '1w', label: '周K' },
|
|
||||||
{ key: '1M', label: '月K' },
|
|
||||||
{ key: '1y', label: '年K' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const timeframe = ref<Timeframe>(settings.chartLayout.indexTimeframe ?? '1d');
|
|
||||||
function setTimeframe(tf: Timeframe) {
|
|
||||||
if (tf === timeframe.value) return;
|
|
||||||
timeframe.value = tf;
|
|
||||||
settings.setChartLayout({ indexTimeframe: tf });
|
|
||||||
void load();
|
|
||||||
}
|
|
||||||
|
|
||||||
const container = ref<HTMLDivElement | null>(null);
|
|
||||||
const loading = ref(false);
|
|
||||||
const error = ref<string | null>(null);
|
|
||||||
const lastDate = ref(''); // 数据末根交易日(收盘口径)
|
|
||||||
let chart: Chart | null = null;
|
|
||||||
let loadToken = 0;
|
|
||||||
let lastCandles: Candle[] | null = null; // 配色切换重建图表时免重拉
|
|
||||||
|
|
||||||
function rebuild(candles: Candle[]) {
|
|
||||||
if (!container.value) return;
|
|
||||||
if (chart) { dispose(container.value); chart = null; }
|
|
||||||
const ch = init(container.value, { styles: darkStyles(settings.upHex, settings.downHex) });
|
|
||||||
if (!ch) return;
|
|
||||||
chart = ch;
|
|
||||||
const data: KLineData[] = candles.map((c) => ({
|
|
||||||
timestamp: new Date(c.ts).getTime(),
|
|
||||||
open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume,
|
|
||||||
}));
|
|
||||||
// 全量已在手:init 一次给足,forward(更早历史)/backward(更新端)都无更多
|
|
||||||
ch.setDataLoader({
|
|
||||||
getBars: ({ type, callback }) => {
|
|
||||||
if (type === 'init') callback(data, { forward: false, backward: false });
|
|
||||||
else callback([], { forward: false, backward: false });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// v10 要求 symbol+period+dataLoader 三者齐备才触发 'init' 加载
|
|
||||||
ch.setSymbol({ ticker: '000001.SH' });
|
|
||||||
ch.setPeriod({ type: 'day', span: 1 });
|
|
||||||
// 主图 MA(周期与详情页默认一致)+ VOL 副图;右侧留白与详情页同款
|
|
||||||
ch.createIndicator({ name: 'MA', paneId: 'candle_pane', calcParams: [5, 10, 20, 60] });
|
|
||||||
ch.createIndicator('VOL');
|
|
||||||
const volPane = ch.getIndicators().find((i) => i.name === 'VOL')?.paneId;
|
|
||||||
ch.setPaneOptions({ id: 'candle_pane', height: 252, minHeight: 160 });
|
|
||||||
if (volPane) ch.setPaneOptions({ id: volPane, height: 76, minHeight: 56 });
|
|
||||||
ch.setOffsetRightDistance(28);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
const token = ++loadToken;
|
|
||||||
loading.value = true;
|
|
||||||
error.value = null;
|
|
||||||
try {
|
|
||||||
const candles = await getIndexCandles(timeframe.value);
|
|
||||||
if (token !== loadToken) return; // 期间已切换周期,旧响应丢弃
|
|
||||||
lastCandles = candles;
|
|
||||||
rebuild(candles);
|
|
||||||
lastDate.value = candles.length ? candles[candles.length - 1].ts.slice(0, 10) : '';
|
|
||||||
} catch (e) {
|
|
||||||
if (token === loadToken) error.value = e instanceof Error ? e.message : '获取指数K线失败';
|
|
||||||
} finally {
|
|
||||||
if (token === loadToken) loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load);
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (container.value) dispose(container.value);
|
|
||||||
chart = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 涨跌配色切换:重建图表应用新颜色,数据用已拉到的直接重放
|
|
||||||
watch(() => settings.priceTone, () => {
|
|
||||||
if (lastCandles) rebuild(lastCandles);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="rounded-lg border border-[#26272E] bg-[#101014] p-3">
|
|
||||||
<div class="mb-2 flex items-center justify-between">
|
|
||||||
<div class="flex items-baseline gap-2">
|
|
||||||
<span class="text-sm font-medium text-[#E5E7EB]">上证指数</span>
|
|
||||||
<span v-if="lastDate" class="font-mono text-xs tabular-nums text-[#6B7280]">收盘口径 · {{ lastDate }}</span>
|
|
||||||
<span v-else class="text-xs text-[#6B7280]">收盘口径</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
v-for="p in PERIODS"
|
|
||||||
:key="p.key"
|
|
||||||
type="button"
|
|
||||||
class="rounded-md border px-2.5 py-1 text-[13px] transition-colors"
|
|
||||||
:class="timeframe === p.key
|
|
||||||
? 'border-blue-600 bg-blue-600 text-white'
|
|
||||||
: 'border-[#26272E] bg-[#101014] text-[#9BA3AE] hover:text-[#E5E7EB]'"
|
|
||||||
@click="setTimeframe(p.key)"
|
|
||||||
>{{ p.label }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="relative h-[340px]">
|
|
||||||
<div ref="container" class="h-full w-full" />
|
|
||||||
<div
|
|
||||||
v-if="loading"
|
|
||||||
class="absolute inset-0 z-10 flex flex-col items-center justify-center bg-black/70 text-sm text-[#9BA3AE]"
|
|
||||||
>
|
|
||||||
<svg class="mb-2 h-6 w-6 animate-spin text-blue-500" viewBox="0 0 24 24" fill="none"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" /></svg>
|
|
||||||
指数K线加载中…
|
|
||||||
</div>
|
|
||||||
<div v-else-if="error" class="flex h-full items-center justify-center text-sm text-[#A8AFB8]">
|
|
||||||
{{ error }}
|
|
||||||
<button type="button" class="ml-2 text-blue-500 hover:underline" @click="load">重试</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -420,6 +420,9 @@ const filteredItems = computed(() => {
|
|||||||
(it) => it.ts_code.toLowerCase().includes(q) || it.name.toLowerCase().includes(q),
|
(it) => it.ts_code.toLowerCase().includes(q) || it.name.toLowerCase().includes(q),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
// 渲染截断:概念板块宽基可到 5555 只成分,全进 DOM 会卡;搜索可收敛,键盘 ↑/↓ 不受影响
|
||||||
|
const ITEM_RENDER_CAP = 300;
|
||||||
|
const shownItems = computed(() => filteredItems.value.slice(0, ITEM_RENDER_CAP));
|
||||||
|
|
||||||
const activeItem = computed(
|
const activeItem = computed(
|
||||||
() => props.items.find((it) => it.ts_code === active.value) ?? null,
|
() => props.items.find((it) => it.ts_code === active.value) ?? null,
|
||||||
@@ -628,7 +631,7 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
|
|||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||||
<button
|
<button
|
||||||
v-for="it in filteredItems"
|
v-for="it in shownItems"
|
||||||
:key="it.ts_code"
|
:key="it.ts_code"
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-full items-center gap-2 border-b border-[#1E2026] px-3 py-2 text-left transition-colors"
|
class="flex w-full items-center gap-2 border-b border-[#1E2026] px-3 py-2 text-left transition-colors"
|
||||||
@@ -647,6 +650,9 @@ const fmtListDate = (s?: string | null) => (s && s.length === 8 ? `${s.slice(0,
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="filteredItems.length === 0" class="px-3 py-8 text-center text-[13px] text-[#9BA3AE]">无匹配</div>
|
<div v-if="filteredItems.length === 0" class="px-3 py-8 text-center text-[13px] text-[#9BA3AE]">无匹配</div>
|
||||||
|
<div v-else-if="filteredItems.length > ITEM_RENDER_CAP" class="px-3 py-2 text-center text-[11px] text-[#6B7280]">
|
||||||
|
共 {{ filteredItems.length.toLocaleString() }} 只,仅显示前 {{ ITEM_RENDER_CAP }} · 用上方搜索收敛
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border-t border-[#1E2026] px-3 py-2 text-xs text-[#9BA3AE]">共 {{ filteredItems.length }} 只</div>
|
<div class="border-t border-[#1E2026] px-3 py-2 text-xs text-[#9BA3AE]">共 {{ filteredItems.length }} 只</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import HomeView from '@/views/HomeView.vue';
|
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/login', name: 'login', component: () => import('@/views/LoginView.vue'), meta: { public: true } },
|
{ path: '/login', name: 'login', component: () => import('@/views/LoginView.vue'), meta: { public: true } },
|
||||||
{ path: '/', name: 'home', component: HomeView },
|
{ path: '/', name: 'home', component: () => import('@/views/HomeView.vue') },
|
||||||
{ path: '/screener', name: 'screener', component: () => import('@/views/ScreenerView.vue') },
|
{ path: '/screener', name: 'screener', component: () => import('@/views/ScreenerView.vue') },
|
||||||
{ path: '/stocks', name: 'stocks', component: () => import('@/views/StocksView.vue') },
|
{ path: '/stocks', name: 'stocks', component: () => import('@/views/StocksView.vue') },
|
||||||
{ path: '/etfs', name: 'etfs', component: () => import('@/views/EtfsView.vue') },
|
{ path: '/etfs', name: 'etfs', component: () => import('@/views/EtfsView.vue') },
|
||||||
|
{ path: '/concepts', name: 'concepts', component: () => import('@/views/ConceptsView.vue') },
|
||||||
{ path: '/indexes', name: 'indexes', component: () => import('@/views/IndexesView.vue') },
|
{ path: '/indexes', name: 'indexes', component: () => import('@/views/IndexesView.vue') },
|
||||||
{ path: '/indexes/:code', name: 'index-detail', component: () => import('@/views/IndexDetailView.vue') },
|
{ path: '/indexes/:code', name: 'index-detail', component: () => import('@/views/IndexDetailView.vue') },
|
||||||
{ path: '/backtest', name: 'backtest', component: () => import('@/views/BacktestView.vue') },
|
{ path: '/backtest', name: 'backtest', component: () => import('@/views/BacktestView.vue') },
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ function normTipFields(v: unknown): TooltipField[] | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TF_VALID = new Set(['1d', '1w', '1M', '1y']);
|
const TF_VALID = new Set(['1d', '1w', '1M', '1y']);
|
||||||
|
const TONE_VALID = new Set(['red-up', 'green-up']);
|
||||||
|
const ADJUST_VALID = new Set(['bfq', 'qfq', 'hfq']);
|
||||||
/** K线周期合法化;非法/缺失返回 undefined(调用方回退默认值) */
|
/** K线周期合法化;非法/缺失返回 undefined(调用方回退默认值) */
|
||||||
function normTimeframe(v: unknown): Timeframe | undefined {
|
function normTimeframe(v: unknown): Timeframe | undefined {
|
||||||
return typeof v === 'string' && TF_VALID.has(v) ? (v as Timeframe) : undefined;
|
return typeof v === 'string' && TF_VALID.has(v) ? (v as Timeframe) : undefined;
|
||||||
@@ -154,16 +156,16 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
const prefs = await getPreferences();
|
const prefs = await getPreferences();
|
||||||
if (dirty.has('priceTone')) {
|
if (dirty.has('priceTone')) {
|
||||||
schedulePush('priceTone', priceTone.value);
|
schedulePush('priceTone', priceTone.value);
|
||||||
} else if (typeof prefs.priceTone === 'string' && prefs.priceTone !== priceTone.value) {
|
} else if (typeof prefs.priceTone === 'string' && TONE_VALID.has(prefs.priceTone) && prefs.priceTone !== priceTone.value) {
|
||||||
priceTone.value = prefs.priceTone as PriceTone;
|
priceTone.value = prefs.priceTone as PriceTone; // 成员已校验
|
||||||
saveLocal(STORAGE_KEY, prefs.priceTone);
|
saveLocal(STORAGE_KEY, prefs.priceTone);
|
||||||
} else if (prefs.priceTone === undefined) {
|
} else if (prefs.priceTone === undefined) {
|
||||||
schedulePush('priceTone', priceTone.value);
|
schedulePush('priceTone', priceTone.value);
|
||||||
}
|
}
|
||||||
if (dirty.has('priceAdjust')) {
|
if (dirty.has('priceAdjust')) {
|
||||||
schedulePush('priceAdjust', priceAdjust.value);
|
schedulePush('priceAdjust', priceAdjust.value);
|
||||||
} else if (typeof prefs.priceAdjust === 'string' && prefs.priceAdjust !== priceAdjust.value) {
|
} else if (typeof prefs.priceAdjust === 'string' && ADJUST_VALID.has(prefs.priceAdjust) && prefs.priceAdjust !== priceAdjust.value) {
|
||||||
priceAdjust.value = prefs.priceAdjust as PriceAdjust;
|
priceAdjust.value = prefs.priceAdjust as PriceAdjust; // 成员已校验
|
||||||
saveLocal(ADJUST_KEY, prefs.priceAdjust);
|
saveLocal(ADJUST_KEY, prefs.priceAdjust);
|
||||||
} else if (prefs.priceAdjust === undefined) {
|
} else if (prefs.priceAdjust === undefined) {
|
||||||
schedulePush('priceAdjust', priceAdjust.value);
|
schedulePush('priceAdjust', priceAdjust.value);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { addWatchlist, getEtfs, removeWatchlist } from '@/api/client';
|
import { addWatchlist, getEtfs, removeWatchlist } from '@/api/client';
|
||||||
import type { EtfListItem, ScreenerItemOut } from '@/api/types';
|
import type { EtfListItem, ScreenerItemOut } from '@/api/types';
|
||||||
|
import { useQuerySync } from '@/composables/useQuerySync';
|
||||||
import EtfSyncBar from '@/components/EtfSyncBar.vue';
|
import EtfSyncBar from '@/components/EtfSyncBar.vue';
|
||||||
import StockDetailOverlay from '@/components/StockDetailOverlay.vue';
|
import StockDetailOverlay from '@/components/StockDetailOverlay.vue';
|
||||||
|
|
||||||
// ---------- 筛选状态(初始值从路由 query 还原,刷新/分享链接不丢现场) ----------
|
// ---------- 筛选状态(初始值从路由 query 还原,刷新/分享链接不丢现场) ----------
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
function qStr(key: string): string | undefined {
|
function qStr(key: string): string | undefined {
|
||||||
const v = route.query[key];
|
const v = route.query[key];
|
||||||
@@ -58,10 +58,14 @@ const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize))
|
|||||||
|
|
||||||
// ---------- 加载(搜索防抖) ----------
|
// ---------- 加载(搜索防抖) ----------
|
||||||
let fetchToken = 0;
|
let fetchToken = 0;
|
||||||
|
let fetchCtrl: AbortController | null = null;
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const token = ++fetchToken;
|
const token = ++fetchToken;
|
||||||
|
fetchCtrl?.abort();
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
fetchCtrl = ctrl;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
@@ -74,13 +78,17 @@ async function load() {
|
|||||||
order: order.value,
|
order: order.value,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: (page.value - 1) * pageSize,
|
offset: (page.value - 1) * pageSize,
|
||||||
|
signal: ctrl.signal,
|
||||||
});
|
});
|
||||||
if (token === fetchToken) {
|
if (token === fetchToken) {
|
||||||
items.value = res.items;
|
items.value = res.items;
|
||||||
total.value = res.total;
|
total.value = res.total;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (token === fetchToken) error.value = e instanceof Error ? e.message : '加载失败';
|
// AbortError = 被更新的请求取消,静默(token 检查通常已挡住,这里双保险)
|
||||||
|
if (token === fetchToken && !(e instanceof Error && e.name === 'AbortError')) {
|
||||||
|
error.value = e instanceof Error ? e.message : '加载失败';
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (token === fetchToken) loading.value = false;
|
if (token === fetchToken) loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -169,43 +177,30 @@ function onWatchedChange() {
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 路由同步:状态 → ?q/&market/…(replace 不产生历史记录) ----------
|
// ---------- 路由同步:状态 → ?q/&market/…(骨架在 useQuerySync,replace 不产生历史记录) ----------
|
||||||
function buildQuery(): Record<string, string> {
|
const { syncRoute } = useQuerySync({
|
||||||
const q: Record<string, string> = {};
|
sources: [search, market, sort, order, page],
|
||||||
if (search.value.trim()) q.q = search.value.trim();
|
buildQuery: () => {
|
||||||
if (market.value !== '全部') q.market = market.value;
|
const q: Record<string, string> = {};
|
||||||
if (sort.value !== 'total_mv') q.sort = sort.value;
|
if (search.value.trim()) q.q = search.value.trim();
|
||||||
if (order.value !== 'desc') q.order = order.value;
|
if (market.value !== '全部') q.market = market.value;
|
||||||
if (page.value > 1) q.page = String(page.value);
|
if (sort.value !== 'total_mv') q.sort = sort.value;
|
||||||
if (previewCode.value) q.code = previewCode.value;
|
if (order.value !== 'desc') q.order = order.value;
|
||||||
return q;
|
if (page.value > 1) q.page = String(page.value);
|
||||||
}
|
if (previewCode.value) q.code = previewCode.value;
|
||||||
|
return q;
|
||||||
let selfNav = 0; // 自己发起的导航在途数量:其 route 变化不回灌状态(防输入被旧 URL 覆盖)
|
},
|
||||||
function syncRoute(push = false) {
|
// 浏览器前进/后退(含返回键关掉 ?code=):把 query 应用回状态
|
||||||
const query = buildQuery();
|
applyQuery: (q) => {
|
||||||
// 与当前 URL 一致就跳过,避免 state→route→state 回声
|
search.value = q.q ?? '';
|
||||||
if (JSON.stringify(query) === JSON.stringify(route.query)) return;
|
market.value = MARKETS.includes(q.market ?? '') ? (q.market as typeof MARKETS[number]) : '全部';
|
||||||
selfNav++;
|
const p = parseInt(q.page ?? '', 10);
|
||||||
const done = () => { selfNav--; };
|
page.value = Number.isFinite(p) && p >= 1 ? p : 1;
|
||||||
void (push ? router.push({ query }) : router.replace({ query })).then(done, done);
|
const s = q.sort ?? 'total_mv';
|
||||||
}
|
sort.value = SORT_KEYS.includes(s as SortKey) ? (s as SortKey) : 'total_mv';
|
||||||
|
order.value = q.order === 'asc' ? 'asc' : 'desc';
|
||||||
// 列表状态变化(含搜索防抖外的输入)随手回写 URL;翻页/筛选也带着当前 ?code
|
previewCode.value = q.code || null;
|
||||||
watch([search, market, sort, order, page], () => syncRoute());
|
},
|
||||||
|
|
||||||
// 浏览器前进/后退(含返回键关掉 ?code=):把 query 应用回状态
|
|
||||||
watch(() => route.query, (q) => {
|
|
||||||
if (selfNav > 0) return;
|
|
||||||
const qOf = (k: string) => (typeof q[k] === 'string' ? (q[k] as string) : '');
|
|
||||||
search.value = qOf('q');
|
|
||||||
market.value = MARKETS.includes(qOf('market')) ? qOf('market') : '全部';
|
|
||||||
const p = parseInt(qOf('page'), 10);
|
|
||||||
page.value = Number.isFinite(p) && p >= 1 ? p : 1;
|
|
||||||
const s = qOf('sort');
|
|
||||||
sort.value = SORT_KEYS.includes(s as SortKey) ? (s as SortKey) : 'total_mv';
|
|
||||||
order.value = qOf('order') === 'asc' ? 'asc' : 'desc';
|
|
||||||
previewCode.value = qOf('code') || null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- 详情浮层开关(写入 ?code=) ----------
|
// ---------- 详情浮层开关(写入 ?code=) ----------
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { RouterLink } from 'vue-router';
|
|||||||
|
|
||||||
import MarketOverview from '@/components/MarketOverview.vue';
|
import MarketOverview from '@/components/MarketOverview.vue';
|
||||||
import MarketSyncBar from '@/components/MarketSyncBar.vue';
|
import MarketSyncBar from '@/components/MarketSyncBar.vue';
|
||||||
|
import LimitBoard from '@/components/LimitBoard.vue';
|
||||||
|
|
||||||
// 首页:大盘行情总览 + 功能入口(看股 / ETF / 选股 / 回测)
|
// 首页:功能入口置顶(看股 / ETF / 选股 / 回测 / 概念板块)+ 大盘行情总览 + 打板专题
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
to: '/stocks',
|
to: '/stocks',
|
||||||
@@ -27,6 +28,13 @@ const features = [
|
|||||||
title: '选股',
|
title: '选股',
|
||||||
desc: '用自然语言描述选股条件,快速完成全市场筛选。',
|
desc: '用自然语言描述选股条件,快速完成全市场筛选。',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: '/concepts',
|
||||||
|
icon: 'M7 7h.01M7 3h5c1.4 0 2.6.6 3.4 1.6l2 2.8c.6.8.6 2 0 2.8l-2 2.8c-.8 1-2 1.6-3.4 1.6H7c-2.2 0-4-1.8-4-4V7c0-2.2 1.8-4 4-4zM17 21v-4M14 21h6',
|
||||||
|
accent: 'bg-rose-500/15 text-rose-300',
|
||||||
|
title: '概念板块',
|
||||||
|
desc: '同花顺概念/行业分类,看成分股行情并进入个股研究。',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
to: '/backtest',
|
to: '/backtest',
|
||||||
icon: 'M3 17l6-6 4 4 8-8M21 7v6h-6',
|
icon: 'M3 17l6-6 4 4 8-8M21 7v6h-6',
|
||||||
@@ -39,11 +47,8 @@ const features = [
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full max-w-6xl">
|
<div class="w-full max-w-6xl">
|
||||||
<MarketOverview />
|
<!-- 功能入口置顶:一屏内即可点进看股/选股等页面 -->
|
||||||
|
<div class="mb-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
|
||||||
<MarketSyncBar />
|
|
||||||
|
|
||||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<RouterLink
|
<RouterLink
|
||||||
v-for="f in features"
|
v-for="f in features"
|
||||||
:key="f.to"
|
:key="f.to"
|
||||||
@@ -51,19 +56,25 @@ const features = [
|
|||||||
class="group flex flex-1 flex-col rounded-lg border border-[#26272E] bg-[#101014] p-6 transition-all hover:-translate-y-1 hover:border-[#3A3D46] hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-black sm:p-7"
|
class="group flex flex-1 flex-col rounded-lg border border-[#26272E] bg-[#101014] p-6 transition-all hover:-translate-y-1 hover:border-[#3A3D46] hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-black sm:p-7"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span :class="['flex h-14 w-14 shrink-0 items-center justify-center rounded-lg', f.accent]">
|
<span :class="['flex h-12 w-12 shrink-0 items-center justify-center rounded-lg', f.accent]">
|
||||||
<svg class="h-6 w-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path :d="f.icon" />
|
<path :d="f.icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div class="text-2xl font-semibold">{{ f.title }}</div>
|
<div class="text-xl font-semibold">{{ f.title }}</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-4 max-w-sm text-sm leading-6 text-[#A8AFB8]">{{ f.desc }}</p>
|
<p class="mt-3 text-[13px] leading-5 text-[#A8AFB8]">{{ f.desc }}</p>
|
||||||
<div class="mt-auto flex items-center justify-end gap-1.5 pt-5 text-sm font-medium text-blue-600">
|
<div class="mt-auto flex items-center justify-end gap-1.5 pt-4 text-sm font-medium text-blue-600">
|
||||||
进入
|
进入
|
||||||
<svg class="h-4 w-4 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
|
<svg class="h-4 w-4 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
|
||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MarketOverview />
|
||||||
|
|
||||||
|
<MarketSyncBar />
|
||||||
|
|
||||||
|
<LimitBoard />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const code = computed(() => String(route.params.code ?? ''));
|
|||||||
const detail = ref<IndexDetail | null>(null);
|
const detail = ref<IndexDetail | null>(null);
|
||||||
const weights = ref<IndexWeights | null>(null);
|
const weights = ref<IndexWeights | null>(null);
|
||||||
const weightsError = ref('');
|
const weightsError = ref('');
|
||||||
|
let weightsToken = 0;
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
|
||||||
@@ -40,11 +41,13 @@ async function loadDetail(c: string) {
|
|||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
// 成分权重仅国内指数有数据,且独立加载(失败不阻塞页面)
|
// 成分权重仅国内指数有数据,且独立加载(失败不阻塞页面)。
|
||||||
|
// 带 token 防乱序:快速切换指数时旧请求晚返回不能覆盖新指数的权重表
|
||||||
if (detail.value?.region === 'cn') {
|
if (detail.value?.region === 'cn') {
|
||||||
|
const wToken = ++weightsToken;
|
||||||
getIndexWeights(c, 50)
|
getIndexWeights(c, 50)
|
||||||
.then((w) => { weights.value = w; })
|
.then((w) => { if (wToken === weightsToken) weights.value = w; })
|
||||||
.catch((e) => { weightsError.value = e instanceof Error ? e.message : '获取成分权重失败'; });
|
.catch((e) => { if (wToken === weightsToken) weightsError.value = e instanceof Error ? e.message : '获取成分权重失败'; });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +66,7 @@ const kError = ref<string | null>(null);
|
|||||||
const lastDate = ref('');
|
const lastDate = ref('');
|
||||||
let chart: Chart | null = null;
|
let chart: Chart | null = null;
|
||||||
let loadToken = 0;
|
let loadToken = 0;
|
||||||
|
let kCtrl: AbortController | null = null;
|
||||||
let lastCandles: Candle[] | null = null;
|
let lastCandles: Candle[] | null = null;
|
||||||
|
|
||||||
function rebuild(candles: Candle[]) {
|
function rebuild(candles: Candle[]) {
|
||||||
@@ -94,16 +98,21 @@ function rebuild(candles: Candle[]) {
|
|||||||
|
|
||||||
async function loadCandles(c: string, tf: Timeframe) {
|
async function loadCandles(c: string, tf: Timeframe) {
|
||||||
const token = ++loadToken;
|
const token = ++loadToken;
|
||||||
|
kCtrl?.abort(); // 快速切指数/切周期时取消在途请求
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
kCtrl = ctrl;
|
||||||
kLoading.value = true;
|
kLoading.value = true;
|
||||||
kError.value = null;
|
kError.value = null;
|
||||||
try {
|
try {
|
||||||
const candles = await getAnyIndexCandles(c, tf);
|
const candles = await getAnyIndexCandles(c, tf, { signal: ctrl.signal });
|
||||||
if (token !== loadToken) return;
|
if (token !== loadToken) return;
|
||||||
lastCandles = candles;
|
lastCandles = candles;
|
||||||
rebuild(candles);
|
rebuild(candles);
|
||||||
lastDate.value = candles.length ? candles[candles.length - 1].ts.slice(0, 10) : '';
|
lastDate.value = candles.length ? candles[candles.length - 1].ts.slice(0, 10) : '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (token === loadToken) kError.value = e instanceof Error ? e.message : '获取指数K线失败';
|
if (token === loadToken && !(e instanceof Error && e.name === 'AbortError')) {
|
||||||
|
kError.value = e instanceof Error ? e.message : '获取指数K线失败';
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (token === loadToken) kLoading.value = false;
|
if (token === loadToken) kLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { addWatchlist, getStockFacets, getStocks, removeWatchlist } from '@/api/client';
|
import { addWatchlist, getStockFacets, getStocks, removeWatchlist } from '@/api/client';
|
||||||
import type { FacetItem, ScreenerItemOut, StockListItem } from '@/api/types';
|
import type { FacetItem, ScreenerItemOut, StockListItem } from '@/api/types';
|
||||||
|
import { useQuerySync } from '@/composables/useQuerySync';
|
||||||
import StockDetailOverlay from '@/components/StockDetailOverlay.vue';
|
import StockDetailOverlay from '@/components/StockDetailOverlay.vue';
|
||||||
|
|
||||||
// ---------- 筛选状态(初始值从路由 query 还原,刷新/分享链接不丢现场) ----------
|
// ---------- 筛选状态(初始值从路由 query 还原,刷新/分享链接不丢现场) ----------
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
function qStr(key: string): string | undefined {
|
function qStr(key: string): string | undefined {
|
||||||
const v = route.query[key];
|
const v = route.query[key];
|
||||||
@@ -60,10 +60,14 @@ const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize))
|
|||||||
|
|
||||||
// ---------- 加载(搜索防抖) ----------
|
// ---------- 加载(搜索防抖) ----------
|
||||||
let fetchToken = 0;
|
let fetchToken = 0;
|
||||||
|
let fetchCtrl: AbortController | null = null;
|
||||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const token = ++fetchToken;
|
const token = ++fetchToken;
|
||||||
|
fetchCtrl?.abort();
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
fetchCtrl = ctrl;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
@@ -78,13 +82,17 @@ async function load() {
|
|||||||
order: order.value,
|
order: order.value,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: (page.value - 1) * pageSize,
|
offset: (page.value - 1) * pageSize,
|
||||||
|
signal: ctrl.signal,
|
||||||
});
|
});
|
||||||
if (token === fetchToken) {
|
if (token === fetchToken) {
|
||||||
items.value = res.items;
|
items.value = res.items;
|
||||||
total.value = res.total;
|
total.value = res.total;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (token === fetchToken) error.value = e instanceof Error ? e.message : '加载失败';
|
// AbortError = 被更新的请求取消,静默(token 检查通常已挡住,这里双保险)
|
||||||
|
if (token === fetchToken && !(e instanceof Error && e.name === 'AbortError')) {
|
||||||
|
error.value = e instanceof Error ? e.message : '加载失败';
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (token === fetchToken) loading.value = false;
|
if (token === fetchToken) loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -191,47 +199,34 @@ function onWatchedChange() {
|
|||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 路由同步:状态 → ?q/&market/…(replace 不产生历史记录) ----------
|
// ---------- 路由同步:状态 → ?q/&market/…(骨架在 useQuerySync,replace 不产生历史记录) ----------
|
||||||
function buildQuery(): Record<string, string> {
|
const { syncRoute } = useQuerySync({
|
||||||
const q: Record<string, string> = {};
|
sources: [search, market, industry, area, sort, order, page],
|
||||||
if (search.value.trim()) q.q = search.value.trim();
|
buildQuery: () => {
|
||||||
if (market.value !== '全部') q.market = market.value;
|
const q: Record<string, string> = {};
|
||||||
if (industry.value) q.industry = industry.value;
|
if (search.value.trim()) q.q = search.value.trim();
|
||||||
if (area.value) q.area = area.value;
|
if (market.value !== '全部') q.market = market.value;
|
||||||
if (sort.value !== 'symbol') q.sort = sort.value;
|
if (industry.value) q.industry = industry.value;
|
||||||
if (order.value !== 'asc') q.order = order.value;
|
if (area.value) q.area = area.value;
|
||||||
if (page.value > 1) q.page = String(page.value);
|
if (sort.value !== 'symbol') q.sort = sort.value;
|
||||||
if (previewCode.value) q.code = previewCode.value;
|
if (order.value !== 'asc') q.order = order.value;
|
||||||
return q;
|
if (page.value > 1) q.page = String(page.value);
|
||||||
}
|
if (previewCode.value) q.code = previewCode.value;
|
||||||
|
return q;
|
||||||
let selfNav = 0; // 自己发起的导航在途数量:其 route 变化不回灌状态(防输入被旧 URL 覆盖)
|
},
|
||||||
function syncRoute(push = false) {
|
// 浏览器前进/后退(含返回键关掉 ?code=):把 query 应用回状态
|
||||||
const query = buildQuery();
|
applyQuery: (q) => {
|
||||||
// 与当前 URL 一致就跳过,避免 state→route→state 回声
|
search.value = q.q ?? '';
|
||||||
if (JSON.stringify(query) === JSON.stringify(route.query)) return;
|
market.value = MARKETS.includes(q.market ?? '') ? (q.market as typeof MARKETS[number]) : '全部';
|
||||||
selfNav++;
|
industry.value = q.industry ?? '';
|
||||||
const done = () => { selfNav--; };
|
area.value = q.area ?? '';
|
||||||
void (push ? router.push({ query }) : router.replace({ query })).then(done, done);
|
const p = parseInt(q.page ?? '', 10);
|
||||||
}
|
page.value = Number.isFinite(p) && p >= 1 ? p : 1;
|
||||||
|
const s = q.sort ?? 'symbol';
|
||||||
// 列表状态变化(含搜索防抖外的输入)随手回写 URL;翻页/筛选也带着当前 ?code
|
sort.value = SORT_KEYS.includes(s as SortKey) ? (s as SortKey) : 'symbol';
|
||||||
watch([search, market, industry, area, sort, order, page], () => syncRoute());
|
order.value = q.order === 'desc' ? 'desc' : 'asc';
|
||||||
|
previewCode.value = q.code || null;
|
||||||
// 浏览器前进/后退(含返回键关掉 ?code=):把 query 应用回状态
|
},
|
||||||
watch(() => route.query, (q) => {
|
|
||||||
if (selfNav > 0) return;
|
|
||||||
const qOf = (k: string) => (typeof q[k] === 'string' ? (q[k] as string) : '');
|
|
||||||
search.value = qOf('q');
|
|
||||||
market.value = MARKETS.includes(qOf('market')) ? qOf('market') : '全部';
|
|
||||||
industry.value = qOf('industry');
|
|
||||||
area.value = qOf('area');
|
|
||||||
const p = parseInt(qOf('page'), 10);
|
|
||||||
page.value = Number.isFinite(p) && p >= 1 ? p : 1;
|
|
||||||
const s = qOf('sort');
|
|
||||||
sort.value = SORT_KEYS.includes(s as SortKey) ? (s as SortKey) : 'symbol';
|
|
||||||
order.value = qOf('order') === 'desc' ? 'desc' : 'asc';
|
|
||||||
previewCode.value = qOf('code') || null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- 详情浮层开关(写入 ?code=) ----------
|
// ---------- 详情浮层开关(写入 ?code=) ----------
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
$ vite
|
|
||||||
|
|
||||||
[32m[1mVITE[22m v6.4.3[39m [2mready in [0m[1m1749[22m[2m[0m ms[22m
|
|
||||||
|
|
||||||
[32m➜[39m [1mLocal[22m: [36mhttp://localhost:[1m5173[22m/[39m
|
|
||||||
[2m [32m➜[39m [1mNetwork[22m[2m: use [22m[1m--host[22m[2m to expose[22m
|
|
||||||
[2m18:31:29[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/types.ts[22m
|
|
||||||
[2m18:31:29[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m18:31:31[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m18:31:45[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m18:31:48[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m19:07:53[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/types.ts[22m
|
|
||||||
[2m19:07:58[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m19:08:04[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m19:08:48[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\MarketOverview.vue, /src/style.css[22m
|
|
||||||
[2m19:08:54[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\MarketOverview.vue, /src/style.css[22m
|
|
||||||
[2m19:09:01[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\MarketOverview.vue, /src/style.css[22m
|
|
||||||
[2m19:09:07[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\MarketOverview.vue, /src/style.css[22m
|
|
||||||
[2m19:09:11[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\MarketOverview.vue, /src/style.css[22m
|
|
||||||
[2m19:11:04[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/router.ts[22m
|
|
||||||
[2m19:29:33[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/views/IndexDetailView.vue, /src/style.css[22m
|
|
||||||
[2m19:29:35[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/views/IndexDetailView.vue, /src/style.css[22m
|
|
||||||
[2m01:50:18[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\MarketOverview.vue, /src/style.css[22m
|
|
||||||
[2m01:50:23[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\MarketOverview.vue, /src/style.css[22m
|
|
||||||
[2m02:16:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/types.ts[22m
|
|
||||||
[2m02:16:33[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/types.ts[22m
|
|
||||||
[2m02:16:36[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m02:16:46[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m02:16:47[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/stores/settings.ts[22m
|
|
||||||
[2m02:16:48[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/stores/settings.ts[22m
|
|
||||||
[2m02:17:18[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:17:20[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:17:40[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:17:41[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:17:42[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:17:43[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:17:43[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:17:44[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\DetailKLine.vue, /src/style.css[22m
|
|
||||||
[2m02:18:28[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:18:30[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:18:35[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:18:49[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:18:55[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:18:57[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:18:57[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:19:09[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:19:09[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m02:24:43[22m [31m[1m[vite][22m[39m [31mhttp proxy error: /api/auth/me[39m
|
|
||||||
AggregateError [ECONNREFUSED]:
|
|
||||||
at internalConnectMultiple (node:net:1134:18)
|
|
||||||
at afterConnectMultiple (node:net:1715:7)
|
|
||||||
[2m07:54:29[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/types.ts[22m
|
|
||||||
[2m07:54:32[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m07:54:39[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mpage reload [39m[2msrc/api/client.ts[22m
|
|
||||||
[2m07:55:37[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m07:55:38[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
[2m08:25:59[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\ReferencePanel.vue, /src/style.css[22m
|
|
||||||
[2m12:48:22[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\ReferencePanel.vue, /src/style.css[22m
|
|
||||||
[2m12:48:23[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\ReferencePanel.vue, /src/style.css[22m
|
|
||||||
[2m12:56:52[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/src/style.css, /@fs/D:\Project\stock\frontend\src\components\ReferencePanel.vue, /@fs/D:\Project\stock\frontend\src\components\ConditionChips.vue[22m
|
|
||||||
[2m12:56:59[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\ReferencePanel.vue, /src/style.css[22m
|
|
||||||
[2m13:30:58[22m [36m[1m[vite][22m[39m [90m[2m(client)[22m[39m [32mhmr update [39m[2m/@fs/D:\Project\stock\frontend\src\components\StockDetailOverlay.vue, /src/style.css[22m
|
|
||||||
Reference in New Issue
Block a user