fix: 佣金按委托级合并计费(分笔成交只收一次佣金)
- calcOrderFees 委托层合并计费:同一委托分笔成交按总金额算一次佣金(最低5元一次) - mapTrade 保留原始成交字段,不再逐笔算费 - 前端聚合调用 calcOrderFees,展开明细只展示成交本身 - 验证:200股拆两笔佣金5元(非10元);两笔独立委托各5元
This commit is contained in:
@@ -154,7 +154,7 @@
|
|||||||
| 佣金 | 成交额 × (m_dCommission ÷ 10000),**不足 5 元按 5 元** | 账号没免五,费率=万分之 m_dCommission(如 0.852 = 万0.852) |
|
| 佣金 | 成交额 × (m_dCommission ÷ 10000),**不足 5 元按 5 元** | 账号没免五,费率=万分之 m_dCommission(如 0.852 = 万0.852) |
|
||||||
| 印花税 | 成交额 × 万分之5(0.05%) | **仅卖出**收取(2023-08-28 起标准,单边) |
|
| 印花税 | 成交额 × 万分之5(0.05%) | **仅卖出**收取(2023-08-28 起标准,单边) |
|
||||||
| 过户费 | 成交额 × 万分之0.1(0.01‰) | **双向**收取,无最低限额(2022-04-29 起标准,中登收取,老师确认) |
|
| 过户费 | 成交额 × 万分之0.1(0.01‰) | **双向**收取,无最低限额(2022-04-29 起标准,中登收取,老师确认) |
|
||||||
| 总费用 | 佣金 + 印花税 + 过户费 | 逐笔计算后聚合展示 |
|
| 总费用 | 佣金 + 印花税 + 过户费 | **委托级合并计费**:同一委托分笔成交按总金额算一次佣金;多笔独立委托各自算(2026-09-01 老师修正) |
|
||||||
|
|
||||||
### 2.2 Sample Data(真实数据样本,2026-09-01 实调)
|
### 2.2 Sample Data(真实数据样本,2026-09-01 实调)
|
||||||
|
|
||||||
@@ -530,6 +530,7 @@ src/
|
|||||||
- 2026-09-01 修正一:快捷按钮由「今日/本周/上周」改为「**今日/本周/本月**」(老师反馈,去掉上周、新增本月)。
|
- 2026-09-01 修正一:快捷按钮由「今日/本周/上周」改为「**今日/本周/本月**」(老师反馈,去掉上周、新增本月)。
|
||||||
- 2026-09-01 修正二(老师纠正):快捷范围为**完整时间段**(本月=9/1~9/30、本周=周一~周日),不做「截至今天」截断——否则月初第一天点「本月」会与「今日」重合、无法操作;这是操作逻辑问题,不是数据问题。
|
- 2026-09-01 修正二(老师纠正):快捷范围为**完整时间段**(本月=9/1~9/30、本周=周一~周日),不做「截至今天」截断——否则月初第一天点「本月」会与「今日」重合、无法操作;这是操作逻辑问题,不是数据问题。
|
||||||
- 2026-09-01 · 第十一轮(费用计算规则,老师定):m_dCommission 的值是**佣金费率(万分之)**——如 0.852 = 万分之0.852 = 0.0000852(不是金额 0.852 元)。费用规则:佣金 = 成交额 × 万0.852,不足 5 元按 5 元(账号没免五);卖出加印花税(万分之5,2023-08-28 起)和过户费(万分之0.1,双向,无最低,2022-04-29 起,老师确认费率);过户费收费方中登、ETF/LOF/可转债/REITs 免收。已实现 calcTradeFees 逐笔计算 + 前端聚合展示(总费用/佣金/印花税/过户费四列)。
|
- 2026-09-01 · 第十一轮(费用计算规则,老师定):m_dCommission 的值是**佣金费率(万分之)**——如 0.852 = 万分之0.852 = 0.0000852(不是金额 0.852 元)。费用规则:佣金 = 成交额 × 万0.852,不足 5 元按 5 元(账号没免五);卖出加印花税(万分之5,2023-08-28 起)和过户费(万分之0.1,双向,无最低,2022-04-29 起,老师确认费率);过户费收费方中登、ETF/LOF/可转债/REITs 免收。已实现 calcTradeFees 逐笔计算 + 前端聚合展示(总费用/佣金/印花税/过户费四列)。
|
||||||
|
- 2026-09-01 修正五(佣金合并计费,老师纠正):**同一笔委托分多笔成交,按总成交金额合并算一次佣金**(不逐笔重复收费);多笔独立委托(分开下单)才各自算佣金、各自触发 5 元最低。实现:calcOrderFees 委托层合并计费(Σ成交额×费率→最低5元一次),mapTrade 保留原始成交字段不再逐笔算费;前端聚合调用 calcOrderFees,展开明细只展示成交本身(费用在委托主行)。验证:200股拆两笔100+100,佣金5元(非10元);两笔独立委托各5元(10元)。
|
||||||
- 2026-09-01 修正四(本周范围,老师纠正):本周 = **周一~周五**(不考虑周六日,不是往前推 7 天/自然周周日);实现 weekSunday→weekFriday(周一+4)。
|
- 2026-09-01 修正四(本周范围,老师纠正):本周 = **周一~周五**(不考虑周六日,不是往前推 7 天/自然周周日);实现 weekSunday→weekFriday(周一+4)。
|
||||||
- 2026-09-01 修正三(交互模型,老师纠正):**交互是单向的**——用户先有操作(点「本月」),才有时间范围;不能反过来从「搜索范围落在哪」反向推断用户交互的是哪个按钮。实现改为 **preset 意图状态**:点按钮 → 记录 preset → 按钮高亮(单向);手动改日期 → 清除 preset(不再是任何快捷操作);轮询由 preset==='today' 决定,而非范围反推。
|
- 2026-09-01 修正三(交互模型,老师纠正):**交互是单向的**——用户先有操作(点「本月」),才有时间范围;不能反过来从「搜索范围落在哪」反向推断用户交互的是哪个按钮。实现改为 **preset 意图状态**:点按钮 → 记录 preset → 按钮高亮(单向);手动改日期 → 清除 preset(不再是任何快捷操作);轮询由 preset==='today' 决定,而非范围反推。
|
||||||
- **2026-09-01 · 第八轮(排序逻辑定稿)**:
|
- **2026-09-01 · 第八轮(排序逻辑定稿)**:
|
||||||
|
|||||||
@@ -48,6 +48,23 @@ function directionText(direction) {
|
|||||||
return '—';
|
return '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 费用计算:按整笔委托合并(同一委托分笔成交只收一次佣金,最低5元;2026-09-01 老师修正) */
|
||||||
|
function calcOrderFeesLocal(trades, commissionRateWan) {
|
||||||
|
const list = Array.isArray(trades) ? trades : [];
|
||||||
|
const totalAmount = list.reduce((s, t) => s + Math.max(0, +(t.amount ?? 0)), 0);
|
||||||
|
const direction = list[0]?.direction ?? '';
|
||||||
|
let commission = totalAmount * (commissionRateWan / 10000);
|
||||||
|
if (commission < 5 && commission > 0) commission = 5;
|
||||||
|
const stampTax = direction === 'sell' ? totalAmount * 0.0005 : 0;
|
||||||
|
const transferFee = totalAmount * 0.00001;
|
||||||
|
return {
|
||||||
|
commission: +commission.toFixed(4),
|
||||||
|
stampTax: +stampTax.toFixed(4),
|
||||||
|
transferFee: +transferFee.toFixed(4),
|
||||||
|
total: +(commission + stampTax + transferFee).toFixed(4),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 合并逻辑:按 m_strOrderSysID 把成交聚合进委托 */
|
/** 合并逻辑:按 m_strOrderSysID 把成交聚合进委托 */
|
||||||
function mergeOrdersAndTrades(orders, trades) {
|
function mergeOrdersAndTrades(orders, trades) {
|
||||||
const tradeByOrder = new Map();
|
const tradeByOrder = new Map();
|
||||||
@@ -59,24 +76,22 @@ function mergeOrdersAndTrades(orders, trades) {
|
|||||||
const ts = tradeByOrder.get(o.orderId) ?? [];
|
const ts = tradeByOrder.get(o.orderId) ?? [];
|
||||||
const totalVol = ts.reduce((s, t) => s + t.volume, 0);
|
const totalVol = ts.reduce((s, t) => s + t.volume, 0);
|
||||||
const totalAmount = ts.reduce((s, t) => s + t.amount, 0);
|
const totalAmount = ts.reduce((s, t) => s + t.amount, 0);
|
||||||
// 费用逐笔计算后聚合(每笔 commission/stampTax/transferFee 已是金额)
|
|
||||||
const aggCommission = ts.reduce((s, t) => s + t.commission, 0);
|
|
||||||
const aggStampTax = ts.reduce((s, t) => s + t.stampTax, 0);
|
|
||||||
const aggTransferFee = ts.reduce((s, t) => s + t.transferFee, 0);
|
|
||||||
const aggFeeTotal = ts.reduce((s, t) => s + t.feeTotal, 0);
|
|
||||||
const weightedPrice = totalVol > 0
|
const weightedPrice = totalVol > 0
|
||||||
? ts.reduce((s, t) => s + t.price * t.volume, 0) / totalVol
|
? ts.reduce((s, t) => s + t.price * t.volume, 0) / totalVol
|
||||||
: 0;
|
: 0;
|
||||||
|
// 费用:按整笔委托合并计算(同一委托分笔成交只收一次佣金,calcOrderFees)
|
||||||
|
const rateWan = ts[0]?.commissionRateWan ?? 0; // 佣金费率(万分之,来自成交,账号级一致)
|
||||||
|
const fees = calcOrderFeesLocal(ts, rateWan);
|
||||||
return {
|
return {
|
||||||
...o,
|
...o,
|
||||||
trades: ts,
|
trades: ts,
|
||||||
aggVolume: totalVol,
|
aggVolume: totalVol,
|
||||||
aggPrice: weightedPrice,
|
aggPrice: weightedPrice,
|
||||||
aggAmount: totalAmount,
|
aggAmount: totalAmount,
|
||||||
aggCommission,
|
aggCommission: fees.commission,
|
||||||
aggStampTax,
|
aggStampTax: fees.stampTax,
|
||||||
aggTransferFee,
|
aggTransferFee: fees.transferFee,
|
||||||
aggFeeTotal,
|
aggFeeTotal: fees.total,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -288,10 +303,6 @@ function OrderRows({ order, expanded, onToggle }) {
|
|||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>成交价</th>
|
<th style={{ ...tdStyle, textAlign: 'right' }}>成交价</th>
|
||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>成交量</th>
|
<th style={{ ...tdStyle, textAlign: 'right' }}>成交量</th>
|
||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>成交额</th>
|
<th style={{ ...tdStyle, textAlign: 'right' }}>成交额</th>
|
||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>总费用</th>
|
|
||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>佣金</th>
|
|
||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>印花税</th>
|
|
||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>过户费</th>
|
|
||||||
<th style={{ ...tdStyle, textAlign: 'right' }}>成交号</th>
|
<th style={{ ...tdStyle, textAlign: 'right' }}>成交号</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -302,10 +313,6 @@ function OrderRows({ order, expanded, onToggle }) {
|
|||||||
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.price)}</td>
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.price)}</td>
|
||||||
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.volume, 0)}</td>
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.volume, 0)}</td>
|
||||||
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.amount)}</td>
|
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.amount)}</td>
|
||||||
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.feeTotal, 2)}</td>
|
|
||||||
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.commission, 2)}</td>
|
|
||||||
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.stampTax, 2)}</td>
|
|
||||||
<td style={{ ...tdStyle, textAlign: 'right' }}>{fmtNum(t.transferFee, 2)}</td>
|
|
||||||
<td style={{ ...tdStyle, textAlign: 'right', color: '#999' }}>{t.tradeId}</td>
|
<td style={{ ...tdStyle, textAlign: 'right', color: '#999' }}>{t.tradeId}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -189,25 +189,30 @@ export class QmtBridgeRestDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算单笔成交的费用(2026-09-01 老师定规则)
|
* 计算一笔委托的总费用(2026-09-01 老师定规则 + 修正)
|
||||||
* - 佣金:成交额 × (佣金费率万分之m_dCommission),不足 5 元按 5 元(账号没免五)
|
* 核心规则:同一笔委托分多笔成交,按总成交金额合并算一次佣金(不逐笔重复收费);
|
||||||
* - 印花税:卖出单边,成交额 × 万分之5(2023-08-28 起标准)
|
* 多笔独立委托(分开下单)才各自算佣金、各自触发 5 元最低。
|
||||||
* - 过户费:双向,成交额 × 万分之0.1(2022-04-29 起标准,无最低限额)
|
* - 佣金:Σ成交额 × (佣金费率万分之m_dCommission),不足 5 元按 5 元(账号没免五,一次)
|
||||||
* @param {number} amount 成交额
|
* - 印花税:卖出单边,Σ成交额 × 万分之5(2023-08-28 起标准)
|
||||||
|
* - 过户费:双向,Σ成交额 × 万分之0.1(2022-04-29 起标准,无最低限额)
|
||||||
|
* @param {Array} trades 该委托的成交列表(每笔含 amount/direction)
|
||||||
* @param {number} commissionRateWan 佣金费率(万分之,来自 m_dCommission)
|
* @param {number} commissionRateWan 佣金费率(万分之,来自 m_dCommission)
|
||||||
* @param {'buy'|'sell'|''} direction 方向
|
* @returns {{ totalAmount: number, commission: number, stampTax: number, transferFee: number, total: number }}
|
||||||
* @returns {{ commission: number, stampTax: number, transferFee: number, total: number }}
|
|
||||||
*/
|
*/
|
||||||
calcTradeFees(amount, commissionRateWan, direction) {
|
calcOrderFees(trades, commissionRateWan) {
|
||||||
const a = Math.max(0, +(amount ?? 0));
|
const list = Array.isArray(trades) ? trades : [];
|
||||||
// 佣金:万分之X,最低 5 元(没免五)
|
// 合并总成交额(同一委托分笔成交合并计费)
|
||||||
let commission = a * (commissionRateWan / 10000);
|
const totalAmount = list.reduce((s, t) => s + Math.max(0, +(t.amount ?? 0)), 0);
|
||||||
|
const direction = list[0]?.direction ?? '';
|
||||||
|
// 佣金:合并总额 × 费率,不足 5 元按 5 元(一次)
|
||||||
|
let commission = totalAmount * (commissionRateWan / 10000);
|
||||||
if (commission < 5 && commission > 0) commission = 5;
|
if (commission < 5 && commission > 0) commission = 5;
|
||||||
// 印花税:卖出单边,万分之5
|
// 印花税:卖出单边,万分之5
|
||||||
const stampTax = direction === 'sell' ? a * 0.0005 : 0;
|
const stampTax = direction === 'sell' ? totalAmount * 0.0005 : 0;
|
||||||
// 过户费:双向,万分之0.1
|
// 过户费:双向,万分之0.1
|
||||||
const transferFee = a * 0.00001;
|
const transferFee = totalAmount * 0.00001;
|
||||||
return {
|
return {
|
||||||
|
totalAmount: +totalAmount.toFixed(2),
|
||||||
commission: +commission.toFixed(4),
|
commission: +commission.toFixed(4),
|
||||||
stampTax: +stampTax.toFixed(4),
|
stampTax: +stampTax.toFixed(4),
|
||||||
transferFee: +transferFee.toFixed(4),
|
transferFee: +transferFee.toFixed(4),
|
||||||
@@ -219,9 +224,6 @@ export class QmtBridgeRestDataSource {
|
|||||||
* m_dCommission 语义(2026-09-01 老师定):佣金费率(万分之),如 0.852 = 万分之0.852 */
|
* m_dCommission 语义(2026-09-01 老师定):佣金费率(万分之),如 0.852 = 万分之0.852 */
|
||||||
mapTrade(t) {
|
mapTrade(t) {
|
||||||
const direction = t.m_nOffsetFlag === 48 ? 'buy' : t.m_nOffsetFlag === 49 ? 'sell' : '';
|
const direction = t.m_nOffsetFlag === 48 ? 'buy' : t.m_nOffsetFlag === 49 ? 'sell' : '';
|
||||||
const commissionRateWan = +(t.m_dCommission ?? t.m_dComssion ?? 0); // 拼写变体兜底
|
|
||||||
const amount = +(t.m_dTradeAmount ?? 0);
|
|
||||||
const fees = this.calcTradeFees(amount, commissionRateWan, direction);
|
|
||||||
return {
|
return {
|
||||||
tradeId: t.m_strTradeID ?? '',
|
tradeId: t.m_strTradeID ?? '',
|
||||||
orderId: t.m_strOrderSysID ?? '', // 关联委托
|
orderId: t.m_strOrderSysID ?? '', // 关联委托
|
||||||
@@ -233,12 +235,8 @@ export class QmtBridgeRestDataSource {
|
|||||||
optName: t.m_strOptName ?? '',
|
optName: t.m_strOptName ?? '',
|
||||||
price: +(t.m_dPrice ?? 0),
|
price: +(t.m_dPrice ?? 0),
|
||||||
volume: t.m_nVolume ?? 0,
|
volume: t.m_nVolume ?? 0,
|
||||||
amount,
|
amount: +(t.m_dTradeAmount ?? 0),
|
||||||
commissionRateWan, // 佣金费率(万分之)
|
commissionRateWan: +(t.m_dCommission ?? t.m_dComssion ?? 0), // 佣金费率(万分之)
|
||||||
commission: fees.commission, // 佣金金额(最低5元)
|
|
||||||
stampTax: fees.stampTax, // 印花税(卖出)
|
|
||||||
transferFee: fees.transferFee, // 过户费
|
|
||||||
feeTotal: fees.total, // 总费用
|
|
||||||
tradeDate: t.m_strTradeDate ?? '',
|
tradeDate: t.m_strTradeDate ?? '',
|
||||||
tradeTime: t.m_strTradeTime ?? '',
|
tradeTime: t.m_strTradeTime ?? '',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user