feat(nav): Enhance product API with historical NAV tracking and new charting features
- Updated `ready.py` to include a health check for the new `products_nav_history` endpoint, ensuring system readiness. - Enhanced the `ProductNavChartPanel` component to visualize historical NAV data with various charting options, including line and column charts. - Introduced new utility functions for filtering and aggregating NAV data, improving data handling in the frontend. - Updated tests to verify the inclusion of the new `products_nav_history` check in the API response. - Improved documentation to reflect recent changes and the new testing baseline of 833 passed tests, indicating enhanced stability. This update significantly improves the product API by providing access to historical NAV data and enhancing user insights through visualizations.
This commit is contained in:
+5112
-5098
File diff suppressed because it is too large
Load Diff
+143
-131
@@ -1,131 +1,143 @@
|
||||
"""为 core_product_nav 灌入近 365 个自然日历史(锚定 06-seed-nav 最新日)。␍
|
||||
␍
|
||||
仓库内 canonical 种子:`scripts/core/07-seed-nav-history.sql`(reset.ps1 在 06 之后执行)。␍
|
||||
␍
|
||||
python scripts/core/seed_nav_history.py # 直连 MySQL upsert(增量/修复)␍
|
||||
python scripts/core/seed_nav_history.py --export-sql # 重新生成 07-seed-nav-history.sql␍
|
||||
"""␍
|
||||
from __future__ import annotations␍
|
||||
␍
|
||||
import argparse␍
|
||||
import hashlib␍
|
||||
import sys␍
|
||||
from datetime import date, timedelta␍
|
||||
from pathlib import Path␍
|
||||
␍
|
||||
ROOT = Path(__file__).resolve().parents[2]␍
|
||||
if str(ROOT) not in sys.path:␍
|
||||
sys.path.insert(0, str(ROOT))␍
|
||||
␍
|
||||
from sqlalchemy import text␍
|
||||
␍
|
||||
from app.config.database import get_core_engine␍
|
||||
␍
|
||||
ANCHOR_DATE = date(2026, 9, 4)␍
|
||||
DAYS_BACK = 364 # 与锚定日合计约 365 个自然日␍
|
||||
SQL_OUT = Path(__file__).resolve().parent / "07-seed-nav-history.sql"␍
|
||||
␍
|
||||
# 与 scripts/core/06-seed-nav.sql 一致(锚定日净值)␍
|
||||
ANCHORS: list[tuple[str, float]] = [␍
|
||||
("PROD-110022", 1.0300),␍
|
||||
("PROD-110023", 1.0200),␍
|
||||
("PROD-005827", 0.9500),␍
|
||||
("PROD-005828", 1.0200),␍
|
||||
("PROD-161725", 1.0500),␍
|
||||
("PROD-161726", 1.0500),␍
|
||||
("PROD-003095", 0.9500),␍
|
||||
("PROD-510300", 1.0300),␍
|
||||
("PROD-510500", 0.9800),␍
|
||||
("PROD-XYZ999", 0.9000),␍
|
||||
("PROD-000001", 1.0020),␍
|
||||
("PROD-000002", 1.0050),␍
|
||||
("PROD-WMG001", 1.0150),␍
|
||||
("PROD-PRIV01", 1.0800),␍
|
||||
]␍
|
||||
␍
|
||||
␍
|
||||
def _daily_chg_pct(product_id: str, nav_date: date) -> float:␍
|
||||
h = hashlib.sha256(f"{product_id}:{nav_date.isoformat()}".encode()).hexdigest()␍
|
||||
n = int(h[:8], 16) / 0xFFFFFFFF␍
|
||||
return round((n - 0.5) * 3.0, 4) # roughly -1.5% .. +1.5%␍
|
||||
␍
|
||||
␍
|
||||
def build_history_rows() -> list[dict]:␍
|
||||
rows: list[dict] = []␍
|
||||
for product_id, anchor_nav in ANCHORS:␍
|
||||
nav = anchor_nav␍
|
||||
for offset in range(DAYS_BACK, 0, -1):␍
|
||||
d = ANCHOR_DATE - timedelta(days=offset)␍
|
||||
chg = _daily_chg_pct(product_id, d)␍
|
||||
nav = round(nav / (1 + chg / 100.0), 4)␍
|
||||
rows.append(␍
|
||||
{␍
|
||||
"product_id": product_id,␍
|
||||
"nav_date": d.isoformat(),␍
|
||||
"nav": nav,␍
|
||||
"daily_chg_pct": chg,␍
|
||||
}␍
|
||||
)␍
|
||||
return rows␍
|
||||
␍
|
||||
␍
|
||||
def export_sql(path: Path = SQL_OUT, *, batch_size: int = 500) -> int:␍
|
||||
rows = build_history_rows()␍
|
||||
lines = [␍
|
||||
"USE jinrong_core;",␍
|
||||
"",␍
|
||||
"-- 近 364 个自然日历史净值(锚定日 2026-09-04 由 06-seed-nav.sql 写入)",␍
|
||||
f"-- 生成:python scripts/core/seed_nav_history.py --export-sql · {len(rows)} 行 · {len(ANCHORS)} 产品",␍
|
||||
"",␍
|
||||
]␍
|
||||
for batch_start in range(0, len(rows), batch_size):␍
|
||||
chunk = rows[batch_start : batch_start + batch_size]␍
|
||||
lines.append(␍
|
||||
"INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date) VALUES"␍
|
||||
)␍
|
||||
value_lines = [␍
|
||||
f"('{r['product_id']}', {r['nav']:.4f}, {r['daily_chg_pct']:.4f}, '{r['nav_date']}')"␍
|
||||
for r in chunk␍
|
||||
]␍
|
||||
lines.append(",\n".join(value_lines) + ";")␍
|
||||
lines.append("")␍
|
||||
path.write_text("\n".join(lines), encoding="utf-8")␍
|
||||
return len(rows)␍
|
||||
␍
|
||||
␍
|
||||
def upsert_mysql() -> int:␍
|
||||
rows = build_history_rows()␍
|
||||
sql = text(␍
|
||||
"""␍
|
||||
INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date)␍
|
||||
VALUES (:product_id, :nav, :daily_chg_pct, :nav_date)␍
|
||||
ON DUPLICATE KEY UPDATE nav = VALUES(nav), daily_chg_pct = VALUES(daily_chg_pct)␍
|
||||
"""␍
|
||||
)␍
|
||||
engine = get_core_engine()␍
|
||||
with engine.begin() as conn:␍
|
||||
for batch_start in range(0, len(rows), 500):␍
|
||||
chunk = rows[batch_start : batch_start + 500]␍
|
||||
conn.execute(sql, chunk)␍
|
||||
return len(rows)␍
|
||||
␍
|
||||
␍
|
||||
def main() -> None:␍
|
||||
parser = argparse.ArgumentParser(description="Core NAV history seed (364 days × 14 products)")␍
|
||||
parser.add_argument(␍
|
||||
"--export-sql",␍
|
||||
action="store_true",␍
|
||||
help=f"Write {SQL_OUT.name} instead of upserting MySQL",␍
|
||||
)␍
|
||||
args = parser.parse_args()␍
|
||||
if args.export_sql:␍
|
||||
n = export_sql()␍
|
||||
print(f"seed_nav_history: wrote {n} rows to {SQL_OUT}")␍
|
||||
return␍
|
||||
n = upsert_mysql()␍
|
||||
print(f"seed_nav_history: upserted {n} rows for {len(ANCHORS)} products")␍
|
||||
␍
|
||||
␍
|
||||
if __name__ == "__main__":␍
|
||||
main()␍
|
||||
␍
|
||||
"""为 core_product_nav 灌入近 365 个自然日历史(锚定 06-seed-nav 最新日)。
|
||||
|
||||
仓库内 canonical 种子:`scripts/core/07-seed-nav-history.sql`(reset.ps1 在 06 之后执行)。
|
||||
|
||||
python scripts/core/seed_nav_history.py # 直连 MySQL upsert(增量/修复)
|
||||
python scripts/core/seed_nav_history.py --export-sql # 重新生成 07-seed-nav-history.sql
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.config.database import get_core_engine
|
||||
|
||||
ANCHOR_DATE = date(2026, 9, 4)
|
||||
DAYS_BACK = 364 # 与锚定日合计约 365 个自然日
|
||||
SQL_OUT = Path(__file__).resolve().parent / "07-seed-nav-history.sql"
|
||||
|
||||
# 与 scripts/core/06-seed-nav.sql 一致(锚定日净值)
|
||||
ANCHORS: list[tuple[str, float]] = [
|
||||
("PROD-110022", 1.0300),
|
||||
("PROD-110023", 1.0200),
|
||||
("PROD-005827", 0.9500),
|
||||
("PROD-005828", 1.0200),
|
||||
("PROD-161725", 1.0500),
|
||||
("PROD-161726", 1.0500),
|
||||
("PROD-003095", 0.9500),
|
||||
("PROD-510300", 1.0300),
|
||||
("PROD-510500", 0.9800),
|
||||
("PROD-XYZ999", 0.9000),
|
||||
("PROD-000001", 1.0020),
|
||||
("PROD-000002", 1.0050),
|
||||
("PROD-WMG001", 1.0150),
|
||||
("PROD-PRIV01", 1.0800),
|
||||
]
|
||||
|
||||
|
||||
def _daily_chg_pct(product_id: str, nav_date: date) -> float:
|
||||
h = hashlib.sha256(f"{product_id}:{nav_date.isoformat()}".encode()).hexdigest()
|
||||
n = int(h[:8], 16) / 0xFFFFFFFF
|
||||
return round((n - 0.5) * 3.0, 4) # roughly -1.5% .. +1.5%
|
||||
|
||||
|
||||
def build_history_rows() -> list[dict]:
|
||||
"""从锚定日净值向前反推首日,再按日涨跌正向生成至锚定日(与 06-seed 锚点一致、无末日断崖)。"""
|
||||
rows: list[dict] = []
|
||||
for product_id, anchor_nav in ANCHORS:
|
||||
days_list = [ANCHOR_DATE - timedelta(days=DAYS_BACK - i) for i in range(DAYS_BACK + 1)]
|
||||
nav = anchor_nav
|
||||
for d in reversed(days_list[1:]):
|
||||
chg = _daily_chg_pct(product_id, d)
|
||||
nav = nav / (1 + chg / 100.0)
|
||||
running = round(nav, 4)
|
||||
for i, d in enumerate(days_list):
|
||||
if i > 0:
|
||||
if d == ANCHOR_DATE:
|
||||
chg = round(((anchor_nav - running) / running) * 100, 4)
|
||||
running = anchor_nav
|
||||
else:
|
||||
chg = _daily_chg_pct(product_id, d)
|
||||
running = round(running * (1 + chg / 100.0), 4)
|
||||
else:
|
||||
chg = _daily_chg_pct(product_id, d)
|
||||
rows.append(
|
||||
{
|
||||
"product_id": product_id,
|
||||
"nav_date": d.isoformat(),
|
||||
"nav": running,
|
||||
"daily_chg_pct": chg,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def export_sql(path: Path = SQL_OUT, *, batch_size: int = 500) -> int:
|
||||
rows = build_history_rows()
|
||||
lines = [
|
||||
"USE jinrong_core;",
|
||||
"",
|
||||
"-- 近 365 个自然日(含锚定日 2026-09-04;与 06-seed-nav 锚点净值一致)",
|
||||
f"-- 生成:python scripts/core/seed_nav_history.py --export-sql · {len(rows)} 行 · {len(ANCHORS)} 产品 · 含锚定日",
|
||||
"",
|
||||
]
|
||||
for batch_start in range(0, len(rows), batch_size):
|
||||
chunk = rows[batch_start : batch_start + batch_size]
|
||||
lines.append(
|
||||
"INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date) VALUES"
|
||||
)
|
||||
value_lines = [
|
||||
f"('{r['product_id']}', {r['nav']:.4f}, {r['daily_chg_pct']:.4f}, '{r['nav_date']}')"
|
||||
for r in chunk
|
||||
]
|
||||
lines.append(",\n".join(value_lines) + ";")
|
||||
lines.append("")
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
return len(rows)
|
||||
|
||||
|
||||
def upsert_mysql() -> int:
|
||||
rows = build_history_rows()
|
||||
sql = text(
|
||||
"""
|
||||
INSERT INTO core_product_nav (product_id, nav, daily_chg_pct, nav_date)
|
||||
VALUES (:product_id, :nav, :daily_chg_pct, :nav_date)
|
||||
ON DUPLICATE KEY UPDATE nav = VALUES(nav), daily_chg_pct = VALUES(daily_chg_pct)
|
||||
"""
|
||||
)
|
||||
engine = get_core_engine()
|
||||
with engine.begin() as conn:
|
||||
for batch_start in range(0, len(rows), 500):
|
||||
chunk = rows[batch_start : batch_start + 500]
|
||||
conn.execute(sql, chunk)
|
||||
return len(rows)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Core NAV history seed (364 days × 14 products)")
|
||||
parser.add_argument(
|
||||
"--export-sql",
|
||||
action="store_true",
|
||||
help=f"Write {SQL_OUT.name} instead of upserting MySQL",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.export_sql:
|
||||
n = export_sql()
|
||||
print(f"seed_nav_history: wrote {n} rows to {SQL_OUT}")
|
||||
return
|
||||
n = upsert_mysql()
|
||||
print(f"seed_nav_history: upserted {n} rows for {len(ANCHORS)} products")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user