feat(nav): Implement historical NAV tracking and enhance product API
- Added `list_nav_history` method in `CoreReadOnlyRepository` to retrieve historical NAV data for products over a specified number of days. - Introduced `get_nav_history` endpoint in `product_service` to return historical NAV sequences, improving product data accessibility. - Updated `products.py` to include new API endpoint for fetching NAV history, ensuring compliance with access control. - Enhanced `ready.py` to include a health check for the new functionality, ensuring system readiness. - Updated documentation to reflect the new testing baseline of 833 passed tests, indicating improved stability and functionality across the application. This update significantly enhances the product API by providing access to historical NAV data, improving user insights into product performance.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ mysql -u root -p < scripts/core/03-seed-customers.sql
|
||||
mysql -u root -p < scripts/core/04-seed-holdings.sql
|
||||
mysql -u root -p < scripts/core/05-seed-trades.sql
|
||||
mysql -u root -p < scripts/core/06-seed-nav.sql
|
||||
mysql -u root -p < scripts/core/07-seed-nav-history.sql
|
||||
|
||||
# Agent 库(若未建)
|
||||
mysql -u root -p < docs/项目框架设计/表设计/01-mysql-共用底座.sql
|
||||
@@ -35,6 +36,7 @@ python scripts/sync/sync_neo4j.py
|
||||
| 分析/风控/合规/运营 | 2+2+2+1(风控另有演示账号 STAFF-90001,我方分支独有) |
|
||||
| 双角色 staff | 1(STAFF-10091 advisor+compliance) |
|
||||
| 产品 | 14 |
|
||||
| 净值历史 | **14 产品 × 365 日**(`06-seed-nav.sql` 锚定日 + `07-seed-nav-history.sql` 364 日;再生见 `seed_nav_history.py --export-sql`) |
|
||||
| 持仓记录 | ~45 |
|
||||
| 交易 | 25+ |
|
||||
| C×R 适当性矩阵 | 25 行(core_suitability_rule,L0 权威) |
|
||||
|
||||
@@ -28,7 +28,8 @@ $files = @(
|
||||
"scripts/core/03-seed-customers.sql",
|
||||
"scripts/core/04-seed-holdings.sql",
|
||||
"scripts/core/05-seed-trades.sql",
|
||||
"scripts/core/06-seed-nav.sql"
|
||||
"scripts/core/06-seed-nav.sql",
|
||||
"scripts/core/07-seed-nav-history.sql"
|
||||
)
|
||||
foreach ($f in $files) { Invoke-SqlFile (Join-Path $Root $f) }
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""为 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()␍
|
||||
␍
|
||||
Reference in New Issue
Block a user