Files

140 lines
5.3 KiB
Python
Raw Permalink Normal View History

2026-09-13 20:48:42 +08:00
"""按 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()