功能细节优化
This commit is contained in:
347
backend/position_cost.py
Normal file
347
backend/position_cost.py
Normal file
@@ -0,0 +1,347 @@
|
||||
"""持仓成本可视化增强"""
|
||||
import datetime as dt
|
||||
from typing import Dict, List, Optional
|
||||
from collections import defaultdict
|
||||
from sqlalchemy import select, func
|
||||
from db import get_session
|
||||
from models import Trade, DailyQuote, StockMetric
|
||||
|
||||
# A股交易成本配置
|
||||
COST_CONFIG = {
|
||||
"stamp_tax": 0.001, # 印花税 0.1%(仅卖出)
|
||||
"commission_rate": 0.0003, # 佣金费率 0.03%
|
||||
"commission_min": 5.0, # 最低佣金 5元
|
||||
"transfer_fee": 0.00001, # 过户费 0.001%(沪市)
|
||||
}
|
||||
|
||||
def calculate_trade_cost(price: float, qty: int, side: str, is_sh: bool = True) -> Dict:
|
||||
"""精确计算交易成本
|
||||
|
||||
Args:
|
||||
price: 成交价格
|
||||
qty: 成交数量
|
||||
side: buy/sell
|
||||
is_sh: 是否沪市(影响过户费)
|
||||
|
||||
Returns:
|
||||
成本明细字典
|
||||
"""
|
||||
amount = price * qty
|
||||
|
||||
# 佣金(买卖都有)
|
||||
commission = max(amount * COST_CONFIG["commission_rate"], COST_CONFIG["commission_min"])
|
||||
|
||||
# 印花税(仅卖出)
|
||||
stamp_tax = amount * COST_CONFIG["stamp_tax"] if side == "sell" else 0.0
|
||||
|
||||
# 过户费(沪市买卖都有,深市无)
|
||||
transfer_fee = amount * COST_CONFIG["transfer_fee"] if is_sh else 0.0
|
||||
|
||||
total_cost = commission + stamp_tax + transfer_fee
|
||||
|
||||
return {
|
||||
"amount": round(amount, 2),
|
||||
"commission": round(commission, 2),
|
||||
"stamp_tax": round(stamp_tax, 2),
|
||||
"transfer_fee": round(transfer_fee, 2),
|
||||
"total_cost": round(total_cost, 2),
|
||||
"cost_rate": round(total_cost / amount * 100, 4) if amount > 0 else 0.0
|
||||
}
|
||||
|
||||
def get_position_cost_lines(code: str) -> Dict:
|
||||
"""获取个股的持仓成本线数据(用于K线图标注)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"code": "600519",
|
||||
"name": "贵州茅台",
|
||||
"current_position": {
|
||||
"qty": 100,
|
||||
"avg_cost": 1680.5,
|
||||
"total_cost": 168050.0,
|
||||
"trades_count": 3
|
||||
},
|
||||
"cost_history": [
|
||||
{"date": "2024-01-15", "cost": 1650.0, "qty": 100, "action": "买入"},
|
||||
{"date": "2024-02-10", "cost": 1680.5, "qty": 100, "action": "补仓"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
with get_session() as s:
|
||||
trades = s.execute(
|
||||
select(Trade).where(Trade.code == code)
|
||||
.order_by(Trade.date, Trade.id)
|
||||
).scalars().all()
|
||||
|
||||
if not trades:
|
||||
return {"ok": False, "msg": "该股票无交易记录"}
|
||||
|
||||
# 计算持仓成本变化
|
||||
qty = 0
|
||||
cost = 0.0
|
||||
cost_history = []
|
||||
|
||||
for t in trades:
|
||||
is_sh = t.code.startswith("6")
|
||||
|
||||
if t.side == "buy":
|
||||
# 买入:加权平均成本
|
||||
old_qty = qty
|
||||
old_cost = cost
|
||||
|
||||
qty += t.qty
|
||||
cost += t.price * t.qty + t.fee
|
||||
|
||||
avg_cost = cost / qty if qty > 0 else 0
|
||||
action = "补仓" if old_qty > 0 else "买入"
|
||||
|
||||
cost_history.append({
|
||||
"date": t.date.isoformat(),
|
||||
"cost": round(avg_cost, 2),
|
||||
"qty": qty,
|
||||
"action": action,
|
||||
"trade_price": t.price,
|
||||
"trade_qty": t.qty
|
||||
})
|
||||
|
||||
else: # sell
|
||||
if qty <= 0:
|
||||
continue
|
||||
|
||||
avg_cost = cost / qty
|
||||
sell_qty = min(t.qty, qty)
|
||||
|
||||
# 卖出:减少持仓
|
||||
cost -= avg_cost * sell_qty
|
||||
qty -= sell_qty
|
||||
|
||||
action = "清仓" if qty == 0 else "减仓"
|
||||
|
||||
cost_history.append({
|
||||
"date": t.date.isoformat(),
|
||||
"cost": round(cost / qty, 2) if qty > 0 else 0,
|
||||
"qty": qty,
|
||||
"action": action,
|
||||
"trade_price": t.price,
|
||||
"trade_qty": sell_qty,
|
||||
"pnl": round((t.price - avg_cost) * sell_qty - t.fee, 2)
|
||||
})
|
||||
|
||||
# 当前持仓
|
||||
current_position = None
|
||||
if qty > 0:
|
||||
avg_cost = cost / qty
|
||||
|
||||
# 获取当前价格
|
||||
metric = s.execute(
|
||||
select(StockMetric).where(StockMetric.code == code)
|
||||
).scalar_one_or_none()
|
||||
|
||||
current_price = metric.close if metric else avg_cost
|
||||
|
||||
current_position = {
|
||||
"qty": qty,
|
||||
"avg_cost": round(avg_cost, 2),
|
||||
"total_cost": round(cost, 2),
|
||||
"current_price": round(current_price, 2),
|
||||
"market_value": round(current_price * qty, 2),
|
||||
"unrealized_pnl": round((current_price - avg_cost) * qty, 2),
|
||||
"unrealized_pct": round((current_price / avg_cost - 1) * 100, 2) if avg_cost > 0 else 0,
|
||||
"trades_count": len([t for t in trades if t.side == "buy"])
|
||||
}
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"code": code,
|
||||
"name": trades[0].name,
|
||||
"current_position": current_position,
|
||||
"cost_history": cost_history
|
||||
}
|
||||
|
||||
def get_position_cost_distribution() -> Dict:
|
||||
"""获取所有持仓的成本分布(盈亏区间图)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"profitable": [...], # 盈利持仓
|
||||
"unprofitable": [...], # 亏损持仓
|
||||
"breakeven": [...] # 持平持仓
|
||||
}
|
||||
"""
|
||||
with get_session() as s:
|
||||
trades = s.execute(
|
||||
select(Trade).order_by(Trade.date, Trade.id)
|
||||
).scalars().all()
|
||||
|
||||
# 计算当前持仓
|
||||
pos = defaultdict(lambda: {"qty": 0, "cost": 0.0, "name": ""})
|
||||
|
||||
for t in trades:
|
||||
p = pos[t.code]
|
||||
p["name"] = t.name or p["name"]
|
||||
|
||||
if t.side == "buy":
|
||||
p["cost"] += t.price * t.qty + t.fee
|
||||
p["qty"] += t.qty
|
||||
else:
|
||||
if p["qty"] > 0:
|
||||
avg = p["cost"] / p["qty"]
|
||||
qty = min(t.qty, p["qty"])
|
||||
p["cost"] -= avg * qty
|
||||
p["qty"] -= qty
|
||||
|
||||
# 获取当前价格
|
||||
codes = [c for c, v in pos.items() if v["qty"] > 0]
|
||||
if not codes:
|
||||
return {"ok": True, "profitable": [], "unprofitable": [], "breakeven": []}
|
||||
|
||||
metrics = s.execute(
|
||||
select(StockMetric).where(StockMetric.code.in_(codes))
|
||||
).scalars().all()
|
||||
|
||||
price_map = {m.code: m.close for m in metrics}
|
||||
|
||||
# 分类统计
|
||||
profitable = []
|
||||
unprofitable = []
|
||||
breakeven = []
|
||||
|
||||
for code, p in pos.items():
|
||||
if p["qty"] <= 0:
|
||||
continue
|
||||
|
||||
avg_cost = p["cost"] / p["qty"]
|
||||
current_price = price_map.get(code, avg_cost)
|
||||
unrealized = (current_price - avg_cost) * p["qty"]
|
||||
unrealized_pct = (current_price / avg_cost - 1) * 100 if avg_cost > 0 else 0
|
||||
|
||||
item = {
|
||||
"code": code,
|
||||
"name": p["name"],
|
||||
"qty": p["qty"],
|
||||
"avg_cost": round(avg_cost, 2),
|
||||
"current_price": round(current_price, 2),
|
||||
"market_value": round(current_price * p["qty"], 2),
|
||||
"cost_value": round(p["cost"], 2),
|
||||
"unrealized": round(unrealized, 2),
|
||||
"unrealized_pct": round(unrealized_pct, 2)
|
||||
}
|
||||
|
||||
if unrealized_pct > 0.5:
|
||||
profitable.append(item)
|
||||
elif unrealized_pct < -0.5:
|
||||
unprofitable.append(item)
|
||||
else:
|
||||
breakeven.append(item)
|
||||
|
||||
# 排序
|
||||
profitable.sort(key=lambda x: x["unrealized"], reverse=True)
|
||||
unprofitable.sort(key=lambda x: x["unrealized"])
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"profitable": profitable,
|
||||
"unprofitable": unprofitable,
|
||||
"breakeven": breakeven,
|
||||
"summary": {
|
||||
"total_positions": len(codes),
|
||||
"profitable_count": len(profitable),
|
||||
"unprofitable_count": len(unprofitable),
|
||||
"breakeven_count": len(breakeven),
|
||||
"win_rate": round(len(profitable) / len(codes) * 100, 1) if codes else 0
|
||||
}
|
||||
}
|
||||
|
||||
def estimate_trade_cost(code: str, price: float, qty: int, side: str) -> Dict:
|
||||
"""估算交易成本(下单前预估)
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
price: 预计成交价
|
||||
qty: 交易数量
|
||||
side: buy/sell
|
||||
|
||||
Returns:
|
||||
成本明细和净值
|
||||
"""
|
||||
is_sh = code.startswith("6")
|
||||
cost_detail = calculate_trade_cost(price, qty, side, is_sh)
|
||||
|
||||
if side == "buy":
|
||||
net_amount = cost_detail["amount"] + cost_detail["total_cost"]
|
||||
msg = f"买入需支付: {round(net_amount, 2)} 元(含交易成本 {cost_detail['total_cost']} 元)"
|
||||
else:
|
||||
net_amount = cost_detail["amount"] - cost_detail["total_cost"]
|
||||
msg = f"卖出可获得: {round(net_amount, 2)} 元(扣除交易成本 {cost_detail['total_cost']} 元)"
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"code": code,
|
||||
"price": price,
|
||||
"qty": qty,
|
||||
"side": side,
|
||||
"cost_detail": cost_detail,
|
||||
"net_amount": round(net_amount, 2),
|
||||
"message": msg
|
||||
}
|
||||
|
||||
def get_cost_breakdown_for_position(code: str) -> Dict:
|
||||
"""获取持仓的详细成本拆解
|
||||
|
||||
Returns:
|
||||
{
|
||||
"total_cost": 168500.0,
|
||||
"purchase_amount": 168050.0, # 实际买入金额
|
||||
"commission": 350.0, # 累计佣金
|
||||
"stamp_tax": 0.0, # 累计印花税(买入无)
|
||||
"transfer_fee": 100.0, # 累计过户费
|
||||
"trades": [...] # 每笔交易明细
|
||||
}
|
||||
"""
|
||||
with get_session() as s:
|
||||
trades = s.execute(
|
||||
select(Trade).where(Trade.code == code, Trade.side == "buy")
|
||||
.order_by(Trade.date)
|
||||
).scalars().all()
|
||||
|
||||
if not trades:
|
||||
return {"ok": False, "msg": "该股票无买入记录"}
|
||||
|
||||
is_sh = code.startswith("6")
|
||||
|
||||
total_purchase = 0.0
|
||||
total_commission = 0.0
|
||||
total_stamp = 0.0
|
||||
total_transfer = 0.0
|
||||
trade_details = []
|
||||
|
||||
for t in trades:
|
||||
cost = calculate_trade_cost(t.price, t.qty, "buy", is_sh)
|
||||
|
||||
total_purchase += cost["amount"]
|
||||
total_commission += cost["commission"]
|
||||
total_stamp += cost["stamp_tax"]
|
||||
total_transfer += cost["transfer_fee"]
|
||||
|
||||
trade_details.append({
|
||||
"date": t.date.isoformat(),
|
||||
"price": t.price,
|
||||
"qty": t.qty,
|
||||
"amount": cost["amount"],
|
||||
"cost_detail": cost
|
||||
})
|
||||
|
||||
total_cost = total_purchase + total_commission + total_stamp + total_transfer
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"code": code,
|
||||
"name": trades[0].name,
|
||||
"total_cost": round(total_cost, 2),
|
||||
"purchase_amount": round(total_purchase, 2),
|
||||
"commission": round(total_commission, 2),
|
||||
"stamp_tax": round(total_stamp, 2),
|
||||
"transfer_fee": round(total_transfer, 2),
|
||||
"cost_rate": round((total_cost - total_purchase) / total_purchase * 100, 4) if total_purchase > 0 else 0,
|
||||
"trades": trade_details
|
||||
}
|
||||
Reference in New Issue
Block a user