"""按 fin_product 为指定客户生成 fin_holdings 持仓数据(本地开发/演示用)。 用法: python scripts/seed_holdings_for_customer.py --customer-id 18 python scripts/seed_holdings_for_customer.py --customer-id 18 --count 5 --force 规则: - 只挑选 fin_product 中 status=在售 的产品,按 id 升序取前 count 只; - 每笔持仓的买入净值 = 当前净值 × (1 - 浮动),浮动由固定 seed 生成,保证可复现; - shares = cost_amount / 买入净值(4 位小数),current_value = shares × 当前净值; - profit_loss / profit_ratio 由上述字段推导; - 客户已有持仓时默认拒绝,需 --force 才会追加。 """ from __future__ import annotations import argparse import asyncio import random import sys from decimal import Decimal, ROUND_HALF_UP from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from sqlalchemy import select from config.database.mysql import get_session_factory from model.fin_holdings import FinHoldings from model.fin_product import FinProduct from model.sys_user import SysUser _MONEY = Decimal("0.01") _SHARES = Decimal("0.0001") _RATIO = Decimal("0.0001") def _money(value: Decimal) -> Decimal: return value.quantize(_MONEY, rounding=ROUND_HALF_UP) async def seed(customer_id: int, *, count: int, force: bool) -> list[dict]: session_factory = get_session_factory() async with session_factory() as db: user = await db.get(SysUser, customer_id) if user is None or user.user_type != "CUSTOMER": raise SystemExit(f"customer_id={customer_id} 不是客户用户或不存在") existing = ( await db.execute( select(FinHoldings).where(FinHoldings.customer_id == customer_id) ) ).scalars().all() if existing and not force: raise SystemExit( f"客户 {customer_id} 已有 {len(existing)} 条持仓,如需追加请加 --force" ) products = ( await db.execute( select(FinProduct) .where(FinProduct.status == "在售") .order_by(FinProduct.id) .limit(count) ) ).scalars().all() if not products: raise SystemExit("fin_product 中没有在售产品") rng = random.Random(f"holdings-{customer_id}") created: list[dict] = [] for product in products: nav = product.nav or Decimal("1.000000") cost_amount = _money(Decimal(rng.randint(5_000, 50_000))) # 买入净值在当前净值的 88%~97% 之间浮动,形成有涨有跌的持仓 buy_nav = _money(nav * Decimal(str(round(rng.uniform(0.88, 0.97), 6)))) if buy_nav <= 0: buy_nav = Decimal("1.0000") shares = (cost_amount / buy_nav).quantize(_SHARES, rounding=ROUND_HALF_UP) current_value = _money(shares * nav) profit_loss = _money(current_value - cost_amount) profit_ratio = (profit_loss / cost_amount).quantize(_RATIO, rounding=ROUND_HALF_UP) db.add( FinHoldings( customer_id=customer_id, product_id=product.id, shares=shares, cost_amount=cost_amount, current_value=current_value, profit_loss=profit_loss, profit_ratio=profit_ratio, status="持有中", ) ) created.append( { "product_id": product.id, "product_name": product.product_name, "buy_nav": str(buy_nav), "nav": str(nav), "shares": str(shares), "cost_amount": str(cost_amount), "current_value": str(current_value), "profit_loss": str(profit_loss), "profit_ratio": str(profit_ratio), } ) await db.commit() return created def main() -> None: parser = argparse.ArgumentParser(description="按 fin_product 生成客户持仓数据") parser.add_argument("--customer-id", type=int, required=True) parser.add_argument("--count", type=int, default=8, help="生成持仓条数(默认 8)") parser.add_argument( "--force", action="store_true", help="客户已有持仓时仍允许追加" ) args = parser.parse_args() created = asyncio.run(seed(args.customer_id, count=args.count, force=args.force)) total_cost = sum(Decimal(item["cost_amount"]) for item in created) total_value = sum(Decimal(item["current_value"]) for item in created) print(f"客户 {args.customer_id} 新增持仓 {len(created)} 条:") for item in created: print( f" [{item['product_id']}] {item['product_name'][:24]}… " f"买入净值={item['buy_nav']} 当前净值={item['nav']} " f"份额={item['shares']} 成本={item['cost_amount']} " f"市值={item['current_value']} 盈亏={item['profit_loss']} " f"({item['profit_ratio']})" ) print(f"合计:成本 {total_cost} 元,市值 {total_value} 元," f"盈亏 {total_value - total_cost} 元") if __name__ == "__main__": main()