140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
"""按 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()
|