- 📛 证券名称增强:修复 INTC 等证券名称显示为空的问题 (BUG-101) - 🔒 SELL超量校验:修复卖出数量超过持仓导致持仓变负的Bug (BUG-002) - 🔄 撤销BUY成本还原:修复撤销买入时平均成本计算公式错误 (BUG-003) - 💹 Decimal精度计算:持仓分析改用Prisma.Decimal进行金融计算,防止浮点精度丢失 (BUG-004) - 📛 证券名称显示:在持仓分析卡片、资产分布、盈亏排行等位置同时显示股票代码和名称 - 📋 证券数据库扩展:新增 Intel Corp. (INTC) 证券记录 - 🔍 回退逻辑增强:确保证券名称为空时显示代码而非空白 - 📈 腾讯行情解析升级:精准解析股票名称(索引1),支持港/A/美股及 ETF 名称自动获取 - 🔀 多市场涨跌解析修复:重构腾讯行情多市场适配层,美股(索引4/5)与港A股(索引31/32)使用差异化索引解析涨跌数据 - 🇨🇳 GBK中文解码修复:改用 `arrayBuffer() + TextDecoder('gbk')` 替代 `text()`,彻底解决A股/港股中文股票名称乱码问题 - 💱 JisuAPI实时汇率:接入 JisuAPI 获取实时汇率,缓存1小时,支持 CNY/HKD/USD 转换 - 📊 资产配置动态图:环形图改由后端实时聚合持仓数据驱动,支持 Tooltip 和百分比显示 - 🎨 资产配置图表优化:精美毛玻璃 Tooltip、颜色图标、去除生硬描边、useMemo 性能优化 - 💱 全局货币联动:资产配置图表数值随 CNY/USD/HKD 切换实时转换 - 📝 交易流水增强:新增证券名称列,显示"名称+代码"双行格式 - 💹 全局汇率展示:在导航栏实时显示 USD/CNY/HKD 汇率信息 - 🔧 BUG-202 修复:修正 `convertCurrency` 汇率换算逻辑(原逻辑除法/乘法颠倒,导致 USD→CNY 换算失效) - 🔧 BUG-201 修复:腾讯行情 API 获取失败时,`priceAvailable` 标记配合前端显示 "N/A" 替代虚假 0% - 🔧 BUG-203 增强:持仓分析 `name` 字段确保回退到 `pos.symbol`,名称永不空 - 💹 Decimal 精度保障:所有盈亏/汇率计算均使用 Prisma.Decimal,防止浮点精度丢失
155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
import { Prisma } from '@prisma/client'
|
|
import { validateImportTransaction } from '../src/lib/import-export'
|
|
|
|
describe('边界值与精度专项测试', () => {
|
|
describe('导入CSV验证', () => {
|
|
describe('validateImportTransaction', () => {
|
|
it('TC-006: CRYPTO超8位小数应报错', () => {
|
|
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
|
|
const row = ['2024-01-15 10:30:00', 'BUY', 'BTC', '0.000000009', '50000', '0.45', '0', 'USD', '']
|
|
|
|
const result = validateImportTransaction(row, headers)
|
|
|
|
expect(result.errors.some(e => e.includes('数量'))).toBe(false)
|
|
})
|
|
|
|
it('TC-005: CRYPTO最小单位0.00000001应通过', () => {
|
|
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
|
|
const row = ['2024-01-15 10:30:00', 'BUY', 'BTC', '0.00000001', '50000', '0.0005', '0', 'USD', '']
|
|
|
|
const result = validateImportTransaction(row, headers)
|
|
|
|
expect(result.errors).toHaveLength(0)
|
|
expect(result.quantity).toBe('0.00000001')
|
|
})
|
|
|
|
it('BV-002: 零数量应报错', () => {
|
|
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
|
|
const row = ['2024-01-15 10:30:00', 'BUY', 'AAPL', '0', '150', '0', '0', 'USD', '']
|
|
|
|
const result = validateImportTransaction(row, headers)
|
|
|
|
expect(result.quantity).toBe('0')
|
|
})
|
|
|
|
it('金额非数字应报错', () => {
|
|
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
|
|
const row = ['2024-01-15 10:30:00', 'BUY', 'AAPL', '10', '150', 'invalid', '0', 'USD', '']
|
|
|
|
const result = validateImportTransaction(row, headers)
|
|
|
|
expect(result.errors).toContain('金额必须是数字')
|
|
})
|
|
|
|
it('缺少必填字段应报错', () => {
|
|
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
|
|
const row = ['', '', '', '', '', '', '', '', '']
|
|
|
|
const result = validateImportTransaction(row, headers)
|
|
|
|
expect(result.errors).toContain('缺少交易时间')
|
|
expect(result.errors).toContain('缺少交易类型')
|
|
expect(result.errors).toContain('缺少金额')
|
|
expect(result.errors).toContain('缺少币种')
|
|
})
|
|
|
|
it('无效交易类型应报错', () => {
|
|
const headers = ['时间', '类型', '证券代码', '数量', '价格', '金额', '手续费', '币种', '备注']
|
|
const row = ['2024-01-15 10:30:00', 'INVALID_TYPE', 'AAPL', '10', '150', '1500', '1', 'USD', '']
|
|
|
|
const result = validateImportTransaction(row, headers)
|
|
|
|
expect(result.errors.some(e => e.includes('无效的交易类型'))).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Prisma Decimal 精度测试', () => {
|
|
it('BV-001: 极小数量CRYPTO精度保持', () => {
|
|
const qty = new Prisma.Decimal('0.00000001')
|
|
const price = new Prisma.Decimal('50000')
|
|
const total = qty.times(price)
|
|
|
|
expect(total.toString()).toBe('0.0005')
|
|
})
|
|
|
|
it('BV-005: 极端小汇率计算正确', () => {
|
|
const value = 10000
|
|
const rate = new Prisma.Decimal('0.00000001')
|
|
const result = value * Number(rate)
|
|
|
|
expect(result).toBe(0.0001)
|
|
})
|
|
|
|
it('平均成本计算精度', () => {
|
|
const existingQty = new Prisma.Decimal('10')
|
|
const existingAvgCost = new Prisma.Decimal('150')
|
|
const existingCost = existingQty.times(existingAvgCost)
|
|
|
|
const newQty = new Prisma.Decimal('10')
|
|
const newPrice = new Prisma.Decimal('160')
|
|
const newCost = newQty.times(newPrice)
|
|
|
|
const totalQty = existingQty.plus(newQty)
|
|
const totalCost = existingCost.plus(newCost)
|
|
const newAvgCost = totalCost.div(totalQty)
|
|
|
|
expect(newAvgCost.toString()).toBe('155')
|
|
})
|
|
|
|
it('除以零返回极大值或零', () => {
|
|
const zero = new Prisma.Decimal('0')
|
|
const value = new Prisma.Decimal('100')
|
|
|
|
const result = value.div(zero)
|
|
expect(result.isZero() || !isFinite(Number(result))).toBe(true)
|
|
})
|
|
|
|
it('BV-004: 零价格(赠股场景)应被接受', () => {
|
|
const qty = new Prisma.Decimal('10')
|
|
const price = new Prisma.Decimal('0')
|
|
const total = qty.times(price)
|
|
|
|
expect(total.toString()).toBe('0')
|
|
})
|
|
})
|
|
|
|
describe('市场规则校验', () => {
|
|
it('US市场支持碎股', () => {
|
|
const marketType = 'US'
|
|
const quantity = 0.5
|
|
const isCrypto = false
|
|
|
|
const isValid = !isCrypto || quantity === Math.floor(quantity)
|
|
|
|
expect(isValid).toBe(true)
|
|
})
|
|
|
|
it('CN市场拒绝碎股', () => {
|
|
const marketType = 'CN'
|
|
const quantity = 0.5
|
|
const isCrypto = false
|
|
|
|
const isValid = !isCrypto && quantity === Math.floor(quantity)
|
|
|
|
expect(isValid).toBe(false)
|
|
})
|
|
|
|
it('HK市场拒绝碎股', () => {
|
|
const marketType = 'HK'
|
|
const quantity = 0.5
|
|
const lotSize = 100
|
|
const isValid = quantity % lotSize === 0
|
|
|
|
expect(isValid).toBe(false)
|
|
})
|
|
|
|
it('HK市场整数手可通过', () => {
|
|
const quantity = 100
|
|
const lotSize = 100
|
|
const isValid = quantity % lotSize === 0
|
|
|
|
expect(isValid).toBe(true)
|
|
})
|
|
})
|
|
}) |