Files
stock_cursor_v0/功能实现/2_持仓成本可视化使用说明.md
2026-06-15 01:26:39 +08:00

12 KiB
Raw Blame History

持仓成本可视化增强使用说明

功能概述

持仓成本可视化增强功能提供精确的交易成本计算、持仓成本线标注、盈亏分布分析等功能,帮助用户更直观地了解持仓成本和盈亏情况。

核心特性

精确成本计算 - 印花税、佣金、过户费精确计算符合A股规则
成本线标注 - K线图上标注持仓成本线
成本历史追踪 - 记录每次买入/卖出后的成本变化
盈亏分布图 - 可视化展示盈利/亏损持仓分布
交易成本预估 - 下单前估算实际成本
成本明细拆解 - 详细展示每笔交易的成本构成


交易成本规则A股

成本构成

费用类型 费率 适用范围 说明
印花税 0.1% 仅卖出 固定费率
佣金 0.03% 买入+卖出 最低5元
过户费 0.001% 买入+卖出 仅沪市

计算示例

买入示例(沪市):

股票代码: 600519贵州茅台
成交价格: 1680元
成交数量: 100股

成交金额 = 1680 × 100 = 168,000元
佣金 = 168,000 × 0.0003 = 50.4元
过户费 = 168,000 × 0.00001 = 1.68元
印花税 = 0元买入无印花税

总成本 = 168,000 + 50.4 + 1.68 = 168,052.08元
成本率 = 0.031%

卖出示例(沪市):

成交金额 = 168,000元
佣金 = 50.4元
过户费 = 1.68元
印花税 = 168,000 × 0.001 = 168元

总成本 = 220.08元
实际到账 = 168,000 - 220.08 = 167,779.92元
成本率 = 0.131%

API 接口

1. 获取持仓成本线

GET /api/portfolio/cost_line/{code}

用途: K线图上标注成本线

响应示例:

{
  "ok": true,
  "code": "600519",
  "name": "贵州茅台",
  "current_position": {
    "qty": 100,
    "avg_cost": 1680.5,
    "total_cost": 168050.0,
    "current_price": 1720.0,
    "market_value": 172000.0,
    "unrealized_pnl": 3950.0,
    "unrealized_pct": 2.35,
    "trades_count": 2
  },
  "cost_history": [
    {
      "date": "2024-01-15",
      "cost": 1650.0,
      "qty": 100,
      "action": "买入",
      "trade_price": 1650.0,
      "trade_qty": 100
    },
    {
      "date": "2024-02-10",
      "cost": 1680.5,
      "qty": 100,
      "action": "补仓",
      "trade_price": 1710.0,
      "trade_qty": 50
    }
  ]
}

前端使用ECharts 标注成本线):

// 在 K 线图上添加成本线
const costLineData = await fetch(`/api/portfolio/cost_line/600519`).then(r => r.json());

if (costLineData.ok && costLineData.current_position) {
  const avgCost = costLineData.current_position.avg_cost;
  
  option.series.push({
    type: 'line',
    name: '持仓成本',
    data: Array(dates.length).fill(avgCost),
    lineStyle: { color: '#FFA500', width: 2, type: 'dashed' },
    z: 10
  });
}

2. 获取持仓成本分布

GET /api/portfolio/cost_distribution

用途: 盈亏分布可视化

响应示例:

{
  "ok": true,
  "profitable": [
    {
      "code": "600519",
      "name": "贵州茅台",
      "qty": 100,
      "avg_cost": 1680.5,
      "current_price": 1720.0,
      "market_value": 172000.0,
      "cost_value": 168050.0,
      "unrealized": 3950.0,
      "unrealized_pct": 2.35
    }
  ],
  "unprofitable": [
    {
      "code": "300750",
      "name": "宁德时代",
      "qty": 50,
      "avg_cost": 220.0,
      "current_price": 210.0,
      "market_value": 10500.0,
      "cost_value": 11000.0,
      "unrealized": -500.0,
      "unrealized_pct": -4.55
    }
  ],
  "breakeven": [],
  "summary": {
    "total_positions": 2,
    "profitable_count": 1,
    "unprofitable_count": 1,
    "breakeven_count": 0,
    "win_rate": 50.0
  }
}

3. 估算交易成本

POST /api/portfolio/estimate_cost
Content-Type: application/json

{
  "code": "600519",
  "price": 1680.0,
  "qty": 100,
  "side": "buy"
}

用途: 下单前预估实际成本

响应示例:

{
  "ok": true,
  "code": "600519",
  "price": 1680.0,
  "qty": 100,
  "side": "buy",
  "cost_detail": {
    "amount": 168000.0,
    "commission": 50.4,
    "stamp_tax": 0.0,
    "transfer_fee": 1.68,
    "total_cost": 52.08,
    "cost_rate": 0.031
  },
  "net_amount": 168052.08,
  "message": "买入需支付: 168052.08 元(含交易成本 52.08 元)"
}

4. 获取持仓成本明细

GET /api/portfolio/cost_breakdown/{code}

用途: 查看累计交易成本拆解

响应示例:

{
  "ok": true,
  "code": "600519",
  "name": "贵州茅台",
  "total_cost": 168052.08,
  "purchase_amount": 168000.0,
  "commission": 50.4,
  "stamp_tax": 0.0,
  "transfer_fee": 1.68,
  "cost_rate": 0.031,
  "trades": [
    {
      "date": "2024-01-15",
      "price": 1680.0,
      "qty": 100,
      "amount": 168000.0,
      "cost_detail": {
        "amount": 168000.0,
        "commission": 50.4,
        "stamp_tax": 0.0,
        "transfer_fee": 1.68,
        "total_cost": 52.08,
        "cost_rate": 0.031
      }
    }
  ]
}

使用场景

场景 1: K线图上标注成本线

前端实现示例:

// 获取K线数据
const klineData = await fetch(`/api/kline?symbol=600519&days=60`).then(r => r.json());

// 获取成本线数据
const costData = await fetch(`/api/portfolio/cost_line/600519`).then(r => r.json());

if (costData.ok && costData.current_position) {
  const avgCost = costData.current_position.avg_cost;
  const currentPrice = costData.current_position.current_price;
  
  // ECharts 配置
  const option = {
    title: {
      text: `${costData.name} - 成本: ${avgCost} 当前: ${currentPrice}`
    },
    series: [
      {
        type: 'candlestick',
        data: klineData.ohlc
      },
      {
        type: 'line',
        name: '持仓成本',
        data: Array(klineData.dates.length).fill(avgCost),
        lineStyle: { 
          color: '#FFA500', 
          width: 2, 
          type: 'dashed' 
        },
        markLine: {
          data: [{ yAxis: avgCost, name: `成本 ${avgCost}` }]
        }
      }
    ]
  };
}

场景 2: 盈亏分布饼图

const distData = await fetch('/api/portfolio/cost_distribution').then(r => r.json());

if (distData.ok) {
  const option = {
    title: { text: '持仓盈亏分布' },
    series: [{
      type: 'pie',
      data: [
        { 
          value: distData.profitable.length, 
          name: `盈利 ${distData.profitable.length}`,
          itemStyle: { color: '#26a69a' }
        },
        { 
          value: distData.unprofitable.length, 
          name: `亏损 ${distData.unprofitable.length}`,
          itemStyle: { color: '#ef5350' }
        }
      ]
    }]
  };
}

场景 3: 下单前成本计算

// 用户输入买入价格和数量
const buyPrice = 1680;
const buyQty = 100;

const estimate = await fetch('/api/portfolio/estimate_cost', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code: '600519',
    price: buyPrice,
    qty: buyQty,
    side: 'buy'
  })
}).then(r => r.json());

if (estimate.ok) {
  alert(`${estimate.message}\n\n成本明细:\n` +
        `佣金: ${estimate.cost_detail.commission}元\n` +
        `过户费: ${estimate.cost_detail.transfer_fee}元\n` +
        `印花税: ${estimate.cost_detail.stamp_tax}元`);
}

完整示例

Python 脚本示例

import requests
import json

BASE_URL = "http://localhost:8000"

# 1. 查看持仓成本线
def show_cost_line(code):
    resp = requests.get(f"{BASE_URL}/api/portfolio/cost_line/{code}")
    data = resp.json()
    
    if data["ok"]:
        pos = data["current_position"]
        print(f"\n{data['name']} ({code})")
        print(f"持仓数量: {pos['qty']}股")
        print(f"平均成本: {pos['avg_cost']}元")
        print(f"当前价格: {pos['current_price']}元")
        print(f"浮动盈亏: {pos['unrealized_pnl']}元 ({pos['unrealized_pct']}%)")
        
        print("\n成本变化历史:")
        for h in data["cost_history"]:
            print(f"{h['date']} {h['action']}: 成本={h['cost']}元, 持仓={h['qty']}股")

# 2. 查看盈亏分布
def show_distribution():
    resp = requests.get(f"{BASE_URL}/api/portfolio/cost_distribution")
    data = resp.json()
    
    if data["ok"]:
        print(f"\n持仓总数: {data['summary']['total_positions']}")
        print(f"盈利: {data['summary']['profitable_count']}")
        print(f"亏损: {data['summary']['unprofitable_count']}")
        print(f"胜率: {data['summary']['win_rate']}%")
        
        print("\n盈利股票:")
        for s in data["profitable"]:
            print(f"{s['name']}: +{s['unrealized']}元 (+{s['unrealized_pct']}%)")
        
        print("\n亏损股票:")
        for s in data["unprofitable"]:
            print(f"{s['name']}: {s['unrealized']}元 ({s['unrealized_pct']}%)")

# 3. 估算交易成本
def estimate_cost(code, price, qty, side="buy"):
    resp = requests.post(f"{BASE_URL}/api/portfolio/estimate_cost", json={
        "code": code,
        "price": price,
        "qty": qty,
        "side": side
    })
    data = resp.json()
    
    if data["ok"]:
        print(f"\n{data['message']}")
        cost = data["cost_detail"]
        print(f"\n成本明细:")
        print(f"  成交金额: {cost['amount']}元")
        print(f"  佣金: {cost['commission']}元")
        print(f"  印花税: {cost['stamp_tax']}元")
        print(f"  过户费: {cost['transfer_fee']}元")
        print(f"  总成本: {cost['total_cost']}元 ({cost['cost_rate']}%)")

# 运行示例
if __name__ == "__main__":
    show_cost_line("600519")
    show_distribution()
    estimate_cost("600519", 1680, 100, "buy")

最佳实践

1. 成本线使用建议

  • 买入参考: 当价格接近成本线时考虑加仓
  • 止损参考: 设置成本线下方一定比例作为止损位
  • 目标价参考: 成本线上方设置分批止盈位

2. 成本计算注意事项

  • 沪深区别: 深市000、002、300开头无过户费
  • 最低佣金: 单笔佣金不足5元按5元收取
  • 印花税: 只在卖出时收取,买入无需支付

3. 盈亏分析建议

  • 定期检查: 每周查看盈亏分布,调整持仓结构
  • 及时止损: 亏损超过-8%的持仓需要重新评估
  • 落袋为安: 盈利超过+20%考虑分批止盈

与其他功能集成

与自选股分组集成

# 将持仓股添加到"持仓股"分组
curl -X POST http://localhost:8000/api/watchlist/groups/3/stocks/batch \
  -H "Content-Type: application/json" \
  -d '{"codes":["600519","300750"]}'

与预警系统集成

# 为持仓股设置成本价预警(跌破成本-5%
AVG_COST=1680
STOP_LOSS=$(echo "$AVG_COST * 0.95" | bc)

curl -X POST http://localhost:8000/api/alerts \
  -H "Content-Type: application/json" \
  -d '{"code":"600519","kind":"price_below","threshold":'$STOP_LOSS',"note":"跌破成本-5%"}'

常见问题

Q: 为什么计算的成本和实际有差异?

A: 可能原因:

  1. 交易记录未完整录入
  2. 券商佣金费率不同(本系统默认万三)
  3. 分红、配股等特殊情况未计入

Q: 如何修改佣金费率?

A: 编辑 backend/position_cost.py 中的 COST_CONFIG 配置:

COST_CONFIG = {
    "commission_rate": 0.0003,  # 改为实际佣金率
    "commission_min": 5.0,       # 改为实际最低佣金
}

Q: 成本线为什么不显示?

A: 检查:

  1. 该股票是否有交易记录
  2. 当前是否有持仓(已清仓则无成本线)
  3. 接口返回是否成功

Q: 补仓后成本如何计算?

A: 使用加权平均法:

新成本 = (原持仓成本 + 补仓金额 + 补仓费用) / 新持仓数量

技术实现

核心模块

  • backend/position_cost.py - 成本计算核心逻辑
  • backend/portfolio.py - 原有持仓管理
  • backend/main.py - API 接口定义

算法说明

移动加权平均成本法:

if side == "buy":
    total_cost += price * qty + fee
    total_qty += qty
    avg_cost = total_cost / total_qty
else:  # sell
    avg_cost = total_cost / total_qty
    pnl = (price - avg_cost) * qty - fee
    total_cost -= avg_cost * qty
    total_qty -= qty

实现完成时间: 2024年
功能状态: 已完成并测试