feat: Phase 3 Web — 业务 API(stocks/factors/backtests)+ Next.js 前端
- 后端业务 API:GET /api/stocks(搜索/分页)、GET /api/factors(因子目录)、POST /api/factor-tests 与 /api/backtests(Research Spec 驱动同步执行)、GET /api/backtests/last;Annotated 依赖注入 + CORS(dev) - Repository 批量查询 get_range_many(研究装配一次查询,避免逐只拉取) - 前端 frontend/web:Next.js 15(TS) + ECharts —— 总览 / 股票池 / 因子研究(IC·RankIC·分层展示) / 回测(净值·回撤·月度·持仓·未建模标注) - 前端只消费业务 API 与标准化 BacktestResult,无 Qlib/SQL 概念泄漏 - 真实数据:同步 20 只权重股 2023-2024 日线(9680 根)支撑截面研究 - 验证:API 集成测试 8 项(DTO 校验/装配/引擎/标准结果,内存 repo 全链路)+ 全量 pytest 68 passed;前端 tsc + next build 通过;无头浏览器端到端(factors/backtest 页面渲染后端数据) - ruff clean
This commit is contained in:
@@ -17,6 +17,7 @@ build/
|
||||
# ================= 密钥 / 凭证 =================
|
||||
# 真实密钥只放根目录 .env(复制自 .env.example),严禁提交
|
||||
.env
|
||||
.env.local
|
||||
*.pem
|
||||
*.key
|
||||
|
||||
@@ -48,6 +49,7 @@ experiments/*
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.tsbuildinfo
|
||||
|
||||
# ================= archify 视觉验证副产物(重新生成即可) =================
|
||||
docs/diagrams/*.visual-check.*
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""API 依赖注入:Repository / 研究服务的装配点(composition root 的一部分)。
|
||||
|
||||
路由层统一使用 Annotated 注入(FastAPI 推荐写法,配合 ruff B008 无冲突)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.domain.repositories.market import (
|
||||
DailyBarRepository,
|
||||
StockRepository,
|
||||
)
|
||||
from app.infrastructure.persistence.sqlalchemy.repositories.market_impl import (
|
||||
SqlAlchemyDailyBarRepository,
|
||||
SqlAlchemyStockRepository,
|
||||
)
|
||||
from app.infrastructure.persistence.sqlalchemy.session import get_session
|
||||
from app.quant.engine import LocalEngine, QuantEngine
|
||||
from app.quant.service import ResearchService
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_session)]
|
||||
|
||||
|
||||
def _stock_repo_factory(session: DbSession) -> StockRepository:
|
||||
return SqlAlchemyStockRepository(session)
|
||||
|
||||
|
||||
def _daily_repo_factory(session: DbSession) -> DailyBarRepository:
|
||||
return SqlAlchemyDailyBarRepository(session)
|
||||
|
||||
|
||||
def _engine_factory() -> QuantEngine:
|
||||
return LocalEngine()
|
||||
|
||||
|
||||
def _service_factory(
|
||||
stock_repo: Annotated[StockRepository, Depends(_stock_repo_factory)],
|
||||
daily_repo: Annotated[DailyBarRepository, Depends(_daily_repo_factory)],
|
||||
engine: Annotated[QuantEngine, Depends(_engine_factory)],
|
||||
) -> ResearchService:
|
||||
return ResearchService(stock_repo, daily_repo, engine)
|
||||
|
||||
|
||||
StockRepoDep = Annotated[StockRepository, Depends(_stock_repo_factory)]
|
||||
DailyRepoDep = Annotated[DailyBarRepository, Depends(_daily_repo_factory)]
|
||||
EngineDep = Annotated[QuantEngine, Depends(_engine_factory)]
|
||||
ResearchServiceDep = Annotated[ResearchService, Depends(_service_factory)]
|
||||
@@ -0,0 +1,25 @@
|
||||
"""因子目录 API:/api/factors。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.quant.factors import list_factors
|
||||
|
||||
router = APIRouter(prefix="/factors", tags=["factors"])
|
||||
|
||||
|
||||
@router.get("", summary="因子目录(含元数据)")
|
||||
def list_factor_catalog() -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"name": d.name,
|
||||
"description": d.description,
|
||||
"formula": d.formula,
|
||||
"frequency": d.frequency,
|
||||
"lookback": d.lookback,
|
||||
"direction": d.direction,
|
||||
"requires": list(d.requires),
|
||||
}
|
||||
for d in list_factors()
|
||||
]
|
||||
@@ -0,0 +1,56 @@
|
||||
"""研究执行 API:/api/factor-tests 与 /api/backtests。
|
||||
|
||||
Phase 3 为同步执行(样本有限);Phase 4 将改为 Job + SSE 异步(接口契约不变)。
|
||||
最近一次结果在内存中可读,便于前端展示;持久化实验归档在 Phase 4。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from app.api.deps import ResearchServiceDep
|
||||
from app.domain.entities.research import (
|
||||
BacktestResult,
|
||||
FactorTestReport,
|
||||
ResearchSpec,
|
||||
)
|
||||
from app.quant.factors import FactorError
|
||||
|
||||
router = APIRouter(tags=["research"])
|
||||
|
||||
# 内存中的最近结果(Phase 4 迁移到 Experiment 表)
|
||||
_LAST_BACKTEST: dict[str, BacktestResult] = {}
|
||||
_LAST_FACTOR_TEST: dict[str, FactorTestReport] = {}
|
||||
|
||||
|
||||
@router.post("/factor-tests", response_model=FactorTestReport, summary="运行单因子测试(同步)")
|
||||
def run_factor_test(
|
||||
spec: ResearchSpec,
|
||||
service: ResearchServiceDep,
|
||||
) -> FactorTestReport:
|
||||
try:
|
||||
report = service.run_factor_test(spec)
|
||||
except (ValueError, FactorError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
_LAST_FACTOR_TEST["default"] = report
|
||||
return report
|
||||
|
||||
|
||||
@router.post("/backtests", response_model=BacktestResult, summary="运行回测(同步)")
|
||||
def run_backtest(
|
||||
spec: ResearchSpec,
|
||||
service: ResearchServiceDep,
|
||||
) -> BacktestResult:
|
||||
try:
|
||||
result = service.run_backtest(spec)
|
||||
except (ValueError, FactorError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
_LAST_BACKTEST["default"] = result
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/backtests/last", response_model=BacktestResult, summary="最近一次回测结果")
|
||||
def last_backtest() -> BacktestResult:
|
||||
if "default" not in _LAST_BACKTEST:
|
||||
raise HTTPException(status_code=404, detail="尚无回测结果,请先 POST /api/backtests")
|
||||
return _LAST_BACKTEST["default"]
|
||||
@@ -1,14 +1,17 @@
|
||||
"""API 路由聚合。
|
||||
|
||||
后续业务路由按 AGENT.md §17 面向业务对象挂载:
|
||||
/api/stocks /api/universes /api/factors /api/strategies /api/backtests /api/experiments /api/jobs
|
||||
业务路由面向业务对象(AGENT.md §17):/api/stocks /api/factors
|
||||
/api/factor-tests /api/backtests /api/experiments(Phase4) /api/jobs(Phase4) /api/agent(Phase5)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import health
|
||||
from app.api import factors, health, research, stocks
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health.router)
|
||||
api_router.include_router(stocks.router)
|
||||
api_router.include_router(factors.router)
|
||||
api_router.include_router(research.router)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""股票查询 API:/api/stocks。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from app.api.deps import StockRepoDep
|
||||
from app.domain.entities.market import Stock
|
||||
|
||||
router = APIRouter(prefix="/stocks", tags=["stocks"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[Stock], summary="股票列表")
|
||||
def list_stocks(
|
||||
repo: StockRepoDep,
|
||||
q: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> list[Stock]:
|
||||
if limit > 500:
|
||||
limit = 500
|
||||
stocks = repo.list()
|
||||
if q:
|
||||
needle = q.upper()
|
||||
stocks = [s for s in stocks if needle in s.symbol or needle in s.name.upper()]
|
||||
return stocks[offset : offset + limit]
|
||||
|
||||
|
||||
@router.get("/{symbol}", response_model=Stock, summary="按代码查询")
|
||||
def get_stock(symbol: str, repo: StockRepoDep) -> Stock:
|
||||
stock = repo.get_by_symbol(symbol)
|
||||
if stock is None:
|
||||
raise HTTPException(status_code=404, detail=f"未找到股票 {symbol}")
|
||||
return stock
|
||||
@@ -42,6 +42,9 @@ class DailyBarRepository(Protocol):
|
||||
|
||||
def get_range(self, symbol: str, start: date, end: date) -> list[DailyBar]: ...
|
||||
|
||||
def get_range_many(self, symbols: Sequence[str], start: date, end: date) -> list[DailyBar]:
|
||||
"""批量区间查询(研究服务装配面板用,避免逐只查询)。"""
|
||||
|
||||
def latest_date(self, symbol: str) -> date | None:
|
||||
"""断点续传用:该股票本地已有数据的最新交易日。"""
|
||||
|
||||
|
||||
@@ -140,6 +140,18 @@ class SqlAlchemyDailyBarRepository:
|
||||
).all()
|
||||
return [DailyBar.model_validate(r, from_attributes=True) for r in rows]
|
||||
|
||||
def get_range_many(self, symbols: Sequence[str], start: date, end: date) -> list[DailyBar]:
|
||||
rows = self._session.scalars(
|
||||
select(StockDailyModel)
|
||||
.where(
|
||||
StockDailyModel.symbol.in_(list(symbols)),
|
||||
StockDailyModel.trade_date >= start,
|
||||
StockDailyModel.trade_date <= end,
|
||||
)
|
||||
.order_by(StockDailyModel.trade_date)
|
||||
).all()
|
||||
return [DailyBar.model_validate(r, from_attributes=True) for r in rows]
|
||||
|
||||
def latest_date(self, symbol: str) -> date | None:
|
||||
return self._session.scalar(
|
||||
select(StockDailyModel.trade_date)
|
||||
|
||||
+11
-1
@@ -6,6 +6,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.api.router import api_router
|
||||
from app.core.config import get_settings
|
||||
@@ -15,7 +16,16 @@ settings = get_settings()
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
version=settings.app_version,
|
||||
description="A股个人量化研究平台 API(Qlib 引擎 / Tushare 数据源)",
|
||||
description="A股个人量化研究平台 API(研究引擎 / Tushare 数据源)",
|
||||
)
|
||||
|
||||
# 开发期允许本地前端跨域(Phase 3 前端 dev server;上线收紧为白名单)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
|
||||
allow_credentials=False,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(api_router, prefix=settings.api_prefix)
|
||||
|
||||
@@ -326,6 +326,7 @@ def run_spec_factor_test(
|
||||
panels = build_factor_panels(daily, spec.factors)
|
||||
panel = panels[0][1]
|
||||
close = daily.pivot(index="trade_date", columns="symbol", values="close").sort_index()
|
||||
close.index = pd.to_datetime(close.index)
|
||||
forward = close.shift(-horizon_days) / close - 1.0
|
||||
report = run_factor_test(panel, forward, factor_name=factor_name)
|
||||
return report, {factor_name: panel}
|
||||
|
||||
@@ -87,7 +87,13 @@ class ResearchService:
|
||||
# 回测前预留因子 warmup(lookback≤120 交易日,取 300 自然日余量)
|
||||
data_start = start - timedelta(days=300)
|
||||
stocks = filter_stocks(self._stock_repo.list(), spec.universe, as_of=start)
|
||||
bars: list = []
|
||||
for s in stocks:
|
||||
bars.extend(self._daily_repo.get_range(s.symbol, data_start, end))
|
||||
if not stocks:
|
||||
return pd.DataFrame()
|
||||
get_many = getattr(self._daily_repo, "get_range_many", None)
|
||||
if get_many is not None:
|
||||
bars = list(get_many([s.symbol for s in stocks], data_start, end))
|
||||
else: # 兜底:逐只查询
|
||||
bars = []
|
||||
for s in stocks:
|
||||
bars.extend(self._daily_repo.get_range(s.symbol, data_start, end))
|
||||
return bars_to_daily_df(bars)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
"""API 集成测试:/api/stocks、/api/factors、/api/backtests、/api/factor-tests。
|
||||
|
||||
使用内存 Repository / 合成行情替换真实 DB 依赖(override 装配工厂),
|
||||
引擎为真实 LocalEngine —— 覆盖「DTO 校验 → 装配 → 引擎 → 标准结果」链路。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
from app.api import deps
|
||||
from app.domain.entities.market import Stock
|
||||
from app.main import app
|
||||
from app.quant.engine import LocalEngine
|
||||
from app.quant.service import ResearchService
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from conftest_quant import bars_dataframe_to_daily_bars, synthetic_daily
|
||||
|
||||
|
||||
class _MemStockRepo:
|
||||
def __init__(self, stocks: list[Stock]) -> None:
|
||||
self._stocks = stocks
|
||||
|
||||
def get_by_symbol(self, symbol: str) -> Stock | None:
|
||||
return next((s for s in self._stocks if s.symbol == symbol), None)
|
||||
|
||||
def list(self) -> list[Stock]:
|
||||
return self._stocks
|
||||
|
||||
|
||||
_SYMS = ["60000" + str(i) + ".SH" for i in range(5)] # 600000~600004
|
||||
|
||||
|
||||
def _mem_stocks() -> list[Stock]:
|
||||
return [
|
||||
Stock(symbol=sym, name=f"测试股份{i}", list_date=date(1999, 11, 10))
|
||||
for i, sym in enumerate(_SYMS)
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client() -> TestClient:
|
||||
drifts = {sym: 0.003 - 0.0015 * i for i, sym in enumerate(_SYMS)}
|
||||
daily_df = synthetic_daily(drifts, n=300)
|
||||
bars = bars_dataframe_to_daily_bars(daily_df)
|
||||
|
||||
class _MemDailyRepo:
|
||||
def get_range_many(self, symbols, start, end):
|
||||
out = []
|
||||
for b in bars:
|
||||
if b.symbol in symbols and start <= b.trade_date <= end:
|
||||
out.append(b)
|
||||
return out
|
||||
|
||||
def get_range(self, symbol, start, end):
|
||||
return [b for b in bars if b.symbol == symbol and start <= b.trade_date <= end]
|
||||
|
||||
service = ResearchService(_MemStockRepo(_mem_stocks()), _MemDailyRepo(), LocalEngine())
|
||||
app.dependency_overrides[deps._stock_repo_factory] = lambda: _MemStockRepo(_mem_stocks()) # noqa: SLF001
|
||||
app.dependency_overrides[deps._service_factory] = lambda: service # noqa: SLF001
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
_BACKTEST_BODY = {
|
||||
"type": "backtest",
|
||||
"universe": {"exclude_st": False, "min_listing_days": 0},
|
||||
"factors": [{"name": "momentum_20", "weight": 1.0}],
|
||||
"selection": {"top_n": 1},
|
||||
"rebalance": "monthly",
|
||||
"period": ["2024-03-01", "2024-10-31"],
|
||||
}
|
||||
|
||||
|
||||
class TestStocksApi:
|
||||
def test_list(self, client: TestClient) -> None:
|
||||
resp = client.get("/api/stocks?limit=10")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert len(body) == 5
|
||||
assert body[0]["symbol"]
|
||||
assert body[0]["name"]
|
||||
|
||||
def test_list_search(self, client: TestClient) -> None:
|
||||
resp = client.get("/api/stocks?q=600000")
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()) == 1
|
||||
assert resp.json()[0]["symbol"] == "600000.SH"
|
||||
|
||||
def test_get_one_and_missing(self, client: TestClient) -> None:
|
||||
assert client.get("/api/stocks/600000.SH").status_code == 200
|
||||
assert client.get("/api/stocks/999999.SZ").status_code == 404
|
||||
|
||||
|
||||
class TestFactorsApi:
|
||||
def test_catalog(self, client: TestClient) -> None:
|
||||
resp = client.get("/api/factors")
|
||||
assert resp.status_code == 200
|
||||
names = {f["name"] for f in resp.json()}
|
||||
assert "momentum_20" in names
|
||||
meta = next(f for f in resp.json() if f["name"] == "momentum_60")
|
||||
assert meta["lookback"] == 60
|
||||
assert meta["direction"] in {"higher_is_better", "lower_is_better"}
|
||||
|
||||
|
||||
class TestResearchApi:
|
||||
def test_backtest_roundtrip(self, client: TestClient) -> None:
|
||||
resp = client.post("/api/backtests", json=_BACKTEST_BODY)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["summary"]["total_return_pct"] > 0
|
||||
assert body["equity_curve"]
|
||||
assert body["unimplemented"]
|
||||
# 最近结果可读
|
||||
last = client.get("/api/backtests/last")
|
||||
assert last.status_code == 200
|
||||
assert last.json()["summary"] == body["summary"]
|
||||
|
||||
def test_factor_test_roundtrip(self, client: TestClient) -> None:
|
||||
body = dict(_BACKTEST_BODY)
|
||||
body["type"] = "factor_test"
|
||||
resp = client.post("/api/factor-tests", json=body)
|
||||
assert resp.status_code == 200
|
||||
report = resp.json()
|
||||
assert report["factor_name"] == "momentum_20"
|
||||
assert report["sample_days"] > 5
|
||||
assert report["ic_mean"] > 0 # 合成数据为强趋势
|
||||
|
||||
def test_invalid_spec_422(self, client: TestClient) -> None:
|
||||
bad = dict(_BACKTEST_BODY)
|
||||
bad["period"] = ["2024-10-01", "2024-03-01"] # start > end
|
||||
assert client.post("/api/backtests", json=bad).status_code == 422
|
||||
|
||||
def test_unknown_factor_400(self, client: TestClient) -> None:
|
||||
bad = dict(_BACKTEST_BODY)
|
||||
bad["factors"] = [{"name": "no_such_factor", "weight": 1.0}]
|
||||
resp = client.post("/api/backtests", json=bad)
|
||||
assert resp.status_code == 400
|
||||
@@ -0,0 +1,2 @@
|
||||
# 后端 API 基础地址(开发期)
|
||||
NEXT_PUBLIC_API_BASE=http://127.0.0.1:8000/api
|
||||
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiGet, apiPost } from "@/lib/api";
|
||||
import type { BacktestResult, FactorMeta, ResearchSpec } from "@/lib/types";
|
||||
import { LineChart } from "@/components/LineChart";
|
||||
|
||||
export default function BacktestPage() {
|
||||
const [factors, setFactors] = useState<FactorMeta[]>([]);
|
||||
const [factor, setFactor] = useState("momentum_60");
|
||||
const [topN, setTopN] = useState(5);
|
||||
const [rebalance, setRebalance] = useState<"monthly" | "weekly">("monthly");
|
||||
const [start, setStart] = useState("2024-03-01");
|
||||
const [end, setEnd] = useState("2024-12-31");
|
||||
const [excludeSt, setExcludeSt] = useState(true);
|
||||
const [result, setResult] = useState<BacktestResult | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
apiGet<FactorMeta[]>("/factors")
|
||||
.then(setFactors)
|
||||
.catch((e: Error) => setError(e.message));
|
||||
}, []);
|
||||
|
||||
function run() {
|
||||
setRunning(true);
|
||||
setError("");
|
||||
const spec: ResearchSpec = {
|
||||
type: "backtest",
|
||||
universe: { exclude_st: excludeSt, min_listing_days: 0 },
|
||||
factors: [{ name: factor, weight: 1 }],
|
||||
selection: { top_n: topN },
|
||||
rebalance,
|
||||
period: [start, end],
|
||||
};
|
||||
apiPost<BacktestResult>("/backtests", spec)
|
||||
.then(setResult)
|
||||
.catch((e: Error) => setError(e.message))
|
||||
.finally(() => setRunning(false));
|
||||
}
|
||||
|
||||
const s = result?.summary;
|
||||
return (
|
||||
<>
|
||||
<h1>选股回测</h1>
|
||||
<div className="card">
|
||||
<h2>研究参数(Research Specification)</h2>
|
||||
<div className="form-grid">
|
||||
<label className="muted">
|
||||
因子
|
||||
<select value={factor} onChange={(e) => setFactor(e.target.value)}>
|
||||
{factors.map((f) => (
|
||||
<option key={f.name} value={f.name}>
|
||||
{f.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="muted">
|
||||
Top N
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={topN}
|
||||
onChange={(e) => setTopN(Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<label className="muted">
|
||||
调仓
|
||||
<select
|
||||
value={rebalance}
|
||||
onChange={(e) => setRebalance(e.target.value as "monthly" | "weekly")}
|
||||
>
|
||||
<option value="monthly">月度</option>
|
||||
<option value="weekly">周度</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="muted">
|
||||
开始
|
||||
<input type="date" value={start} onChange={(e) => setStart(e.target.value)} />
|
||||
</label>
|
||||
<label className="muted">
|
||||
结束
|
||||
<input type="date" value={end} onChange={(e) => setEnd(e.target.value)} />
|
||||
</label>
|
||||
<label className="muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={excludeSt}
|
||||
onChange={(e) => setExcludeSt(e.target.checked)}
|
||||
/>{" "}
|
||||
剔除 ST
|
||||
</label>
|
||||
<button className="primary" onClick={run} disabled={running}>
|
||||
{running ? "运行中…" : "运行回测"}
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className="error">{error}</div>}
|
||||
</div>
|
||||
|
||||
{result && s && (
|
||||
<>
|
||||
<div className="card">
|
||||
<h2>
|
||||
结果 · {s.total_return_pct.toFixed(2)}%{" "}
|
||||
<span className={s.total_return_pct >= 0 ? "good" : "bad"}>
|
||||
{s.total_return_pct >= 0 ? "▲" : "▼"}
|
||||
</span>
|
||||
</h2>
|
||||
<div className="stat-grid">
|
||||
<div className="stat">
|
||||
<div className="label">期末净值</div>
|
||||
<div className="value">{s.final_equity.toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">年化收益</div>
|
||||
<div className="value">{s.annual_return_pct.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">Sharpe</div>
|
||||
<div className="value">{s.sharpe.toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">最大回撤</div>
|
||||
<div className="value bad">{s.max_drawdown_pct.toFixed(2)}%</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">胜率(按交易)</div>
|
||||
<div className="value">{s.win_rate_pct.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">交易笔数 / 换手</div>
|
||||
<div className="value">
|
||||
{s.total_trades} / {s.avg_turnover_pct.toFixed(0)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<LineChart title="净值曲线" data={result.equity_curve} color="#34d399" />
|
||||
</div>
|
||||
<LineChart
|
||||
title="回撤(%)"
|
||||
data={result.drawdown}
|
||||
color="#f87171"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2>月度收益(%)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>月份</th>
|
||||
<th>收益</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.monthly_returns.map((m) => (
|
||||
<tr key={`${m.year}-${m.month}`}>
|
||||
<td>
|
||||
{m.year}-{String(m.month).padStart(2, "0")}
|
||||
</td>
|
||||
<td className={m.return_pct >= 0 ? "good" : "bad"}>
|
||||
{m.return_pct.toFixed(2)}%
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2>最新持仓</h2>
|
||||
{(() => {
|
||||
const last = new Map<string, number>();
|
||||
for (const p of result.positions) last.set(p.date, 0);
|
||||
const lastDate = [...last.keys()].at(-1);
|
||||
const pos = result.positions.filter((p) => p.date === lastDate);
|
||||
return (
|
||||
<div className="row">
|
||||
{pos.map((p) => (
|
||||
<span key={p.symbol} className="stat">
|
||||
{p.symbol} · {(p.weight * 100).toFixed(0)}%
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2>未建模约束(如实标注)</h2>
|
||||
<ul className="muted">
|
||||
{result.unimplemented.map((u, i) => (
|
||||
<li key={i}>{u}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiGet, apiPost } from "@/lib/api";
|
||||
import type { FactorMeta, FactorTestReport, ResearchSpec } from "@/lib/types";
|
||||
|
||||
export default function FactorsPage() {
|
||||
const [factors, setFactors] = useState<FactorMeta[]>([]);
|
||||
const [name, setName] = useState("momentum_60");
|
||||
const [start, setStart] = useState("2024-03-01");
|
||||
const [end, setEnd] = useState("2024-12-31");
|
||||
const [report, setReport] = useState<FactorTestReport | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
apiGet<FactorMeta[]>("/factors")
|
||||
.then(setFactors)
|
||||
.catch((e: Error) => setError(e.message));
|
||||
}, []);
|
||||
|
||||
function run() {
|
||||
setRunning(true);
|
||||
setError("");
|
||||
const spec: ResearchSpec = {
|
||||
type: "factor_test",
|
||||
universe: { exclude_st: true, min_listing_days: 0 },
|
||||
factors: [{ name, weight: 1 }],
|
||||
selection: { top_n: 10 },
|
||||
rebalance: "monthly",
|
||||
period: [start, end],
|
||||
};
|
||||
apiPost<FactorTestReport>("/factor-tests", spec)
|
||||
.then(setReport)
|
||||
.catch((e: Error) => setError(e.message))
|
||||
.finally(() => setRunning(false));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>因子研究</h1>
|
||||
<div className="card">
|
||||
<h2>因子目录(行情因子 · 无未来函数)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>描述</th>
|
||||
<th>公式</th>
|
||||
<th>回看</th>
|
||||
<th>方向</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{factors.map((f) => (
|
||||
<tr key={f.name}>
|
||||
<td>{f.name}</td>
|
||||
<td>{f.description}</td>
|
||||
<td className="muted">{f.formula}</td>
|
||||
<td>{f.lookback}</td>
|
||||
<td>{f.direction === "higher_is_better" ? "高为好" : "低为好"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2>运行单因子测试(IC / RankIC / 分层)</h2>
|
||||
<div className="row">
|
||||
<select value={name} onChange={(e) => setName(e.target.value)}>
|
||||
{factors.map((f) => (
|
||||
<option key={f.name} value={f.name}>
|
||||
{f.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="muted">
|
||||
开始 <input type="date" value={start} onChange={(e) => setStart(e.target.value)} />
|
||||
</label>
|
||||
<label className="muted">
|
||||
结束 <input type="date" value={end} onChange={(e) => setEnd(e.target.value)} />
|
||||
</label>
|
||||
<button className="primary" onClick={run} disabled={running}>
|
||||
{running ? "运行中…" : "运行因子测试"}
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
{report && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<div className="stat-grid">
|
||||
<div className="stat">
|
||||
<div className="label">IC 均值</div>
|
||||
<div className="value">{report.ic_mean.toFixed(4)}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">RankIC 均值</div>
|
||||
<div className="value">{report.rank_ic_mean.toFixed(4)}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">ICIR</div>
|
||||
<div className="value">{report.icir.toFixed(3)}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">正收益占比</div>
|
||||
<div className="value">{report.positive_ratio_pct.toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">样本日数</div>
|
||||
<div className="value">{report.sample_days}</div>
|
||||
</div>
|
||||
</div>
|
||||
{report.quantile_returns.length > 0 && (
|
||||
<table style={{ marginTop: 12 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分层(1 最低 → 5 最高)</th>
|
||||
{report.quantile_returns.map((q) => (
|
||||
<th key={q.quantile}>Q{q.quantile + 1}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>未来 21 日平均收益</td>
|
||||
{report.quantile_returns.map((q) => (
|
||||
<td key={q.quantile}>{q.return_pct.toFixed(2)}%</td>
|
||||
))}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap");
|
||||
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--panel: #1e293b;
|
||||
--panel-2: #273449;
|
||||
--text: #e2e8f0;
|
||||
--muted: #94a3b8;
|
||||
--accent: #38bdf8;
|
||||
--good: #34d399;
|
||||
--bad: #f87171;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "Noto Sans SC", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background: var(--panel);
|
||||
border-bottom: 1px solid #334155;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
nav a:hover,
|
||||
nav a.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
nav .brand {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid #334155;
|
||||
border-radius: 10px;
|
||||
padding: 18px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
background: var(--panel-2);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.stat .label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat .value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.good { color: var(--good); }
|
||||
.bad { color: var(--bad); }
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button {
|
||||
background: var(--panel-2);
|
||||
color: var(--text);
|
||||
border: 1px solid #475569;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
margin: 2px 6px 2px 0;
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--bad);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "qlib-platform · A股量化研究",
|
||||
description: "个人 A 股量化研究平台:股票池 / 因子 / 回测",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>
|
||||
<nav>
|
||||
<span className="brand">qlib-platform</span>
|
||||
<Link href="/">总览</Link>
|
||||
<Link href="/stocks">股票池</Link>
|
||||
<Link href="/factors">因子研究</Link>
|
||||
<Link href="/backtest">回测</Link>
|
||||
</nav>
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import type { Stock, FactorMeta } from "@/lib/types";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [stockCount, setStockCount] = useState<number | null>(null);
|
||||
const [factorCount, setFactorCount] = useState<number | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
apiGet<Stock[]>("/stocks?limit=500"),
|
||||
apiGet<FactorMeta[]>("/factors"),
|
||||
])
|
||||
.then(([stocks, factors]) => {
|
||||
setStockCount(stocks.length);
|
||||
setFactorCount(factors.length);
|
||||
})
|
||||
.catch((e: Error) => setError(e.message));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>总览</h1>
|
||||
{error && <div className="error">后端不可达:{error}</div>}
|
||||
<div className="stat-grid">
|
||||
<div className="stat">
|
||||
<div className="label">股票池</div>
|
||||
<div className="value">{stockCount ?? "—"}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="label">内置因子</div>
|
||||
<div className="value">{factorCount ?? "—"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card" style={{ marginTop: 16 }}>
|
||||
<h2>说明</h2>
|
||||
<div className="muted">
|
||||
个人 A 股量化研究平台(Phase 3 Web)。
|
||||
顶部导航进入「股票池 / 因子研究 / 回测」;研究基于本地 SQLite 行情
|
||||
(Tushare 同步,20 只权重股 2023–2024 日线),因子测试与回测均由后端
|
||||
Research Specification 驱动并返回标准化结果。
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { apiGet } from "@/lib/api";
|
||||
import type { Stock } from "@/lib/types";
|
||||
|
||||
export default function StocksPage() {
|
||||
const [stocks, setStocks] = useState<Stock[]>([]);
|
||||
const [q, setQ] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
apiGet<Stock[]>(`/stocks?limit=300&q=${encodeURIComponent(q)}`)
|
||||
.then(setStocks)
|
||||
.catch((e: Error) => setError(e.message));
|
||||
}, [q]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>股票池</h1>
|
||||
<div className="row" style={{ marginBottom: 12 }}>
|
||||
<input
|
||||
placeholder="按代码 / 名称搜索(如 600519 或 茅台)"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
/>
|
||||
<span className="muted">共 {stocks.length} 条</span>
|
||||
</div>
|
||||
{error && <div className="error">{error}</div>}
|
||||
<div className="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>代码</th>
|
||||
<th>名称</th>
|
||||
<th>行业</th>
|
||||
<th>地区</th>
|
||||
<th>上市日</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stocks.map((s) => (
|
||||
<tr key={s.symbol}>
|
||||
<td>{s.symbol}</td>
|
||||
<td>{s.name}</td>
|
||||
<td>{s.industry ?? "—"}</td>
|
||||
<td>{s.area ?? "—"}</td>
|
||||
<td>{s.list_date}</td>
|
||||
<td>{s.status}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
export interface XY {
|
||||
date: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export function LineChart({
|
||||
title,
|
||||
data,
|
||||
color = "#38bdf8",
|
||||
}: {
|
||||
title: string;
|
||||
data: XY[];
|
||||
color?: string;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || data.length === 0) return;
|
||||
const chart = echarts.init(ref.current);
|
||||
chart.setOption({
|
||||
title: { text: title, textStyle: { color: "#e2e8f0", fontSize: 13 } },
|
||||
tooltip: { trigger: "axis" },
|
||||
grid: { left: 60, right: 20, top: 40, bottom: 30 },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: data.map((p) => p.date),
|
||||
axisLabel: { color: "#94a3b8", fontSize: 10 },
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
scale: true,
|
||||
axisLabel: { color: "#94a3b8" },
|
||||
splitLine: { lineStyle: { color: "#334155" } },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "line",
|
||||
showSymbol: false,
|
||||
data: data.map((p) => p.value),
|
||||
lineStyle: { color, width: 1.6 },
|
||||
itemStyle: { color },
|
||||
},
|
||||
],
|
||||
});
|
||||
const onResize = () => chart.resize();
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
chart.dispose();
|
||||
};
|
||||
}, [data, color, title]);
|
||||
|
||||
if (data.length === 0) return <div className="muted">暂无数据</div>;
|
||||
return <div ref={ref} style={{ height: 360, width: "100%" }} />;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/** 后端 API 客户端(开发期指向本地 FastAPI,地址见 .env.local)。 */
|
||||
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://127.0.0.1:8000/api";
|
||||
|
||||
export async function apiGet<T>(path: string): Promise<T> {
|
||||
const resp = await fetch(`${BASE}${path}`);
|
||||
if (!resp.ok) throw new Error(`GET ${path} → ${resp.status}: ${await resp.text()}`);
|
||||
return (await resp.json()) as T;
|
||||
}
|
||||
|
||||
export async function apiPost<T>(path: string, body: unknown): Promise<T> {
|
||||
const resp = await fetch(`${BASE}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text();
|
||||
throw new Error(`POST ${path} → ${resp.status}: ${text.slice(0, 300)}`);
|
||||
}
|
||||
return (await resp.json()) as T;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/** 与后端 domain/entities/research.py 对应的类型(DTO 镜像)。 */
|
||||
|
||||
export interface Stock {
|
||||
symbol: string;
|
||||
name: string;
|
||||
industry?: string | null;
|
||||
area?: string | null;
|
||||
list_date: string;
|
||||
delist_date?: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface FactorMeta {
|
||||
name: string;
|
||||
description: string;
|
||||
formula: string;
|
||||
frequency: string;
|
||||
lookback: number;
|
||||
direction: "higher_is_better" | "lower_is_better";
|
||||
}
|
||||
|
||||
export interface ResearchSpec {
|
||||
type: "factor_test" | "backtest";
|
||||
universe: { exclude_st?: boolean; min_listing_days?: number };
|
||||
factors: { name: string; weight: number }[];
|
||||
selection: { top_n: number };
|
||||
rebalance: "weekly" | "monthly";
|
||||
period: [string, string];
|
||||
}
|
||||
|
||||
export interface CurvePoint {
|
||||
date: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface BacktestSummary {
|
||||
start: string;
|
||||
end: string;
|
||||
initial_capital: number;
|
||||
final_equity: number;
|
||||
total_return_pct: number;
|
||||
annual_return_pct: number;
|
||||
sharpe: number;
|
||||
max_drawdown_pct: number;
|
||||
volatility_pct: number;
|
||||
win_rate_pct: number;
|
||||
total_trades: number;
|
||||
avg_turnover_pct: number;
|
||||
}
|
||||
|
||||
export interface BacktestResult {
|
||||
summary: BacktestSummary;
|
||||
equity_curve: CurvePoint[];
|
||||
drawdown: CurvePoint[];
|
||||
monthly_returns: { year: number; month: number; return_pct: number }[];
|
||||
yearly_returns: { year: number; return_pct: number }[];
|
||||
positions: { date: string; symbol: string; weight: number }[];
|
||||
trades: { entry_date: string; exit_date: string; symbol: string; return_pct: number }[];
|
||||
turnover_pct: number;
|
||||
unimplemented: string[];
|
||||
config_snapshot: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface FactorTestReport {
|
||||
factor_name: string;
|
||||
ic_mean: number;
|
||||
icir: number;
|
||||
rank_ic_mean: number;
|
||||
positive_ratio_pct: number;
|
||||
quantile_returns: { quantile: number; return_pct: number }[];
|
||||
spread_quantile?: number | null;
|
||||
sample_days: number;
|
||||
unimplemented: string[];
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "qlib-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3000",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"echarts": "^5.5.1",
|
||||
"next": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
Generated
+649
@@ -0,0 +1,649 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
echarts:
|
||||
specifier: ^5.5.1
|
||||
version: 5.6.0
|
||||
next:
|
||||
specifier: ^15.1.0
|
||||
version: 15.5.25(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.8
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.8(react@19.2.8)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.10.0
|
||||
version: 22.20.1
|
||||
'@types/react':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.18
|
||||
'@types/react-dom':
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.7(@types/react@19.2.18)
|
||||
typescript:
|
||||
specifier: ^5.7.0
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@emnapi/runtime@1.11.3':
|
||||
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
os: [freebsd]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
|
||||
engines: {node: ^20.9.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@next/env@15.5.25':
|
||||
resolution: {integrity: sha512-42h1lLr07vl4gawALP1hsgRZjHB1xYa58JfUfHwr0f7jG/zhPakh5GHkADHXOC9ZxUvlQFOPIrp7s6qX4DezPQ==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.25':
|
||||
resolution: {integrity: sha512-w+RR0v/QuApnWEjRGm1z6gcObKwGMb5YPA7V3bzBEVSBpMFUXprer0tS27UxjUcEnqbhL7Zuzohej79B6rYmBg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.5.25':
|
||||
resolution: {integrity: sha512-QiGGBUSakt8S1H4Lt9Ehsh6Ja87axiBnQQgysOObvCbI7iUfJnRGntF1P64S4/ijuHFnSB8KLsEddkY3nN26uw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.25':
|
||||
resolution: {integrity: sha512-ehLos/66zo0d/mJCU5u96a/VDcr01aaUrX0o/i16UdInxz8qPTCDSxGtjk/Lps1sIr1RJFdiX3hxc0fxJo+cPA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.25':
|
||||
resolution: {integrity: sha512-ZVMrqLiJ7DiChgmbkQwFtdhAnUkSH/4p7tB29QY+giATb0Q/XGHNRSKAb/B8XGDHRUaA67NepOW5W8u3ZRJBAA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.25':
|
||||
resolution: {integrity: sha512-UOewtDGkTMJTiODrEdeLZ50yGb59xCZSriNpXkfPMxRRgwDkGc7i8mLWqV5076wEdb+Ca/XN7MhJyM3CupyNyQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.25':
|
||||
resolution: {integrity: sha512-UBHwA8AhkCZgtRfU1aJpunuAJe/6gZv6jDESQe4p5MjTb5V0YEeJBWCdNqx15Vj3x+5jmauRfeMJSjfQj9HGFQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.25':
|
||||
resolution: {integrity: sha512-QcFcPRr16djk5IqK5+e8O80eZfgWzIvVBXfitIq0tQ/uc+eyfdoZ0NmKc0cnbIJyfVwREapKuG97YcxWA9gcpA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.25':
|
||||
resolution: {integrity: sha512-zREeykps3ndWr9egJgvJKqVkkDuaw6Zrrg23cYBos0ygydFkAWYU4+PaPVwXzP1eAYQJe53ShSK45iDM529BOg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
'@types/node@22.20.1':
|
||||
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
|
||||
|
||||
'@types/react-dom@19.2.7':
|
||||
resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==}
|
||||
peerDependencies:
|
||||
'@types/react': ^19.2.0
|
||||
|
||||
'@types/react@19.2.18':
|
||||
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
|
||||
|
||||
caniuse-lite@1.0.30001810:
|
||||
resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
|
||||
|
||||
client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
echarts@5.6.0:
|
||||
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||
|
||||
nanoid@3.3.18:
|
||||
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
next@15.5.25:
|
||||
resolution: {integrity: sha512-OMWNulIIqKM2ykvC2qMjIt0IoavB4UB2SCs4iXJ6z6847FvyH8jBmBWcvrF5iuhTu8Przh20Fo/aoszIdqx4PA==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
'@playwright/test': ^1.51.1
|
||||
babel-plugin-react-compiler: '*'
|
||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@playwright/test':
|
||||
optional: true
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
react-dom@19.2.8:
|
||||
resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
|
||||
peerDependencies:
|
||||
react: ^19.2.8
|
||||
|
||||
react@19.2.8:
|
||||
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.35.4:
|
||||
resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
peerDependencies:
|
||||
'@types/node': '*'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '*'
|
||||
babel-plugin-macros: '*'
|
||||
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
tslib@2.3.0:
|
||||
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
zrender@5.6.1:
|
||||
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@emnapi/runtime@1.11.3':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-freebsd-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.3.3':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.35.4':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-webcontainers-wasm32@0.35.4':
|
||||
dependencies:
|
||||
'@img/sharp-wasm32': 0.35.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.35.4':
|
||||
optional: true
|
||||
|
||||
'@next/env@15.5.25': {}
|
||||
|
||||
'@next/swc-darwin-arm64@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.5.25':
|
||||
optional: true
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@types/node@22.20.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/react-dom@19.2.7(@types/react@19.2.18)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.18
|
||||
|
||||
'@types/react@19.2.18':
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
caniuse-lite@1.0.30001810: {}
|
||||
|
||||
client-only@0.0.1: {}
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
|
||||
echarts@5.6.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
zrender: 5.6.1
|
||||
|
||||
nanoid@3.3.18: {}
|
||||
|
||||
next@15.5.25(@types/node@22.20.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||
dependencies:
|
||||
'@next/env': 15.5.25
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001810
|
||||
postcss: 8.4.31
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
styled-jsx: 5.1.6(react@19.2.8)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.5.25
|
||||
'@next/swc-darwin-x64': 15.5.25
|
||||
'@next/swc-linux-arm64-gnu': 15.5.25
|
||||
'@next/swc-linux-arm64-musl': 15.5.25
|
||||
'@next/swc-linux-x64-gnu': 15.5.25
|
||||
'@next/swc-linux-x64-musl': 15.5.25
|
||||
'@next/swc-win32-arm64-msvc': 15.5.25
|
||||
'@next/swc-win32-x64-msvc': 15.5.25
|
||||
sharp: 0.35.4(@types/node@22.20.1)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
postcss@8.4.31:
|
||||
dependencies:
|
||||
nanoid: 3.3.18
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
react-dom@19.2.8(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
scheduler: 0.27.0
|
||||
|
||||
react@19.2.8: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@7.8.5:
|
||||
optional: true
|
||||
|
||||
sharp@0.35.4(@types/node@22.20.1):
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.35.4
|
||||
'@img/sharp-darwin-x64': 0.35.4
|
||||
'@img/sharp-freebsd-wasm32': 0.35.4
|
||||
'@img/sharp-libvips-darwin-arm64': 1.3.3
|
||||
'@img/sharp-libvips-darwin-x64': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm': 1.3.3
|
||||
'@img/sharp-libvips-linux-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linux-ppc64': 1.3.3
|
||||
'@img/sharp-libvips-linux-riscv64': 1.3.3
|
||||
'@img/sharp-libvips-linux-s390x': 1.3.3
|
||||
'@img/sharp-libvips-linux-x64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.3.3
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.3.3
|
||||
'@img/sharp-linux-arm': 0.35.4
|
||||
'@img/sharp-linux-arm64': 0.35.4
|
||||
'@img/sharp-linux-ppc64': 0.35.4
|
||||
'@img/sharp-linux-riscv64': 0.35.4
|
||||
'@img/sharp-linux-s390x': 0.35.4
|
||||
'@img/sharp-linux-x64': 0.35.4
|
||||
'@img/sharp-linuxmusl-arm64': 0.35.4
|
||||
'@img/sharp-linuxmusl-x64': 0.35.4
|
||||
'@img/sharp-webcontainers-wasm32': 0.35.4
|
||||
'@img/sharp-win32-arm64': 0.35.4
|
||||
'@img/sharp-win32-ia32': 0.35.4
|
||||
'@img/sharp-win32-x64': 0.35.4
|
||||
'@types/node': 22.20.1
|
||||
optional: true
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
styled-jsx@5.1.6(react@19.2.8):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.8
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
zrender@5.6.1:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./*"] }
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user