Merge branch 'glms/feature/1.4.2' of http://git.yiliantech.com/gitlab/otc-dev/zszq-trs into glms/feature/1.4.2
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
using YLErp.Modules.EodModule.SettlementModule;
|
||||
|
||||
namespace YLErp.Modules.EodModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证 EodCheckSettlePrice 的持仓分支按 ClientIds 收敛:
|
||||
/// 给定收盘客户时,不应再把“仅属于其他客户”的上一交易日持仓标的纳入结算价缺失检查。
|
||||
///
|
||||
/// 采用确定性夹具:插入两条合成持仓(客户A持标的A、客户B持标的B),直接调用抽出的
|
||||
/// static 查询方法断言过滤语义,finally 中清理,避免依赖测试库现有数据形状。
|
||||
/// 若 underlying_manager 无足够的对冲类型标的,则 Assert.Inconclusive 跳过。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class EodCheckSettlePriceClientScopeTest : UnitTestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void PositionUnderlyingQuery_ExcludesOtherClients_WhenClientIdsGiven()
|
||||
{
|
||||
using var db = DbContextFactory.GetYLDbContext();
|
||||
|
||||
// 选两个存在的、非期货的标的(避开 GetFutureTypes,保证通过方法内部的期货到期过滤);
|
||||
// 持仓 TradeType 固定为"股票"(属于 TradeTypesForHedge),才能进入结算价检查。
|
||||
var futureTypes = ConsGlobal.InstrumentType.GetFutureTypes();
|
||||
var underlyings = db.underlying_manager
|
||||
.Where(u => u.UnderlyingCode != null && !futureTypes.Contains(u.UnderlyingInstrumentType))
|
||||
.Take(5)
|
||||
.ToList();
|
||||
if (underlyings.Count < 2)
|
||||
{
|
||||
Assert.Inconclusive("underlying_manager 无足够的非期货标的,跳过");
|
||||
return;
|
||||
}
|
||||
var uA = underlyings[0];
|
||||
var uB = underlyings[1];
|
||||
|
||||
// 复用一条现有持仓的 BookId/TradeId,确保外键合法(若存在)
|
||||
var sample = db.eod_trade_position.FirstOrDefault(p => p.BookId != 0);
|
||||
int bookId = sample?.BookId ?? 1;
|
||||
int tradeId = sample?.TradeId ?? 0;
|
||||
|
||||
// 合成日期与客户,避免与测试库真实数据冲突
|
||||
var preSettleDate = new DateTime(2026, 5, 1);
|
||||
var settleDate = new DateTime(2026, 5, 2);
|
||||
int clientA = 900001;
|
||||
int clientB = 900002;
|
||||
|
||||
var rows = new List<eod_trade_position>
|
||||
{
|
||||
new eod_trade_position
|
||||
{
|
||||
ValueDate = preSettleDate,
|
||||
ClientId = clientA,
|
||||
UnderlyingCode = uA.UnderlyingCode,
|
||||
UnderlyingId = uA.id,
|
||||
TradeType = "股票",
|
||||
BookId = bookId,
|
||||
TradeId = tradeId,
|
||||
Amount = 1,
|
||||
HedgeUniqueCode = "UT_CLIENTSCOPE_A"
|
||||
},
|
||||
new eod_trade_position
|
||||
{
|
||||
ValueDate = preSettleDate,
|
||||
ClientId = clientB,
|
||||
UnderlyingCode = uB.UnderlyingCode,
|
||||
UnderlyingId = uB.id,
|
||||
TradeType = "股票",
|
||||
BookId = bookId,
|
||||
TradeId = tradeId,
|
||||
Amount = 1,
|
||||
HedgeUniqueCode = "UT_CLIENTSCOPE_B"
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var r in rows)
|
||||
{
|
||||
r.OptId = 0;
|
||||
r.OptName = "UT_CLIENTSCOPE";
|
||||
r.OptDate = DateTime.Now;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
db.eod_trade_position.AddRange(rows);
|
||||
db.SaveChanges();
|
||||
|
||||
var fullSet = EodCheckSettlePrice.QueryPositionUnderlyingCodes(db, preSettleDate, settleDate, null)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var filteredA = EodCheckSettlePrice.QueryPositionUnderlyingCodes(db, preSettleDate, settleDate, new List<int> { clientA })
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Assert.IsTrue(fullSet.Contains(uA.UnderlyingCode), "全客户结果应包含客户A的标的");
|
||||
Assert.IsTrue(fullSet.Contains(uB.UnderlyingCode), "全客户结果应包含客户B的标的");
|
||||
Assert.IsTrue(filteredA.Contains(uA.UnderlyingCode), "按客户A收敛后仍应包含客户A的标的");
|
||||
// 关键断言:修复点——按客户A收敛后不应再包含“仅属客户B”的标的
|
||||
Assert.IsFalse(filteredA.Contains(uB.UnderlyingCode),
|
||||
"修复验证失败:按客户A收敛后仍包含仅属客户B的持仓标的(ClientId 过滤未生效)");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 清理合成数据,使测试库状态不变
|
||||
foreach (var r in rows)
|
||||
{
|
||||
var exist = db.eod_trade_position.FirstOrDefault(x =>
|
||||
x.ValueDate == preSettleDate && x.ClientId == r.ClientId &&
|
||||
x.UnderlyingCode == r.UnderlyingCode && x.HedgeUniqueCode == r.HedgeUniqueCode);
|
||||
if (exist != null)
|
||||
{
|
||||
db.eod_trade_position.Remove(exist);
|
||||
}
|
||||
}
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,7 +713,7 @@ namespace YLErp.Modules.SwapModule
|
||||
"部分平仓计算必须带入自动互换遗留的待实现尾差");
|
||||
Assert.AreEqual(position.id, service.LastInterestCalculationEodPosition.PositionId,
|
||||
"部分平仓计息必须按腿标识匹配上一日日终");
|
||||
AssertDecimal(0.006383561644m, firstCloseResult.InterestIncomeSum,
|
||||
AssertDecimal(-0.010438356164m, firstCloseResult.InterestIncomeSum,
|
||||
"部分平仓后待实现应延续历史尾差");
|
||||
AssertDecimal(0.02m, firstCloseResult.RealizedInterest,
|
||||
"部分平仓后累计已实现应包含此前自动互换和本次平仓");
|
||||
@@ -744,7 +744,7 @@ namespace YLErp.Modules.SwapModule
|
||||
const decimal rate = 0.0299m;
|
||||
const decimal pendingInterest = 0.820379534246m;
|
||||
const decimal settledInterest = 0.82m;
|
||||
const decimal expectedPendingInterest = 0.820569301369m;
|
||||
const decimal expectedPendingInterest = 0.410474008219m;
|
||||
var service = new StubEodPositionService();
|
||||
var td = CreateTrade();
|
||||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
@@ -782,6 +782,31 @@ namespace YLErp.Modules.SwapModule
|
||||
"The previous EOD identity must not be reset to a new position");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_MANUAL_PREPAY_PARTIAL_CLOSE_UsesHistoryPlusRemainingDailyInterest()
|
||||
{
|
||||
var service = new StubEodPositionService();
|
||||
var td = CreateTrade();
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestMode = (int)InterestModeEnum.初始预付金;
|
||||
position.InterestPrincipalFix = Principal;
|
||||
var previousEod = CreatePreEod(StartDate.AddDays(2), 100m);
|
||||
previousEod.InterestMode = position.InterestMode;
|
||||
previousEod.TdInterestPrincipal = Principal;
|
||||
var closeFlow = CreateSwapFlowEvent(StartDate.AddDays(3), 50m);
|
||||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
closeFlow.InterestPrincipal = 500m;
|
||||
|
||||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
previousEod, position, td, StartDate.AddDays(3), null,
|
||||
500m, 0m, new List<swap_flow_event> { closeFlow }, 500m, false);
|
||||
|
||||
var expected = previousEod.InterestIncomeSum
|
||||
+ result.TdInterestIncome - result.TdCloseInterest;
|
||||
AssertDecimal(expected, result.InterestIncomeSum,
|
||||
"预付金部分平仓待实现收益应为历史待实现+平仓后当日新增-平仓实现");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_AUTO_SETTLEMENT_005_AutoSettlementKeepsRemainingPrincipal()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 【绿灯验收】国联民生-期间结算事件验证-分红前部分平仓过的交易,平仓与分红结束并收盘后,
|
||||
/// 继续平仓 / 互换时"浮动端平仓盈亏·分红(DividendIn)"应为 0(修复方案C:后端读 EOD 单一可信源)。
|
||||
/// ============================================================================
|
||||
/// 参考交易:GLMS-20260105-0004(测试库 id=1921,标的 230004.IB)
|
||||
/// 场景时间线(库内真实数据):
|
||||
/// 2026-01-05 开仓,期初持仓 50,000,000(支付方向 Direction=2,多头 PositionType=1)
|
||||
/// 2026-02-28 部分平仓 40%(20,000,000),剩余 30,000,000;该日早于 3/2 登记日,
|
||||
/// 已平仓的 40% 不享有 3/2 付息 → 平仓事件 DividendIn=0(正确)
|
||||
/// 2026-03-02 债券期间付息登记日;当日自动互换(EventType=4),按【剩余持仓 30,000,000】
|
||||
/// 实现分红 DividendIn=-54,240(=30,000,000 × 0.001808);EOD PosiDividendSum=0(全实现)
|
||||
/// 2026-03-03 收盘后对该交易继续平仓/互换
|
||||
///
|
||||
/// 修复前(Bug):前端 getDivindIn 用期初全额持仓(50M)×totalInterest 算出 -90,400,
|
||||
/// 扣除 consumedDividend(-54,240) 得 remainDividend=-36,160 展示 → 错误。
|
||||
/// -36,160 恰=已平仓40%(20M)×单位付息(0.001808)×支付方向(-1),即把"登记日前已平仓、
|
||||
/// 不享有该笔分红"的部分重复计入。
|
||||
///
|
||||
/// 修复后(方案C):后端 InitUnwind/InitIncome 经 GetPreEodDividendSum 读上一收盘日
|
||||
/// eod_swap_position.PosiDividendSum。3/2 互换后该值=0 → DividendIn=0(正确)。
|
||||
/// 前端 getDivindIn 不再自算,直接用后端值。
|
||||
/// ============================================================================
|
||||
/// 本测试连真实测试库,调用真实 SwapDealService.GetPreEodDividendSum 验证修复后 DividendIn=0。
|
||||
/// 连不上库时 Inconclusive 跳过(CI 无 DB 环境不挡)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class GLMS20260105PartialCloseDividendBugTest
|
||||
{
|
||||
private const string TradeNumber = "GLMS-20260105-0004";
|
||||
// 3/3 继续平仓/互换的业务日期;上一收盘日为 3/2(EOD 已生成,PosiDividendSum=0)
|
||||
private static readonly DateTime DealDate0303 = new(2026, 3, 3);
|
||||
|
||||
/// <summary>
|
||||
/// 连真实库的 SwapDealService 子类,仅用于暴露 protected GetPreEodDividendSum 供单测调用。
|
||||
/// 不 override 任何 seam → DbContext 走真实 YLContext(与 DbContextFactory.GetYLDbContext() 同库)。
|
||||
/// </summary>
|
||||
private sealed class RealDbSwapDealService : SwapDealService
|
||||
{
|
||||
public RealDbSwapDealService() : base(OptUserInfo.UnitTestUser) { }
|
||||
|
||||
public decimal ExposeGetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
|
||||
=> GetPreEodDividendSum(tradeId, positionId, dealDate);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("DBRecording")]
|
||||
public void Green_3_3_Unwind_DividendIn_FromEOD_Equals_Zero()
|
||||
{
|
||||
YLContext db;
|
||||
try { db = DbContextFactory.GetYLDbContext(); }
|
||||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||||
|
||||
try
|
||||
{
|
||||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber);
|
||||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {TradeNumber}"); return; }
|
||||
|
||||
// 浮动腿持仓:非初始、UnderlyingCode 非空(当前剩余 30,000,000)
|
||||
var positions = db.swap_position.Where(x => x.SwapTradeId == td.id && !x.Invalid).ToList();
|
||||
var floatLeg = positions.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode) && !x.IsInitial);
|
||||
Assert.IsNotNull(floatLeg, "未找到浮动腿持仓(非初始、UnderlyingCode 非空)");
|
||||
|
||||
// ---- 调用真实 GetPreEodDividendSum(方案C 修复核心)----
|
||||
var service = new RealDbSwapDealService();
|
||||
decimal dividendIn = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, DealDate0303);
|
||||
|
||||
// ---- 绿灯断言:3/2 互换后 EOD PosiDividendSum=0 → DividendIn=0 ----
|
||||
Assert.AreEqual(0m, dividendIn, 0.01m,
|
||||
$"[绿灯·方案C验收] 3/3平仓/互换 DividendIn 期望=0(3/2互换后剩余持仓30M的分红已全部实现," +
|
||||
$"EOD PosiDividendSum=0)。实际={dividendIn}。修复前该值为 -36,160(期初持仓×totalInterest 重算误计入已平仓40%)。");
|
||||
|
||||
// ---- 健全性:上一 EOD 确为 3/2,且 PosiDividendSum=0 ----
|
||||
var lastEod = db.eod_swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && x.ValueDate < DealDate0303 && x.PositionId == floatLeg.PositionId)
|
||||
.OrderByDescending(x => x.ValueDate).FirstOrDefault();
|
||||
Assert.IsNotNull(lastEod, "应存在 3/2 的 EOD 持仓记录");
|
||||
Assert.AreEqual(new DateTime(2026, 3, 2), lastEod.ValueDate, "上一收盘日应为 3/2");
|
||||
Assert.AreEqual(0m, lastEod.PosiDividendSum, 0.01m,
|
||||
$"3/2 EOD PosiDividendSum 应=0(当日 TdPosiDividend={lastEod.TdPosiDividend} 全额由互换 TdCloseDividend={lastEod.TdCloseDividend} 实现)");
|
||||
Assert.AreEqual(30_000_000m, lastEod.PosiQuantity, "3/2 剩余持仓应为 30,000,000(2/28已平仓40%)");
|
||||
}
|
||||
finally { db?.Dispose(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 互换页(InitIncome)与平仓页(InitUnwind)读同一 EOD:同 position、同 dealDate,
|
||||
/// GetPreEodDividendSum 返回值必然一致。本测试明确覆盖"点击收益互换同理"路径
|
||||
/// (用户反馈:平仓页 -36,160,点击收益互换同样错误)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestCategory("DBRecording")]
|
||||
public void Green_3_3_Income_DividendIn_FromEOD_Equals_Zero()
|
||||
{
|
||||
YLContext db;
|
||||
try { db = DbContextFactory.GetYLDbContext(); }
|
||||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||||
|
||||
try
|
||||
{
|
||||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber);
|
||||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {TradeNumber}"); return; }
|
||||
|
||||
var floatLeg = db.swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.UnderlyingCode != null && x.UnderlyingCode != "")
|
||||
.FirstOrDefault();
|
||||
Assert.IsNotNull(floatLeg, "未找到浮动腿持仓");
|
||||
|
||||
// InitIncome 与 InitUnwind 调用 GetPreEodDividendSum 的入参一致 → 结果一致
|
||||
var service = new RealDbSwapDealService();
|
||||
decimal incomeDividendIn = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, DealDate0303);
|
||||
|
||||
Assert.AreEqual(0m, incomeDividendIn, 0.01m,
|
||||
$"[互换页·方案C验收] 3/3收益互换 DividendIn 期望=0,实际={incomeDividendIn}。" +
|
||||
$"InitIncome 与 InitUnwind 共用 GetPreEodDividendSum,应返回同一 EOD 值。");
|
||||
}
|
||||
finally { db?.Dispose(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 防御性:dealDate 早于任何 EOD(如交易首日尚未收盘)时,GetPreEodDividendSum 应返回 0,
|
||||
/// 不抛异常。覆盖"无 EOD 记录"边界——fallback 到 dealDate.AddDays(-1) 后查无数据 → 0。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestCategory("DBRecording")]
|
||||
public void Green_NoEodBeforeDealDate_Returns_Zero_WithoutThrowing()
|
||||
{
|
||||
YLContext db;
|
||||
try { db = DbContextFactory.GetYLDbContext(); }
|
||||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||||
|
||||
try
|
||||
{
|
||||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == TradeNumber);
|
||||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {TradeNumber}"); return; }
|
||||
|
||||
var floatLeg = db.swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.UnderlyingCode != null && x.UnderlyingCode != "")
|
||||
.FirstOrDefault();
|
||||
Assert.IsNotNull(floatLeg, "未找到浮动腿持仓");
|
||||
|
||||
var service = new RealDbSwapDealService();
|
||||
// dealDate = 交易开始日前一天,确保无任何 EOD 满足 ValueDate < dealDate
|
||||
var preStartDate = td.StartDate.Value.AddDays(-1);
|
||||
decimal dividendIn = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, preStartDate);
|
||||
|
||||
Assert.AreEqual(0m, dividendIn, 0.01m,
|
||||
$"[防御] dealDate({preStartDate:yyyy-MM-dd})早于所有 EOD 时,GetPreEodDividendSum 应返回 0,实际={dividendIn}。" +
|
||||
$"与历史 InitUnwind/InitIncome 中 DividendIn=0 行为一致,不抛异常。");
|
||||
}
|
||||
finally { db?.Dispose(); }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 非0场景验收(关键防回归)
|
||||
// ----------------------------------------------------------------------------
|
||||
// GLMS-20260105-0004 的 PosiDividendSum 恰好=0(3/2 已全额互换),无法暴露
|
||||
// "DividendPending 硬编码 0" 的回归。本测试用一笔 PosiDividendSum≠0 的活交易
|
||||
// GLMS-20260706-0004(标的 230004.IB,持仓 1亿,待实现分红 -150,000,多日稳定)
|
||||
// 验证:GetPreEodDividendSum 返回非0的 -150,000 → 修复后 DividendIn/DividendPending
|
||||
// 均应为此值(而非历史硬0回归)。
|
||||
// 若本测试因硬0回归而失败(实际=0),即说明有人把 DividendPending 改回了硬0,
|
||||
// 或 GetPreEodDividendSum 传导链路被破坏。
|
||||
// ============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// 非0场景:PosiDividendSum≠0 时,GetPreEodDividendSum 返回真实非0值,
|
||||
/// 该值应同时成为 DividendIn 与 DividendPending(全量口径)。
|
||||
/// 这是针对"硬0被0掩盖"盲区的核心防回归测试。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
[TestCategory("DBRecording")]
|
||||
public void Green_NonZeroPosiDividendSum_Returns_RealValue_NotHardZero()
|
||||
{
|
||||
const string nonZeroTrade = "GLMS-20260706-0004"; // PosiDividendSum=-150,000,确认成交活交易
|
||||
YLContext db;
|
||||
try { db = DbContextFactory.GetYLDbContext(); }
|
||||
catch (Exception ex) { Assert.Inconclusive($"无法连接测试库(CI/无DB环境正常跳过):{ex.Message}"); return; }
|
||||
|
||||
try
|
||||
{
|
||||
var td = db.trade.FirstOrDefault(t => t.TradeNumber == nonZeroTrade);
|
||||
if (td == null) { Assert.Inconclusive($"测试库不存在交易 {nonZeroTrade}"); return; }
|
||||
|
||||
var floatLeg = db.swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && !x.IsInitial && x.UnderlyingCode != null && x.UnderlyingCode != "")
|
||||
.FirstOrDefault();
|
||||
Assert.IsNotNull(floatLeg, $"未找到 {nonZeroTrade} 的浮动腿持仓");
|
||||
|
||||
// 取该交易最近一次 EOD 的 PosiDividendSum 作为期望值(多日稳定 -150,000)
|
||||
var lastEod = db.eod_swap_position
|
||||
.Where(x => x.SwapTradeId == td.id && !x.Invalid && x.PositionId == floatLeg.PositionId && x.PosiQuantity > 0)
|
||||
.OrderByDescending(x => x.ValueDate).FirstOrDefault();
|
||||
Assert.IsNotNull(lastEod, $"未找到 {nonZeroTrade} 的有效 EOD");
|
||||
Assert.AreNotEqual(0m, lastEod.PosiDividendSum, 0.01m,
|
||||
$"前置:{nonZeroTrade} 的 PosiDividendSum 应≠0(本测试专为非0场景设计),实际={lastEod.PosiDividendSum}。" +
|
||||
$"若该交易已互换/平仓致归0,请换另一笔 PosiDividendSum≠0 的活交易。");
|
||||
|
||||
// dealDate 取 lastEod 次日,确保 GetPreEodDividendSum 读到这笔非0 EOD
|
||||
var dealDate = lastEod.ValueDate.AddDays(1);
|
||||
var service = new RealDbSwapDealService();
|
||||
decimal dividend = service.ExposeGetPreEodDividendSum(td.id, floatLeg.PositionId, dealDate);
|
||||
|
||||
// ---- 核心:返回值=真实非0的 PosiDividendSum,不是硬0 ----
|
||||
Assert.AreEqual(lastEod.PosiDividendSum, dividend, 0.01m,
|
||||
$"[非0场景·方案C验收] {nonZeroTrade} GetPreEodDividendSum 应返回 EOD PosiDividendSum={lastEod.PosiDividendSum}," +
|
||||
$"实际={dividend}。该值将同时成为 DividendPending(待结算分红收益,全量口径)。" +
|
||||
$"若实际=0,说明 DividendPending 硬0回归未修复,或传导链路被破坏(参见 SwapDealService.GetPreEodDividendSum 注释的口径论证)。");
|
||||
|
||||
Console.WriteLine($"[非0场景验证通过] {nonZeroTrade}: PosiDividendSum={dividend}(非0)→ DividendIn/DividendPending 均为此值,非硬0。");
|
||||
}
|
||||
finally { db?.Dispose(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,27 +111,25 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PartialCloseTradingFeeAndPendingFeeUseTheSameRoundedOriginalFeeAllocation()
|
||||
public void ManuallyAdjustedPendingFeeDoesNotOverrideBaseRateCloseFee()
|
||||
{
|
||||
var oriPosition = new swap_position
|
||||
{
|
||||
PosiFeeType = 0,
|
||||
PosiTradingFeeUnit = 1.1234m,
|
||||
PosiTradingFeePending = 113.46m
|
||||
PosiFeeType = 1,
|
||||
PosiTradingFeeUnit = 0.123456m,
|
||||
PosiTradingFeePending = 1235.56m
|
||||
};
|
||||
var unwindData = new UnwindData
|
||||
{
|
||||
NotionalValue = 10098m,
|
||||
CloseNotionalValue = 4039.2m,
|
||||
NotionalQty = 10000m,
|
||||
CloseQty = 4000m
|
||||
CloseQty = 10000m
|
||||
};
|
||||
|
||||
var tradingFee = InvokeCalcInitTradingFee(oriPosition, unwindData);
|
||||
var pendingFee = InvokeCalcInitTradingFeePending(oriPosition, new swap_position(), unwindData);
|
||||
|
||||
Assert.AreEqual(45.38m, tradingFee);
|
||||
Assert.AreEqual(45.38m, pendingFee);
|
||||
Assert.AreEqual(1234.56m, tradingFee);
|
||||
Assert.AreEqual(1235.56m, pendingFee);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -63,5 +63,35 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_预付金返息, service.ClientCashCalls[1].action);
|
||||
Console.WriteLine($"SI_002: 互换={service.ClientCashCalls[0].amount}, 预付金返息={service.ClientCashCalls[1].amount} ✅");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SI_003_SwapIncome_含预付金腿_不返还预付金本金()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.ExerciseDate = new DateTime(2026, 12, 31);
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(swapRealizedPnL: 0m);
|
||||
unwindData.FlowEvents.Add(new swap_flow_event
|
||||
{
|
||||
UnderlyingCode = "UT-FLOAT",
|
||||
MarkClosePnl = 100m,
|
||||
PayDirection = 1
|
||||
});
|
||||
unwindData.FlowEvents.Add(new swap_flow_event
|
||||
{
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestDirection = 1,
|
||||
InterestPrincipal = 10000m,
|
||||
InterestClosePnL = 2m
|
||||
});
|
||||
|
||||
service.SwapIncome(unwindData);
|
||||
|
||||
Assert.AreEqual(0m, unwindData.SwapMarginAmount, "手动互换不应返还预付金本金");
|
||||
Assert.IsFalse(service.ClientCashCalls.Any(x => x.action == ClientCashInCashOut.系统操作_应付预付金),
|
||||
"手动互换不应生成应付预付金流水");
|
||||
Assert.IsTrue(service.ClientCashCalls.Any(x => x.action == ClientCashInCashOut.系统操作_预付金返息),
|
||||
"手动互换仍应结算预付金返息");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace YLErp.Modules.SwapModule
|
||||
|
||||
// 输出别名(转发到基类捕获属性)
|
||||
public List<eod_swap_position> CreatedEodPositions => PersistedPositions;
|
||||
public List<swap_position> LastInterestCalculationPositions { get; private set; }
|
||||
|
||||
public TestableSwapEodService(
|
||||
List<trade> trades, List<swap_position> positions,
|
||||
@@ -55,6 +56,7 @@ namespace YLErp.Modules.SwapModule
|
||||
protected override List<trade_extend> FindTradeExtends(List<int> tradeIds) => _extends;
|
||||
protected override List<eod_swap> FindEodSwapsByDate(DateTime valueDate) => _eodSwaps;
|
||||
protected override List<swap_flow_event> FindFlowEvents(int swapTradeId, DateTime settleDate) => _flowEvents;
|
||||
protected override List<swap_flow_event> FindCompletedFlowEvents(List<int> tradeIds) => _flowEvents;
|
||||
protected override List<eod_swap_position> FindEodSwapPositions(int swapTradeId, DateTime preSettleDate)
|
||||
=> _eodPositions.Where(x => x.SwapTradeId == swapTradeId && x.ValueDate >= preSettleDate).ToList();
|
||||
protected override List<swap_position> FindSwapPositions(int swapTradeId)
|
||||
@@ -80,7 +82,17 @@ namespace YLErp.Modules.SwapModule
|
||||
decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue,
|
||||
decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, bool needPrice,
|
||||
decimal grossPrice, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false,
|
||||
List<swap_flow_event> closeList = null) => new List<swap_flow_event>();
|
||||
List<swap_flow_event> closeList = null)
|
||||
{
|
||||
LastInterestCalculationPositions = positions;
|
||||
return positions.Select(position => new swap_flow_event
|
||||
{
|
||||
PositionId = position.id,
|
||||
InterestPrincipal = 1000m,
|
||||
InterestRate = 0.01m,
|
||||
FloatRate = 0.01m
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public void ExecuteSwapPositionCompose(DateTime settleDate, DateTime preSettleDate)
|
||||
=> SwapPositionCompose(settleDate, preSettleDate, null);
|
||||
@@ -250,5 +262,173 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.IsTrue(ex.Message.Contains("未收盘"), $"异常消息应含'未收盘',实际:{ex.Message}");
|
||||
Console.WriteLine($"SPC_004: 抛异常'{ex.Message}' ✅");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SPC_005_部分平仓后_预付金日终按实时剩余本金计息()
|
||||
{
|
||||
const long initialPrepayId = 2;
|
||||
var td = CreateTrade();
|
||||
var initialPrepay = new swap_position
|
||||
{
|
||||
id = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 1000m, IsInitial = true, Invalid = false,
|
||||
PosiStartDate = SettleDate.AddDays(-1), PosiMatuirityDate = td.ExerciseDate.Value,
|
||||
InterestSwapInterval = "[]"
|
||||
};
|
||||
var realPrepay = new swap_position
|
||||
{
|
||||
id = 3, PositionId = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 700m, IsInitial = false, Invalid = false
|
||||
};
|
||||
var prepayEod = new eod_swap_position
|
||||
{
|
||||
id = 200, SwapTradeId = SwapTradeId, PositionId = initialPrepayId,
|
||||
ValueDate = PreSettleDate, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 700m, TdInterestPrincipal = 700m
|
||||
};
|
||||
var service = new TestableSwapEodService(
|
||||
new List<trade> { td },
|
||||
new List<swap_position> { CreateFloatPosition(1, 1000), initialPrepay, realPrepay },
|
||||
new List<eod_swap_position> { CreateFloatEodPosition(1, 1000, 1.0020m), prepayEod },
|
||||
new List<eod_swap> { new eod_swap { SwapTradeId = SwapTradeId, ValueDate = PreSettleDate } },
|
||||
new List<trade_extend> { CreateExtend() },
|
||||
new List<swap_flow_event>
|
||||
{
|
||||
CreateCloseFlowEvent(1, 300),
|
||||
new swap_flow_event
|
||||
{
|
||||
SwapTradeId = SwapTradeId, PositionId = initialPrepayId,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
EventDate = SettleDate,
|
||||
DataState = (int)SwapFlowDateStateEnum.完成
|
||||
}
|
||||
});
|
||||
|
||||
service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate);
|
||||
|
||||
var calculatedPrepay = service.LastInterestCalculationPositions
|
||||
.Single(x => x.id == initialPrepayId);
|
||||
Assert.AreEqual(700m, calculatedPrepay.InterestPrincipalFix);
|
||||
Assert.AreEqual(initialPrepayId, calculatedPrepay.id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SPC_006_平仓日_预付金日终不得重复扣减实时剩余本金()
|
||||
{
|
||||
const long initialPrepayId = 2;
|
||||
var td = CreateTrade();
|
||||
var initialPrepay = new swap_position
|
||||
{
|
||||
id = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 1000m, IsInitial = true, Invalid = false,
|
||||
IsAnnualized = true,
|
||||
PosiStartDate = SettleDate.AddDays(-1), PosiMatuirityDate = td.ExerciseDate.Value,
|
||||
InterestSwapInterval = "[]"
|
||||
};
|
||||
var realPrepay = new swap_position
|
||||
{
|
||||
id = 3, PositionId = initialPrepayId, SwapTradeId = SwapTradeId, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 700m, IsInitial = false, Invalid = false
|
||||
};
|
||||
var prepayEod = new eod_swap_position
|
||||
{
|
||||
id = 200, SwapTradeId = SwapTradeId, PositionId = initialPrepayId,
|
||||
ValueDate = PreSettleDate, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 1000m, TdInterestPrincipal = 1000m
|
||||
};
|
||||
var closeFlow = CreateCloseFlowEvent(1, 300);
|
||||
closeFlow.InterestRate = 0.01m;
|
||||
var prepayCloseFlow = new swap_flow_event
|
||||
{
|
||||
SwapTradeId = SwapTradeId, PositionId = initialPrepayId,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
EventDate = SettleDate, DataState = (int)SwapFlowDateStateEnum.完成,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipal = 300m
|
||||
};
|
||||
var service = new TestableSwapEodService(
|
||||
new List<trade> { td },
|
||||
new List<swap_position> { CreateFloatPosition(1, 1000), initialPrepay, realPrepay },
|
||||
new List<eod_swap_position> { CreateFloatEodPosition(1, 1000, 1.0020m), prepayEod },
|
||||
new List<eod_swap> { new eod_swap { SwapTradeId = SwapTradeId, ValueDate = PreSettleDate } },
|
||||
new List<trade_extend> { CreateExtend() },
|
||||
new List<swap_flow_event> { closeFlow, prepayCloseFlow });
|
||||
|
||||
service.ExecuteSwapPositionCompose(SettleDate, PreSettleDate);
|
||||
|
||||
var persistedPrepay = service.CreatedEodPositions
|
||||
.Single(x => x.PositionId == initialPrepayId);
|
||||
Assert.AreEqual(700m, persistedPrepay.InterestPrincipalFix,
|
||||
"实时腿已经扣减到700,日终不得再次按平仓比例扣减");
|
||||
Assert.AreEqual(700m, persistedPrepay.TdInterestPrincipal,
|
||||
"平仓日预付金计息本金应立即切换为实时剩余本金");
|
||||
Assert.AreEqual(700m * 0.01m / 365m, persistedPrepay.TdInterestIncome,
|
||||
"平仓日新增利息应按实时剩余本金计算");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SPC_007_HistoricalReplayUsesAsOfPrincipal()
|
||||
{
|
||||
const long originalPositionId = 2;
|
||||
var original = new swap_position
|
||||
{
|
||||
id = originalPositionId, PosiDirection = 0,
|
||||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 10000m
|
||||
};
|
||||
var realtime = new swap_position
|
||||
{
|
||||
PositionId = originalPositionId,
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipalFix = 7000m
|
||||
};
|
||||
var close = new swap_flow_event
|
||||
{
|
||||
PositionId = originalPositionId,
|
||||
PositionType = 0,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
EventDate = new DateTime(2026, 7, 9),
|
||||
InterestMode = (int)InterestModeEnum.初始预付金,
|
||||
InterestPrincipal = 3000m
|
||||
};
|
||||
var floatClose = new swap_flow_event
|
||||
{
|
||||
PositionId = 1,
|
||||
PositionType = 1,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
EventDate = new DateTime(2026, 7, 9),
|
||||
TradingAmount = 3000000m
|
||||
};
|
||||
var originalWithFloat = new List<swap_position>
|
||||
{
|
||||
original,
|
||||
new swap_position { id = 1, PosiDirection = 1, PosiNotionalValue = 10000000m }
|
||||
};
|
||||
|
||||
var beforeClose = SwapDealService.ResolveInterestLegPositionsAsOf(
|
||||
originalWithFloat, new List<swap_position> { realtime },
|
||||
new[] { close, floatClose }, new DateTime(2026, 7, 8))
|
||||
.Single(x => x.id == originalPositionId);
|
||||
var onCloseDate = SwapDealService.ResolveInterestLegPositionsAsOf(
|
||||
originalWithFloat, new List<swap_position> { realtime },
|
||||
new[] { close, floatClose }, new DateTime(2026, 7, 9))
|
||||
.Single(x => x.id == originalPositionId);
|
||||
|
||||
Assert.AreEqual(10000m, beforeClose.InterestPrincipalFix);
|
||||
Assert.AreEqual(7000m, onCloseDate.InterestPrincipalFix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,35 @@ namespace YLErp.Modules.SwapModule
|
||||
Console.WriteLine($"UW_005: 反序列化SwapRealizedPnL=8000, 资金流水={service.ClientCashCalls[0].amount}, TradeStatus={td.TradeStatus} ✅");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UW_005A_ApproveSwapTrade_互换审核_不返还预付金本金()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.ExerciseDate = new DateTime(2026, 12, 31);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 100m, swapMarginAmount: -10000m);
|
||||
var swapEvent = new swap_event
|
||||
{
|
||||
id = 2,
|
||||
SwapTradeId = SwapDealTestFactory.SwapTradeId,
|
||||
EventType = (int)SwapEventTypeEnum.互换,
|
||||
Invalid = false,
|
||||
EventData = JsonConvert.SerializeObject(unwindData)
|
||||
};
|
||||
var service = new TestableSwapDealService(td,
|
||||
swapEvents: new Dictionary<int, swap_event>
|
||||
{
|
||||
[(int)SwapEventTypeEnum.互换] = swapEvent
|
||||
});
|
||||
|
||||
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.互换);
|
||||
|
||||
Assert.AreEqual(1, service.ClientCashCalls.Count, "互换审批只应生成互换结算流水");
|
||||
Assert.AreEqual(ClientCashInCashOut.系统操作_互换, service.ClientCashCalls[0].action);
|
||||
Assert.IsFalse(service.ClientCashCalls.Any(x => x.action == ClientCashInCashOut.系统操作_应付预付金),
|
||||
"互换审批不应生成应付预付金流水");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 场景6:ApplySwapTrade 提交审核 —— 前置校验与保存事件
|
||||
// ================================================================
|
||||
@@ -197,6 +226,8 @@ namespace YLErp.Modules.SwapModule
|
||||
public void UW_007_SwapUnwind_占期初A转占剩余B_全平判定正确()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 600000;
|
||||
td.TradeAmount = 600000;
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 0m, closeMethod: (int)CloseMethodEnum.全部平仓, closePercent: 0.6m,
|
||||
@@ -259,6 +290,33 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.AreEqual(500000.01, td.StockEqvNotional, 0.000001, "trade 剩余名义本金应在扣减后舍入两位小数");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UW_013_SwapUnwind_合法零点零一剩余不应判定全平()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 1000000.01;
|
||||
td.TradeAmount = 10000.01;
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 0m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓,
|
||||
closePercent: 1000000m / 1000000.01m,
|
||||
closeQty: 10000m,
|
||||
closeNotionalValue: 1000000m,
|
||||
positionQty: 10000.01m);
|
||||
unwindData.NotionalValue = 1000000.01m;
|
||||
unwindData.PosiNotionalValue = 1000000.01m;
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual("确认成交", td.TradeStatus,
|
||||
"剩余名义本金和数量均为0.01时仍应保持部分平仓状态");
|
||||
Assert.AreEqual(1, td.HasPartialUnWind,
|
||||
"合法的0.01尾差不应被清零");
|
||||
Assert.AreEqual(0.01, td.StockEqvNotional, 0.000001);
|
||||
Assert.AreEqual(0.01, td.TradeAmount, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UW_010_SwapUnwind_现金与两位利息事件保持一致()
|
||||
{
|
||||
@@ -289,5 +347,183 @@ namespace YLErp.Modules.SwapModule
|
||||
Assert.AreEqual(10m, unwindData.SwapRealizedPnL);
|
||||
Assert.AreEqual(-10d, service.ClientCashCalls[0].amount, 0.001d);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UW_011_trade2308_full_close_uses_remaining_values_and_zeroes_tail()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 4906156.15;
|
||||
td.TradeAmount = 5000000;
|
||||
td.Notional = 5000000;
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 0m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓,
|
||||
closePercent: 0.5m,
|
||||
closeQty: 5000000.01m,
|
||||
closeNotionalValue: 4906156.15m,
|
||||
positionQty: 5000000m);
|
||||
unwindData.NotionalValue = 9812312.31m;
|
||||
unwindData.PosiNotionalValue = 4906156.15m;
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
var saved = service.SaveSwapDealCalls[0].data;
|
||||
Assert.AreEqual((int)CloseMethodEnum.部分平仓, saved.CloseMethod,
|
||||
"CloseMethod 保留本次部分平仓意图,终态由扣减后的持仓事实决定");
|
||||
Assert.AreEqual(5000000m, saved.CloseQty);
|
||||
Assert.AreEqual(4906156.15m, saved.CloseNotionalValue);
|
||||
Assert.AreEqual(0d, td.StockEqvNotional, 0.000001);
|
||||
Assert.AreEqual(0d, td.TradeAmount, 0.000001);
|
||||
Assert.AreEqual(0d, td.Notional, 0.000001);
|
||||
Assert.AreEqual("已平仓", td.TradeStatus);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UW_014_full_close_quantity_normalization_recalculates_pnl_and_cash()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 4906156.15;
|
||||
td.TradeAmount = 5000000;
|
||||
td.Notional = 5000000;
|
||||
var service = new TestableSwapDealService(td);
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 50000000.10m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓,
|
||||
closePercent: 0.5m,
|
||||
closeQty: 5000000.01m,
|
||||
closeNotionalValue: 4906156.15m,
|
||||
positionQty: 5000000m);
|
||||
unwindData.NotionalValue = 9812312.31m;
|
||||
unwindData.PosiNotionalValue = 4906156.15m;
|
||||
unwindData.SwapCloseAmount = 50000000.10m;
|
||||
var floatEvent = new swap_flow_event
|
||||
{
|
||||
UnderlyingCode = "UT-FLOAT",
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
PayDirection = 1,
|
||||
PosiGrossPrice = 1m,
|
||||
TradingAmountAvg = 11m,
|
||||
MarkClosePnl = 50000000.10m
|
||||
};
|
||||
unwindData.FlowEvents.Add(floatEvent);
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
Assert.AreEqual(5000000m, unwindData.CloseQty);
|
||||
Assert.AreEqual(50000000m, floatEvent.MarkClosePnl);
|
||||
Assert.AreEqual(50000000m, unwindData.SwapRealizedPnL);
|
||||
Assert.AreEqual(50000000m, unwindData.SwapCloseAmount);
|
||||
Assert.AreEqual(-50000000d, service.ClientCashCalls.Single().amount, 0.001d);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UW_012_approve_restores_A_to_B_and_normalizes_flow_for_full_close()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 4906156.15;
|
||||
td.TradeAmount = 5000000;
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 0m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓,
|
||||
closePercent: 4906156.15m / 9812312.31m,
|
||||
closeQty: 5000000.01m,
|
||||
closeNotionalValue: 4906156.15m,
|
||||
positionQty: 5000000m);
|
||||
unwindData.NotionalValue = 9812312.31m;
|
||||
unwindData.PosiNotionalValue = 4906156.15m;
|
||||
var floatEvent = new swap_flow_event
|
||||
{
|
||||
EventId = 1,
|
||||
UnderlyingCode = "261031.IB",
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
Quantity = 5000000.01m,
|
||||
PositionQty = -0.01m
|
||||
};
|
||||
var swapEvent = new swap_event
|
||||
{
|
||||
id = 1,
|
||||
SwapTradeId = SwapDealTestFactory.SwapTradeId,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
Invalid = false,
|
||||
EventData = JsonConvert.SerializeObject(unwindData)
|
||||
};
|
||||
var service = new TestableSwapDealService(td,
|
||||
swapEvents: new Dictionary<int, swap_event>
|
||||
{
|
||||
[(int)SwapEventTypeEnum.平仓] = swapEvent
|
||||
},
|
||||
flowEventsByEventId: new Dictionary<long, List<swap_flow_event>>
|
||||
{
|
||||
[1] = new List<swap_flow_event> { floatEvent }
|
||||
});
|
||||
|
||||
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
Assert.AreEqual(1m, swapEvent.unwindData.ClosePercent);
|
||||
Assert.AreEqual((int)CloseMethodEnum.部分平仓, swapEvent.unwindData.CloseMethod,
|
||||
"审批不应把部分平仓事件改写为全平意图");
|
||||
Assert.AreEqual(5000000m, swapEvent.unwindData.CloseQty);
|
||||
Assert.AreEqual(4906156.15m, swapEvent.unwindData.CloseNotionalValue);
|
||||
Assert.AreEqual(5000000m, floatEvent.Quantity);
|
||||
Assert.AreEqual(0m, floatEvent.PositionQty);
|
||||
Assert.AreEqual("已平仓", td.TradeStatus);
|
||||
Assert.AreEqual(0d, td.StockEqvNotional, 0.000001);
|
||||
Assert.AreEqual(0d, td.TradeAmount, 0.000001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UW_015_approve_full_close_recalculates_normalized_pnl_before_cash()
|
||||
{
|
||||
var td = SwapDealTestFactory.CreateTrade();
|
||||
td.StockEqvNotional = 4906156.15;
|
||||
td.TradeAmount = 5000000;
|
||||
var unwindData = SwapDealTestFactory.CreateUnwindData(
|
||||
swapRealizedPnL: 50000000.10m,
|
||||
closeMethod: (int)CloseMethodEnum.部分平仓,
|
||||
closePercent: 4906156.15m / 9812312.31m,
|
||||
closeQty: 5000000.01m,
|
||||
closeNotionalValue: 4906156.15m,
|
||||
positionQty: 5000000m);
|
||||
unwindData.NotionalValue = 9812312.31m;
|
||||
unwindData.PosiNotionalValue = 4906156.15m;
|
||||
unwindData.SwapCloseAmount = 50000000.10m;
|
||||
var floatEvent = new swap_flow_event
|
||||
{
|
||||
EventId = 1,
|
||||
UnderlyingCode = "261031.IB",
|
||||
PositionType = (int)PositionTypeFlag.Long,
|
||||
PayDirection = 1,
|
||||
PosiGrossPrice = 1m,
|
||||
TradingAmountAvg = 11m,
|
||||
MarkClosePnl = 50000000.10m,
|
||||
Quantity = 5000000.01m,
|
||||
PositionQty = -0.01m
|
||||
};
|
||||
var swapEvent = new swap_event
|
||||
{
|
||||
id = 1,
|
||||
SwapTradeId = SwapDealTestFactory.SwapTradeId,
|
||||
EventType = (int)SwapEventTypeEnum.平仓,
|
||||
Invalid = false,
|
||||
EventData = JsonConvert.SerializeObject(unwindData)
|
||||
};
|
||||
var service = new TestableSwapDealService(td,
|
||||
swapEvents: new Dictionary<int, swap_event>
|
||||
{
|
||||
[(int)SwapEventTypeEnum.平仓] = swapEvent
|
||||
},
|
||||
flowEventsByEventId: new Dictionary<long, List<swap_flow_event>>
|
||||
{
|
||||
[1] = new List<swap_flow_event> { floatEvent }
|
||||
});
|
||||
|
||||
service.ApproveSwapTrade(td, (int)SwapEventTypeEnum.平仓);
|
||||
|
||||
Assert.AreEqual(5000000m, swapEvent.unwindData.CloseQty);
|
||||
Assert.AreEqual(50000000m, swapEvent.unwindData.SwapRealizedPnL);
|
||||
Assert.AreEqual(-50000000d, service.ClientCashCalls.Single().amount, 0.001d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ namespace YLErp.Model
|
||||
public string AccountCapitalTopicGroupId { get; set; }
|
||||
|
||||
public int AutoOffsetReset { get; set; }
|
||||
/// <summary>
|
||||
/// TRS合约数据推送topic(对外,如onebp等)
|
||||
/// </summary>
|
||||
public string ContractTopic { get; set; } = "onederi.trs.onebp.contract.v1";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ namespace YLErp.Modules.EodModule
|
||||
|
||||
private Dictionary<string,long> GetTFeatureBondInnerCode(List<string> underlyingCodes,DateTime valueDate,DbConnection conn)
|
||||
{
|
||||
var sql = "SELECT contractcode,deliverableinnercode,spread FROM fut_cgbderiv WHERE TradingDay = @ValueDate AND pricetype = 3 AND (contractcode,irr) IN (SELECT contractcode,MAX(irr) FROM fut_cgbderiv WHERE TradingDay = @ValueDate AND contractcode IN (@UmCodes) AND pricetype = 3 GROUP BY contractcode);";
|
||||
var sql = "SELECT contractcode,deliverableinnercode,spread FROM fut_cgbderiv WHERE tradingday = @ValueDate AND pricetype = 3 AND (contractcode,irr) IN (SELECT contractcode,MAX(irr) FROM fut_cgbderiv WHERE tradingday = @ValueDate AND contractcode IN (@UmCodes) AND pricetype = 3 GROUP BY contractcode);";
|
||||
var datas = conn.Query<TFeatureBondInnerCodeQueryDto>(sql, new
|
||||
{
|
||||
ValueDate = valueDate,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Linq.Dynamic.Core;
|
||||
using YLErp.BLL;
|
||||
using YLErp.Configuration.Enums;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
|
||||
namespace YLErp.Modules.EodModule.SettlementModule
|
||||
@@ -7,7 +7,7 @@ namespace YLErp.Modules.EodModule.SettlementModule
|
||||
/// <summary>
|
||||
/// 检查当日结算价,是否全部进系统。
|
||||
/// </summary>
|
||||
class EodCheckSettlePrice : EodSettleServiceBaseV2
|
||||
public class EodCheckSettlePrice : EodSettleServiceBaseV2
|
||||
{
|
||||
public const string Step = "检查标的结算价格缺失";
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace YLErp.Modules.EodModule.SettlementModule
|
||||
|
||||
allQuery = DbContext.trade.Where(tradePredicate).Select(n => n.UnderlyingCode).Distinct();
|
||||
|
||||
if (PS.Config.ErpElement.ForwardTradePriceModel == Configuration.Enums.ForwardTradePriceModel.STANDARD)
|
||||
if (PS.Config.ErpElement.ForwardTradePriceModel == ForwardTradePriceModel.STANDARD)
|
||||
{
|
||||
allQuery = allQuery.Union(DbContext.trade.Where(tradePredicate).Where(x => x.BasisUnderlyingCode != null && x.BasisUnderlyingCode != "").Select(n => n.BasisUnderlyingCode).Distinct());
|
||||
}
|
||||
@@ -88,14 +88,8 @@ namespace YLErp.Modules.EodModule.SettlementModule
|
||||
allQuery = allQuery == null ? exTradeQuery.Distinct() : allQuery.Union(exTradeQuery.Distinct());
|
||||
|
||||
var preSettleDate = _context.PreSettleDate;
|
||||
//最后一个交易日持仓信息
|
||||
var futureTypes = ConsGlobal.InstrumentType.GetFutureTypes();
|
||||
var positionQuery = from t in DbContext.eod_trade_position
|
||||
join um in DbContext.underlying_manager on t.UnderlyingCode equals um.UnderlyingCode
|
||||
where t.ValueDate == preSettleDate && t.Amount != 0
|
||||
&& ConsTrade.TradeTypesForHedge.Contains(t.TradeType)
|
||||
&& (!futureTypes.Contains(um.UnderlyingInstrumentType) || um.MaturityDate >= settleDate)
|
||||
select t.UnderlyingCode;
|
||||
// 持仓分支传入 clienIds:使上一交易日持仓标的按当前收盘客户作用域收敛(防御点详见 QueryPositionUnderlyingCodes)。
|
||||
var positionQuery = QueryPositionUnderlyingCodes(DbContext, preSettleDate, settleDate, clienIds);
|
||||
|
||||
allQuery = allQuery.Union(positionQuery.Distinct());
|
||||
}
|
||||
@@ -130,6 +124,41 @@ namespace YLErp.Modules.EodModule.SettlementModule
|
||||
_context.RaiseError(Step, "标的代码:" + codes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取上一交易日持仓中需结算价的标的(持仓分支)。
|
||||
/// 抽成 static 以便单测直接覆盖 ClientId 收敛语义。
|
||||
///
|
||||
/// ── 防御行为(EQD-6967)─────────────────────────────────────────────
|
||||
/// 收盘缺失价检查在“按客户作用域结算(ClientIds 非 null)”时,本应只校验
|
||||
/// 当前收盘客户自己的标的,而不应把“其他客户”或“系统级(ClientId=0)”的
|
||||
/// 上一交易日持仓标的误报为缺失价。历史上持仓分支完全未引用 ClientId,
|
||||
/// 导致客户 A 收盘时被其他客户/系统级持仓的标的噪声干扰(见缺陷现象)。
|
||||
///
|
||||
/// 本方法通过 clienIds 过滤做收敛,与 OTC 分支、客户产品分支已有的
|
||||
/// ClientId 过滤语义保持一致:
|
||||
/// · clienIds == null → 系统级全量结算,短路为 true(不收窄,零回归);
|
||||
/// · clienIds != null → 仅返回属于指定客户的持仓标的(Contains(t.ClientId)),
|
||||
/// 过滤掉其他客户及系统级(ClientId=0)持仓噪声。
|
||||
/// 注意:场外交易结算模式(IsSettleExchangeTrades=false)下持仓分支整块跳过,
|
||||
/// 此时本方法不会被调用,属操作层面的规避而非修复。
|
||||
/// ───────────────────────────────────────────────────────────────────
|
||||
/// </summary>
|
||||
public static IQueryable<string> QueryPositionUnderlyingCodes(YLContext db, DateTime preSettleDate, DateTime settleDate, IEnumerable<int> clienIds)
|
||||
{
|
||||
var futureTypes = ConsGlobal.InstrumentType.GetFutureTypes();
|
||||
return from t in db.eod_trade_position
|
||||
join um in db.underlying_manager on t.UnderlyingCode equals um.UnderlyingCode
|
||||
where t.ValueDate == preSettleDate && t.Amount != 0
|
||||
&& ConsTrade.TradeTypesForHedge.Contains(t.TradeType)
|
||||
&& (!futureTypes.Contains(um.UnderlyingInstrumentType) || um.MaturityDate >= settleDate)
|
||||
// 【防御点·EQD-6967】按客户作用域收敛持仓标的:
|
||||
// clienIds==null → 系统级全量结算,不收窄(零回归);
|
||||
// clienIds!=null → 仅保留指定客户持仓,过滤掉其他客户/系统级(ClientId=0)持仓噪声。
|
||||
&& (clienIds == null || clienIds.Contains(t.ClientId))
|
||||
select t.UnderlyingCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取互换标的
|
||||
/// </summary>
|
||||
|
||||
@@ -47,6 +47,65 @@ namespace YLErp.Modules.SwapModule
|
||||
unwindData.CloseNotionalValue = Math.Round(unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static bool NormalizeFullCloseRequest(UnwindData unwindData)
|
||||
{
|
||||
if (unwindData.CloseMethod != (int)CloseMethodEnum.全部平仓
|
||||
&& unwindData.ClosePercent < 1
|
||||
&& !(unwindData.PositionQty > 0 && unwindData.CloseQty >= unwindData.PositionQty)
|
||||
&& !(unwindData.PosiNotionalValue > 0 && unwindData.CloseNotionalValue >= unwindData.PosiNotionalValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var closeQty = unwindData.CloseQty;
|
||||
var closeNotionalValue = unwindData.CloseNotionalValue;
|
||||
unwindData.ClosePercent = 1;
|
||||
if (unwindData.PositionQty > 0) unwindData.CloseQty = unwindData.PositionQty;
|
||||
if (unwindData.PosiNotionalValue > 0) unwindData.CloseNotionalValue = unwindData.PosiNotionalValue;
|
||||
return closeQty != unwindData.CloseQty || closeNotionalValue != unwindData.CloseNotionalValue;
|
||||
}
|
||||
|
||||
private static void RecalculateNormalizedUnwindAmounts(UnwindData unwindData)
|
||||
{
|
||||
var floatLeg = unwindData.FlowEvents.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
|
||||
if (floatLeg == null || floatLeg.PosiGrossPrice == 0) return;
|
||||
|
||||
var input = new UnwindInput
|
||||
{
|
||||
Multiplier = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType) ? 100 : 1,
|
||||
PosiGrossPrice = floatLeg.PosiGrossPrice,
|
||||
TradingAmountAvg = floatLeg.TradingAmountAvg,
|
||||
CloseQty = unwindData.CloseQty,
|
||||
PositionQty = unwindData.PositionQty,
|
||||
ContractSize = floatLeg.ContractSize,
|
||||
CloseNotionalValue = unwindData.CloseNotionalValue,
|
||||
PayDirection = floatLeg.PayDirection,
|
||||
PositionType = floatLeg.PositionType,
|
||||
TradingFee = floatLeg.TradingFee.ToString(),
|
||||
TradingFeePending = floatLeg.TradingFeePending.ToString(),
|
||||
DividendIn = floatLeg.DividendIn.ToString()
|
||||
};
|
||||
foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
var target = leg.InterestMode == (int)InterestModeEnum.初始预付金
|
||||
|| leg.InterestMode == (int)InterestModeEnum.追加预付金
|
||||
? input.MarginLegs
|
||||
: input.InterestLegs;
|
||||
target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
|
||||
}
|
||||
|
||||
var result = FrontendCalcReference.CalcUnwind(input);
|
||||
floatLeg.MarkClosePnl = result.MarkClosePnl;
|
||||
unwindData.SwapCloseAmount = result.SwapCloseAmount;
|
||||
unwindData.SwapRealizedPnL = result.SwapRealizedPnL;
|
||||
unwindData.SwapMarginRebatePnl = result.SwapMarginRebatePnl;
|
||||
}
|
||||
|
||||
private static bool IsFullCloseAfterDeduction(UnwindData unwindData, double remainingNotional, double remainingQuantity)
|
||||
{
|
||||
return unwindData.ClosePercent == 1 || (remainingNotional == 0 && remainingQuantity == 0);
|
||||
}
|
||||
|
||||
// 待实现利息会进入 decimal(30,12) 日终快照
|
||||
private const int InterestCalculationPrecision = 12;
|
||||
|
||||
@@ -85,6 +144,10 @@ namespace YLErp.Modules.SwapModule
|
||||
if (unwindData.FlowEvents.Any(x => !string.IsNullOrEmpty(x.UnderlyingCode)))
|
||||
{
|
||||
CalcCloseAmount(unwindData);
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
unwindData.SwapMarginAmount = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -287,7 +350,15 @@ namespace YLErp.Modules.SwapModule
|
||||
floatEvent.PositionId = position.PositionId;
|
||||
floatEvent.EventType = (int)SwapEventTypeEnum.平仓;
|
||||
floatEvent.EventReason = "交易";
|
||||
floatEvent.DividendIn = 0;
|
||||
// 方案C:分红收益改由上一收盘日 EOD PosiDividendSum 提供(单一可信源),
|
||||
// 前端 getDivindIn 不再覆盖;消除"期初持仓×totalInterest"对已平仓部分的重复计入。
|
||||
// 同一 EOD 值取一次喂两栏:
|
||||
// DividendIn = "浮动端平仓盈亏·分红"(本次动作要落袋的,落库后被前端按需展示)
|
||||
// DividendPending = "待结算分红收益"(仍挂在账上、未来才结的存量 = PosiDividendSum 全量口径,
|
||||
// 见 GetPreEodDividendSum 注释的口径论证;切勿改回硬0或分摊,会落库回归)
|
||||
decimal preEodDividendSum = GetPreEodDividendSum(tradeId, position.PositionId, dealDate);
|
||||
floatEvent.DividendIn = preEodDividendSum;
|
||||
floatEvent.DividendPending = preEodDividendSum;
|
||||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||||
floatEvent.CloseFee = 0;
|
||||
@@ -324,19 +395,12 @@ namespace YLErp.Modules.SwapModule
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (oriPosition.PosiTradingFeeUnit == 0)
|
||||
if (oriPosition.PosiFeeType == 1)
|
||||
{
|
||||
return 0;
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit * unwindData.CloseQty, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
|
||||
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
|
||||
if (originalBase <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static decimal CalcInitTradingFeePending(swap_position oriPosition, swap_position position, UnwindData unwindData)
|
||||
@@ -346,7 +410,14 @@ namespace YLErp.Modules.SwapModule
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
return CalcInitTradingFee(oriPosition, unwindData);
|
||||
var closeBase = oriPosition.PosiFeeType == 1 ? unwindData.CloseQty : unwindData.CloseNotionalValue;
|
||||
var originalBase = oriPosition.PosiFeeType == 1 ? unwindData.NotionalQty : unwindData.NotionalValue;
|
||||
if (originalBase <= 0)
|
||||
{
|
||||
return position?.PosiTradingFeePending ?? 0;
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeePending * closeBase / originalBase, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
/// <summary>
|
||||
/// 校验上日是否收盘
|
||||
@@ -498,7 +569,9 @@ namespace YLErp.Modules.SwapModule
|
||||
if (position != null)
|
||||
{
|
||||
floatEvent.PositionId = position.PositionId;
|
||||
floatEvent.DividendIn = 0;
|
||||
// 方案C:分红收益改由上一收盘日 EOD PosiDividendSum 提供(单一可信源),
|
||||
// 前端 getDivindIn 不再覆盖;消除"期初持仓×totalInterest"对已平仓部分的重复计入。
|
||||
floatEvent.DividendIn = GetPreEodDividendSum(tradeId, position.PositionId, dealDate);
|
||||
floatEvent.UnderlyingCode = position.UnderlyingCode;
|
||||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||||
floatEvent.CloseFee = 0;
|
||||
@@ -606,6 +679,65 @@ namespace YLErp.Modules.SwapModule
|
||||
return p;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算预付金腿当前真实持仓 (当前持仓+未来持仓)
|
||||
/// </summary>
|
||||
/// <param name="origPositions"></param>
|
||||
/// <param name="realPositions"></param>
|
||||
/// <param name="completedFlowEvents"></param>
|
||||
/// <param name="settleDate"></param>
|
||||
/// <returns></returns>
|
||||
public static List<swap_position> ResolveInterestLegPositionsAsOf(
|
||||
List<swap_position> origPositions, List<swap_position> realPositions,
|
||||
IEnumerable<swap_flow_event> completedFlowEvents, DateTime settleDate)
|
||||
{
|
||||
realPositions ??= new List<swap_position>();
|
||||
var futureFlows = (completedFlowEvents ?? Enumerable.Empty<swap_flow_event>())
|
||||
.Where(x => x.EventType == (int)SwapEventTypeEnum.平仓 && x.EventDate > settleDate)
|
||||
.ToList();
|
||||
var originalNotional = origPositions.Where(x => x.PosiDirection > 0)
|
||||
.Sum(x => x.PosiNotionalValue);
|
||||
var futureCloseNotional = futureFlows.Where(x => x.PositionType > 0)
|
||||
.Sum(x => x.TradingAmount);
|
||||
var hasNotionalFlows = futureCloseNotional > 0 && originalNotional > 0;
|
||||
var futureClosePrincipal = futureFlows
|
||||
.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金
|
||||
|| x.InterestMode == (int)InterestModeEnum.追加预付金)
|
||||
.GroupBy(x => x.PositionId)
|
||||
.ToDictionary(x => x.Key, x => x.Sum(v => v.InterestPrincipal));
|
||||
var priorClosePositionIds = new HashSet<long>((completedFlowEvents ?? Enumerable.Empty<swap_flow_event>())
|
||||
.Where(x => x.EventType == (int)SwapEventTypeEnum.平仓 && x.EventDate <= settleDate)
|
||||
.Select(x => x.PositionId));
|
||||
|
||||
return origPositions.Where(x => x.PosiDirection == 0).Select(p =>
|
||||
{
|
||||
if (p.InterestMode == (int)InterestModeEnum.初始预付金
|
||||
|| p.InterestMode == (int)InterestModeEnum.追加预付金)
|
||||
{
|
||||
var realLeg = realPositions.FirstOrDefault(r => r.PositionId == p.id);
|
||||
if (realLeg != null)
|
||||
{
|
||||
if (!priorClosePositionIds.Contains(p.id))
|
||||
{
|
||||
return p;
|
||||
}
|
||||
var futurePrincipal = hasNotionalFlows
|
||||
? p.InterestPrincipalFix * futureCloseNotional / originalNotional
|
||||
: futureClosePrincipal.TryGetValue(p.id, out var flowPrincipal) ? flowPrincipal : 0m;
|
||||
var asOfPrincipal = realLeg.InterestPrincipalFix + futurePrincipal;
|
||||
asOfPrincipal = Math.Min(p.InterestPrincipalFix, Math.Max(0m, asOfPrincipal));
|
||||
if (asOfPrincipal != p.InterestPrincipalFix)
|
||||
{
|
||||
var clone = p.Clone();
|
||||
clone.InterestPrincipalFix = asOfPrincipal;
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static decimal ResolveUnwindPreviousNotional(
|
||||
eod_swap lastEod,
|
||||
@@ -834,6 +966,47 @@ namespace YLErp.Modules.SwapModule
|
||||
return notionalValue > 0 ? posiNotionalValue / notionalValue : 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取"上一收盘日"浮动腿的待实现分红(eod_swap_position.PosiDividendSum),
|
||||
/// 用于平仓/互换预览页展示"浮动端平仓盈亏·分红(DividendIn)" 与 "待结算分红收益(DividendPending)"。
|
||||
/// <para>方案C:替代前端 totalInterest × 期初持仓 的重算——后者会把登记日前已平仓、
|
||||
/// 不享有该笔分红的部分重复计入(GLMS-20260105-0004 误显 -36,160)。
|
||||
/// EOD 的 PosiDividendSum 已按"实际持仓递推 + 当日实现扣除"算出待实现分红,
|
||||
/// 是单一可信源。</para>
|
||||
/// <para>复用 GetUnwindInterests(cs:624-626) 的"上一 EOD 日期"推导:取 eod_swap 中
|
||||
/// ValueDate < dealDate 的最大日期,无则 dealDate.AddDays(-1);再经
|
||||
/// SwapEodPositionService.GetPreEodPositions 取该日持仓,匹配 PositionId。</para>
|
||||
/// <para>抽为 protected virtual:与 GetMaxIncomeValueDate 一致,便于测试替身覆写、
|
||||
/// 也兼容无 EOD 的边界(返回 0,与历史 DividendIn=0 行为一致)。</para>
|
||||
/// </summary>
|
||||
/// <returns>上一收盘日该浮动腿的待实现分红;无 EOD 记录返回 0</returns>
|
||||
/// <remarks>
|
||||
/// 【口径论证·勿改】为什么 DividendPending 也用本方法的全量值(非分摊、非硬0):
|
||||
/// <para>1. 字段语义直接对应:EOD PosiDividendSum 的 DisplayName="浮动端平仓盈亏·分红未实现"
|
||||
/// (EodSwapPosition.cs:186),递推式 PosiDividendSum=前日+当日新计-当日实现
|
||||
/// (SwapEodPositionService.cs:1825),即"扣过当日实现后、还挂在账上未来才结的存量"。
|
||||
/// 前端列"待结算分红收益"(SwapflowList.js:561) 字面就是同一回事 → 直接取 PosiDividendSum。</para>
|
||||
/// <para>2. 是"存量"非"流量":DividendPending 描述的是"账上还欠多少"(与本次平仓比例无关的总额),
|
||||
/// 而 DividendIn 才是"本次动作落袋多少"。两者口径本就不同,各自正确。若把 DividendPending 改成
|
||||
/// 按本次平仓比例分摊,会把"存量"误当"流量",与列名"待结算"矛盾。</para>
|
||||
/// <para>3. 历史教训:方案C 初版曾把前端 DividendPending 硬编码 0(commit e3c473ba),因测试交易
|
||||
/// PosiDividendSum 恰好=0(3/2 已全额互换)而测试通过、掩盖问题。但对 PosiDividendSum≠0 的部分
|
||||
/// 平仓交易,硬0 会落库(SwapFlowEventService.cs:588 冲账取负写入 swap_flow_event.DividendPending)
|
||||
/// 并在事件列表"待结算分红收益"列显示错误的 0 —— 这是确定的回归。故本方法返回值同时喂两栏,
|
||||
/// 前端不得再覆盖。例外:互换页 DividendPending 保持 0(互换语义=全量结清,结清后待结算归0)。</para>
|
||||
/// </remarks>
|
||||
protected virtual decimal GetPreEodDividendSum(int tradeId, long positionId, DateTime dealDate)
|
||||
{
|
||||
var lastEod = DbContext.eod_swap
|
||||
.Where(x => x.ValueDate < dealDate && x.SwapTradeId == tradeId)
|
||||
.OrderByDescending(o => o.ValueDate).FirstOrDefault();
|
||||
var preEodDate = lastEod == null ? dealDate.AddDays(-1) : lastEod.ValueDate;
|
||||
var preEod = new SwapEodPositionService(this)
|
||||
.GetPreEodPositions(tradeId, preEodDate)
|
||||
.FirstOrDefault(x => x.PositionId == positionId);
|
||||
return preEod == null ? 0m : preEod.PosiDividendSum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取固定利率
|
||||
/// </summary>
|
||||
@@ -1335,10 +1508,14 @@ namespace YLErp.Modules.SwapModule
|
||||
NormalizeNotionalValues(unwindData);
|
||||
NormalizeManualSettlementAmounts(unwindData, (int)SwapEventTypeEnum.平仓, "系统操作_平仓");
|
||||
//CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
// 前端按"占期初(original)"语义传 ClosePercent(A);后端全链路按"占剩余(remaining)"语义(B)消费。
|
||||
// 入口统一转换为 B,落库展示用的 A 由 SaveSwapDealInternal 还原。
|
||||
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||||
if (NormalizeFullCloseRequest(unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
}
|
||||
ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易
|
||||
bool cofirm = false;
|
||||
ExecuteInTransaction(() =>
|
||||
{
|
||||
@@ -1354,18 +1531,24 @@ namespace YLErp.Modules.SwapModule
|
||||
DealFloatPosition(unwindData);
|
||||
var flowList = new List<swap_flow_event>(unwindData.FlowEvents);
|
||||
var eventId = SaveSwapDeal(unwindData, (int)SwapEventTypeEnum.平仓, clientCashId, "系统操作_平仓");
|
||||
if (unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓 || unwindData.ClosePercent == 1)
|
||||
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(unwindData.CloseQty);
|
||||
var isFullClose = IsFullCloseAfterDeduction(unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
if (isFullClose)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
td.StockEqvNotional = 0;
|
||||
td.TradeAmount = 0;
|
||||
CallSaveSwapTradeClientCash(td, unwindData.ValueDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
td.HasPartialUnWind = 1;
|
||||
td.StockEqvNotional = remainingStockEqvNotional;
|
||||
td.TradeAmount = remainingTradeAmount;
|
||||
}
|
||||
td.Notional = td.TradeAmount;
|
||||
td.UnWindDate = unwindData.UnwindDate;
|
||||
td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount -= Convert.ToDouble(unwindData.CloseQty);
|
||||
SaveAllChanges();
|
||||
cofirm = true;
|
||||
});
|
||||
@@ -1860,20 +2043,65 @@ namespace YLErp.Modules.SwapModule
|
||||
throw new Exception("该笔交易状态为平仓待复核,未找到相关记录,请检查该笔交易是否有效");
|
||||
}
|
||||
swapEvent.unwindData = JsonConvert.DeserializeObject<UnwindData>(swapEvent.EventData);
|
||||
NormalizeNotionalValues(swapEvent.unwindData);
|
||||
// Stored events keep display ratio A; approval calculations consume remaining ratio B.
|
||||
swapEvent.unwindData.ClosePercent = ToRemainingClosePercent(
|
||||
swapEvent.unwindData.ClosePercent,
|
||||
swapEvent.unwindData.NotionalValue,
|
||||
swapEvent.unwindData.PosiNotionalValue);
|
||||
var flowList = FindFlowEventsByEventId(swapEvent.id);
|
||||
swapEvent.unwindData.FlowEvents = flowList;
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
if (NormalizeFullCloseRequest(swapEvent.unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(swapEvent.unwindData);
|
||||
}
|
||||
}
|
||||
if (eventType == (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
NormalizeIncomeUnwindDate(swapEvent.unwindData);
|
||||
ValidateIncomeValueDate(swapEvent.unwindData, td);
|
||||
}
|
||||
var flowList = FindFlowEventsByEventId(swapEvent.id);
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
foreach (var item in flowList.Where(x => x.PositionType > 0))
|
||||
{
|
||||
item.Quantity = swapEvent.unwindData.CloseQty;
|
||||
item.PositionQty = swapEvent.unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: swapEvent.unwindData.PositionQty - swapEvent.unwindData.CloseQty;
|
||||
}
|
||||
}
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
int clientCashId = AddClientCash(td, Convert.ToDouble(-swapEvent.unwindData.SwapRealizedPnL), action, swapEvent.unwindData.ValueDate);
|
||||
if (swapEvent.unwindData.SwapMarginAmount != 0)
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓 && swapEvent.unwindData.SwapMarginAmount != 0)
|
||||
{
|
||||
AddClientCash(td, Convert.ToDouble(swapEvent.unwindData.SwapMarginAmount), ClientCashInCashOut.系统操作_应付预付金, swapEvent.unwindData.ValueDate);
|
||||
}
|
||||
swapEvent.ClientCashId = clientCashId;
|
||||
if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
|
||||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||||
if (eventType != (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
var remainingStockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingTradeAmount = td.TradeAmount - Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||||
var isFullClose = IsFullCloseAfterDeduction(swapEvent.unwindData, remainingStockEqvNotional, remainingTradeAmount);
|
||||
if (isFullClose)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
td.StockEqvNotional = 0;
|
||||
td.TradeAmount = 0;
|
||||
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
|
||||
}
|
||||
else
|
||||
{
|
||||
td.TradeStatus = ConsTrade.确认成交;
|
||||
td.HasPartialUnWind = 1;
|
||||
td.StockEqvNotional = remainingStockEqvNotional;
|
||||
td.TradeAmount = remainingTradeAmount;
|
||||
}
|
||||
}
|
||||
else if (swapEvent.unwindData.CloseMethod == (int)CloseMethodEnum.全部平仓)
|
||||
{
|
||||
td.TradeStatus = "已平仓";
|
||||
CallSaveSwapTradeClientCash(td, swapEvent.unwindData.ValueDate);
|
||||
@@ -1883,12 +2111,6 @@ namespace YLErp.Modules.SwapModule
|
||||
td.TradeStatus = ConsTrade.确认成交;
|
||||
td.HasPartialUnWind = 1;
|
||||
}
|
||||
td.UnWindDate = swapEvent.unwindData.UnwindDate;
|
||||
if (eventType != (int)SwapEventTypeEnum.互换)
|
||||
{
|
||||
td.StockEqvNotional = Math.Round(td.StockEqvNotional - Convert.ToDouble(swapEvent.unwindData.CloseNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
td.TradeAmount -= Convert.ToDouble(swapEvent.unwindData.CloseQty);
|
||||
}
|
||||
|
||||
td.Notional = td.TradeAmount;
|
||||
UpdateInitalPosition(flowList, swapEvent.unwindData, eventType);
|
||||
@@ -1920,6 +2142,13 @@ namespace YLErp.Modules.SwapModule
|
||||
// 与 SwapUnwind(L1270) 保持一致——缺少此转换会导致 SaveSwapDealInternal 的 B→A 还原出错
|
||||
// (例如第二次部分平仓 50%(A) → 错误还原为 0.325 而非 0.50)。
|
||||
unwindData.ClosePercent = ToRemainingClosePercent(unwindData.ClosePercent, unwindData.NotionalValue, unwindData.PosiNotionalValue);
|
||||
if (eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
if (NormalizeFullCloseRequest(unwindData))
|
||||
{
|
||||
RecalculateNormalizedUnwindAmounts(unwindData);
|
||||
}
|
||||
}
|
||||
string action = eventType == (int)SwapEventTypeEnum.互换 ? ClientCashInCashOut.系统操作_互换 : ClientCashInCashOut.系统操作_平仓费;
|
||||
ExecuteInTransaction(() =>
|
||||
{
|
||||
@@ -2053,8 +2282,17 @@ namespace YLErp.Modules.SwapModule
|
||||
else
|
||||
{
|
||||
// 平仓时才扣减持仓
|
||||
position.PosiQuantity -= unwindData.CloseQty;
|
||||
position.PosiNotionalValue = Math.Round(position.PosiNotionalValue - unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
var remainingPositionQty = position.PosiQuantity - unwindData.CloseQty;
|
||||
var remainingPositionNotional = Math.Round(
|
||||
position.PosiNotionalValue - unwindData.CloseNotionalValue,
|
||||
ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.PosiQuantity = unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: remainingPositionQty;
|
||||
position.PosiNotionalValue = unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: remainingPositionNotional;
|
||||
position.PosiTradingFee -= position.PosiTradingFee * unwindData.ClosePercent;
|
||||
position.PosiTradingFeePending -= position.PosiTradingFeePending * unwindData.ClosePercent;
|
||||
}
|
||||
@@ -2068,10 +2306,13 @@ namespace YLErp.Modules.SwapModule
|
||||
position.InterestFeePending += interest.InterestFee;
|
||||
if ((interest.InterestMode == (int)InterestModeEnum.追加预付金 || interest.InterestMode == (int)InterestModeEnum.初始预付金) && eventType == (int)SwapEventTypeEnum.平仓)
|
||||
{
|
||||
position.InterestPrincipalFix = Math.Round(
|
||||
var remainingInterestPrincipal = Math.Round(
|
||||
position.InterestPrincipalFix - interest.InterestPrincipal,
|
||||
ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero);
|
||||
position.InterestPrincipalFix = unwindData.ClosePercent == 1
|
||||
? 0
|
||||
: remainingInterestPrincipal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +307,14 @@ namespace YLErp.Modules.SwapModule
|
||||
return DbContext.swap_flow_event.Where(eventExpression).ToList();
|
||||
}
|
||||
|
||||
protected virtual List<swap_flow_event> FindCompletedFlowEvents(List<int> tradeIds)
|
||||
{
|
||||
return DbContext.swap_flow_event
|
||||
.Where(x => tradeIds.Contains(x.SwapTradeId)
|
||||
&& x.DataState == (int)SwapFlowDateStateEnum.完成)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
@@ -352,6 +360,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var tradeRealPositionList = allTradePositionList.Where(t => !t.IsInitial).ToList();
|
||||
var tradeExtendList = FindTradeExtends(tradeIds);
|
||||
var eodSwapList = FindEodSwapsByDate(preSettleDate);
|
||||
var completedFlowEvents = FindCompletedFlowEvents(tradeIds);
|
||||
List<int> eventTyps = new List<int>() { (int)SwapEventTypeEnum.平仓, (int)SwapEventTypeEnum.互换, (int)SwapEventTypeEnum.自动互换 };
|
||||
foreach (var td in tradeQueryList)
|
||||
{
|
||||
@@ -364,7 +373,10 @@ namespace YLErp.Modules.SwapModule
|
||||
var realPositions = tradeRealPositionList.Where(s => s.SwapTradeId == td.id);
|
||||
var posiList = positions.Where(x => x.PosiQuantity > 0).ToList();
|
||||
var realPosiList = realPositions.ToList();
|
||||
var interestList = positions.Where(x => x.InterestDirection > 0).ToList();
|
||||
var tradeCompletedFlowEvents = completedFlowEvents.Where(x => x.SwapTradeId == td.id).ToList();
|
||||
var interestList = SwapDealService.ResolveInterestLegPositionsAsOf(
|
||||
positions.ToList(), realPosiList, tradeCompletedFlowEvents, settleDate)
|
||||
.Where(x => x.InterestDirection > 0).ToList();
|
||||
DateTime posiDate = td.TradeDate.Value;//交易日期
|
||||
var lastEodSwap = eodSwapList.FirstOrDefault(x => x.SwapTradeId == td.id);
|
||||
//上一交易日无日终归档,且不是交易日期,且当前收盘日期不是交易日期,报错
|
||||
@@ -1351,8 +1363,9 @@ namespace YLErp.Modules.SwapModule
|
||||
//持仓内容-利息腿
|
||||
newEodPayPosition.InterestDirection = position.InterestDirection;
|
||||
newEodPayPosition.InterestMode = position.InterestMode;
|
||||
// ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减。
|
||||
newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix;
|
||||
newEodPayPosition.InterestPrincipalFix *= (1 - closePercent);
|
||||
// newEodPayPosition.InterestPrincipalFix *= (1 - closePercent);
|
||||
newEodPayPosition.InterestRateDefault = position.InterestRateDefault;
|
||||
newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval;
|
||||
newEodPayPosition.IsAnnualized = position.IsAnnualized;
|
||||
@@ -1364,9 +1377,11 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.interest_rest_days = position.interest_rest_days;
|
||||
newEodPayPosition.interest_rule = position.interest_rule;
|
||||
//利息端估值用信息
|
||||
newEodPayPosition.TdInterestPrincipal = position.InterestMode == (int)InterestModeEnum.标的期初全价
|
||||
? posiNotionalValue
|
||||
: interests.Count > 0 ? interests.First().InterestPrincipal : 0;
|
||||
newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode)
|
||||
? position.InterestPrincipalFix
|
||||
: position.InterestMode == (int)InterestModeEnum.标的期初全价
|
||||
? posiNotionalValue
|
||||
: interests.Count > 0 ? interests.First().InterestPrincipal : 0;
|
||||
if (interval != null)
|
||||
{
|
||||
newEodPayPosition.TdInterestRate = interval.Rate;
|
||||
@@ -1397,8 +1412,11 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
var pendingInterestBeforeSettlement = autoSwap
|
||||
? interestAmountBeforeSettlement
|
||||
: lastInterestIncomeSum + newEodPayPosition.TdInterestIncome;
|
||||
newEodPayPosition.InterestIncomeSum = RoundEodInterest(
|
||||
interestAmountBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee;
|
||||
}
|
||||
//持仓内容-利息腿-损益统计(本方视角)
|
||||
|
||||
@@ -94,28 +94,34 @@ namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
var obj = DbContext.swap_event.Where(O => O.id == swap_Event.id).FirstOrDefault();
|
||||
var trade = DbContext.trade.Where(O => O.id == swap_Event.SwapTradeId).FirstOrDefault();
|
||||
if (trade == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易,请刷新页面后再次尝试!");
|
||||
}
|
||||
var oldMaturityDate = trade.ExerciseDate.Value;
|
||||
if (obj != null && !string.IsNullOrEmpty(obj.EventData))
|
||||
{
|
||||
oldMaturityDate = JsonConvert.DeserializeObject<ExtenstionData>(obj.EventData).OldMaturityDate;
|
||||
}
|
||||
if (swap_Event.extenstionData.NewMaturityDate <= oldMaturityDate)
|
||||
{
|
||||
throw new ServiceException("新到期日应晚于原到期日!");
|
||||
}
|
||||
if (obj != null && obj.ValueDate >= swap_Event.ValueDate)
|
||||
{
|
||||
throw new ServiceException("新展期日应晚于原展期日!");
|
||||
}
|
||||
if (swap_Event.extenstionData.NewMaturityDate< trade.StartDate)
|
||||
{
|
||||
throw new ServiceException("新到期日不应早于交易开始日期!");
|
||||
}
|
||||
swap_Event.extenstionData.OldMaturityDate = oldMaturityDate;
|
||||
string data = JsonConvert.SerializeObject(swap_Event.extenstionData);
|
||||
if (obj != null)
|
||||
{
|
||||
obj.Invalid = true;
|
||||
}
|
||||
swap_Event.EventReason = "交易展期";
|
||||
if (swap_Event.extenstionData.OldMaturityDate == swap_Event.extenstionData.NewMaturityDate)
|
||||
{
|
||||
throw new ServiceException("新到期日不应和原到期日一致!");
|
||||
}
|
||||
if (obj!=null&&obj.ValueDate >= swap_Event.ValueDate)
|
||||
{
|
||||
throw new ServiceException("新展期日应晚于原展期日!");
|
||||
}
|
||||
if (trade == null)
|
||||
{
|
||||
throw new ServiceException("未找到交易,请刷新页面后再次尝试!");
|
||||
}
|
||||
if (swap_Event.extenstionData.NewMaturityDate< trade.StartDate)
|
||||
{
|
||||
throw new ServiceException("新到期日不应早于交易开始日期!");
|
||||
}
|
||||
trade.ExerciseDate = swap_Event.extenstionData.NewMaturityDate;
|
||||
//修改互换观察日到期日
|
||||
new SwapTradeService(this).UpdateObservationDay(trade.id, swap_Event.extenstionData.OldMaturityDate, trade.ExerciseDate.Value);
|
||||
|
||||
@@ -276,10 +276,10 @@ namespace YLErp.BLL
|
||||
}
|
||||
var swapPositions = db.Set<swap_position>()
|
||||
.Where(sp => swapTradeIds.Contains(sp.SwapTradeId) && sp.IsInitial && sp.UnderlyingCode != null)
|
||||
.Select(sp => new { sp.SwapTradeId, sp.PosiNetPrice, sp.UnderlyingCode })
|
||||
.Select(sp => new { sp.SwapTradeId, sp.PosiGrossPrice, sp.UnderlyingCode })
|
||||
.ToList();
|
||||
var logger = LogFactory.GetLogger<tradeBLL>();
|
||||
var posDict = swapPositions.GroupBy(sp => sp.SwapTradeId).ToDictionary(g => g.Key, g => g.First().PosiNetPrice);
|
||||
var posDict = swapPositions.GroupBy(sp => sp.SwapTradeId).ToDictionary(g => g.Key, g => g.First().PosiGrossPrice);
|
||||
var umProvider = DataCacheProvider.GetUnderlyingDataSource();
|
||||
// 需求②:平仓/行权/互换交易,审批角色应取 CloseProcess 流程的节点角色,而非 TradeProcess
|
||||
var closeProcessRoles = db.approvalprocess
|
||||
@@ -312,14 +312,14 @@ namespace YLErp.BLL
|
||||
tradeLinq.TradeSinglePrice = option.OpenCommission;
|
||||
}
|
||||
|
||||
// --- 新增逻辑:针对收益互换类型,用 swap_position.PosiNetPrice 覆盖展示用的期初标的价格 ---
|
||||
// --- 针对收益互换类型,用 swap_position.PosiGrossPrice 覆盖展示用的期初标的价格 ---
|
||||
try
|
||||
{
|
||||
if (tradeLinq.TradeType == "收益互换" && posDict.TryGetValue(tradeLinq.id, out var netPrice) && netPrice > 0)
|
||||
if (tradeLinq.TradeType == "收益互换" && posDict.TryGetValue(tradeLinq.id, out var grossPrice) && grossPrice > 0)
|
||||
{
|
||||
// 将期初价格覆盖为互换持仓的 PosiNetPrice(仅使用 PosiDirection != 0 的期初持仓)
|
||||
logger.Info($"tradeOpeningProcessQuery.DAL override: tradeId={tradeLinq.id} beforeInitialSpot={tradeLinq.InitialSpotPrice} dbPosi={netPrice}");
|
||||
tradeLinq.InitialSpotPrice = Convert.ToDouble(netPrice);
|
||||
// 将期初价格覆盖为互换持仓的 PosiGrossPrice(仅使用 PosiDirection != 0 的期初持仓)
|
||||
logger.Info($"tradeOpeningProcessQuery.DAL override: tradeId={tradeLinq.id} beforeInitialSpot={tradeLinq.InitialSpotPrice} dbPosi={grossPrice}");
|
||||
tradeLinq.InitialSpotPrice = Convert.ToDouble(grossPrice);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,48 +1,56 @@
|
||||
// 通过 /front/swappriceprecision 加载。可按 UnderlyingInstrumentType 修改;缺项或非法值由页面精度组件回退内置默认规则。
|
||||
window.main = window.main || {};
|
||||
window.main.swapPricePrecision = {
|
||||
Stock: { integerDigits: 7, precision: 2 },
|
||||
StockIndex: { integerDigits: 7, precision: 2 },
|
||||
StockIF: { integerDigits: 7, precision: 4 },
|
||||
CommodityFutures: { integerDigits: 7, precision: 4 },
|
||||
CommoditySpot: { integerDigits: 7, precision: 4 },
|
||||
NewOtcStock: { integerDigits: 7, precision: 4 },
|
||||
HKStock: { integerDigits: 7, precision: 4 },
|
||||
HKStockIndex: { integerDigits: 7, precision: 4 },
|
||||
Fund: { integerDigits: 7, precision: 4 },
|
||||
common: {
|
||||
amount: { precision: 2, grouping: true },
|
||||
quantity: { integerDigits: 16, precision: 2, grouping: true },
|
||||
rate: { precision: 4 }
|
||||
},
|
||||
Stock: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
StockIndex: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
StockIF: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
CommodityFutures: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
CommoditySpot: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
NewOtcStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
HKStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
HKStockIndex: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
Fund: { integerDigits: 7, precision: 4, quantityPrecision: 4, quantityIntegerDigits: 12 },
|
||||
Bond: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
TBonds: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
CreditBonds: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
OtherBonds: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
TBFutures: { integerDigits: 8, precision: 4 },
|
||||
OtherFutures: { integerDigits: 8, precision: 4 },
|
||||
GoldSpot: { integerDigits: 8, precision: 4 },
|
||||
OtherSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadFutures: { integerDigits: 8, precision: 4 },
|
||||
AbroadSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadStock: { integerDigits: 8, precision: 2 },
|
||||
AbroadStockIndex: { integerDigits: 8, precision: 4 },
|
||||
ExRate: { integerDigits: 2, precision: 8 },
|
||||
Shibor: { integerDigits: 2, precision: 4 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4 },
|
||||
RateYield: {integerDigits: 6, precision: 8},
|
||||
BondIndex: {integerDigits: 6, precision: 4},
|
||||
|
||||
// TODO: 利率收益率(6+8)、债券指数(6+4)、黄金期货(6+4)待对应的 UnderlyingInstrumentType 枚举确认后启用。
|
||||
TBFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
OtherFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
GoldFutures: { quantityIntegerDigits: 12 },
|
||||
GoldSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
OtherSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadStock: { integerDigits: 8, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadStockIndex: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
ExRate: { integerDigits: 2, precision: 8, quantityPrecision: 8, quantityIntegerDigits: 16 },
|
||||
Shibor: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
RateYield: {integerDigits: 6, precision: 8, quantityPrecision: 2, quantityIntegerDigits: 12},
|
||||
BondIndex: {integerDigits: 6, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12},
|
||||
};
|
||||
|
||||
@@ -79,6 +79,10 @@ namespace YLErp.Web.Controllers
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取某债券期间付息,扣除当天已消费部分
|
||||
/// <para>【已废弃·不再用于平仓/互换预览】分红展示改由后端 SwapDealService.GetPreEodDividendSum
|
||||
/// 读 EOD PosiDividendSum 提供(方案C,单一可信源)。前端 unwindSwapTrade.js / incomeSwapTrade.js
|
||||
/// 的 getDivindIn 不再调用本接口。本接口仅保留供历史调用方,consumedDividend 查询无日期过滤的
|
||||
/// 隐患随废弃自然消解,不再单独修复。</para>
|
||||
/// </summary>
|
||||
/// <param name="startDate"></param>
|
||||
/// <param name="endDate"></param>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
{{dateFormat(extenstion.extenstionData.OldMaturityDate)}}
|
||||
</td>
|
||||
<td>
|
||||
<vue-datepicker :holiday="1" v-model="extenstion.extenstionData.NewMaturityDate" :disabled="!edit" style="width:100px;"/>
|
||||
<vue-datepicker :holiday="1" :mindate="extenstion.extenstionData.OldMaturityDate" v-model="extenstion.extenstionData.NewMaturityDate" :disabled="!edit" style="width:100px;"/>
|
||||
</td>
|
||||
<td>{{extenstion.OptName}}</td>
|
||||
<td>{{dateFormat(extenstion.OptTime,'YYYY-MM-DD HH:mm:ss')}}</td>
|
||||
|
||||
@@ -42,15 +42,15 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>成交名义本金</td>
|
||||
<td>{{deal.NotionalValue}}</td>
|
||||
<td>{{formatAmount(deal.NotionalValue)}}</td>
|
||||
<td>持仓名义本金</td>
|
||||
<td>{{deal.PosiNotionalValue}}</td>
|
||||
<td>{{formatAmount(deal.PosiNotionalValue)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>成交数量</td>
|
||||
<td> {{deal.NotionalQty}}</td>
|
||||
<td> {{formatQuantity(deal.NotionalQty)}}</td>
|
||||
<td>持仓数量</td>
|
||||
<td> {{deal.PositionQty2}}</td>
|
||||
<td> {{formatQuantity(deal.PositionQty2)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>起始日期</td>
|
||||
@@ -66,7 +66,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>平仓总额</td>
|
||||
<td> {{deal.SwapCloseAmount}}</td>
|
||||
<td> {{formatAmount(deal.SwapCloseAmount)}}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -98,7 +98,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestFee" v-bind:format="inputFormatInterestAmount" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -126,7 +126,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestFee" v-bind:format="inputFormatInterestAmount" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -177,13 +177,13 @@
|
||||
<span title="使用系统标的价格" class="glyphicon glyphicon-refresh"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{floatPosition.Quantity}}</td>
|
||||
<td>{{formatQuantity(floatPosition.Quantity)}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="floatPosition.TradingFee" v-bind:format="inputFormatInterestAmount" v-on:input="changeTradingFee"></vue-number-input>
|
||||
<div class="bubble-box" style="margin-left:6px;">我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用</div>
|
||||
</td>
|
||||
<td> <vue-number-input v-model="floatPosition.DividendIn" v-bind:format="inputFormatDividend" v-on:input="changeTradingFee"></vue-number-input></td>
|
||||
<td style="font-size:18px;">{{floatPosition.FloatPnlSum}}</td>
|
||||
<td style="font-size:18px;">{{formatAmount(floatPosition.FloatPnlSum)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
</script>
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapLongShort.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
<div class="pb-3" id="vueDiv">
|
||||
@@ -26,11 +28,11 @@
|
||||
<div id="commomArea" class="row" style="height:140px">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">成交名义本金</label>
|
||||
<input class="text-box" v-model="deal.NotionalValue" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.NotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">持仓名义本金</label>
|
||||
<input class="text-box" v-model="deal.PosiNotionalValue" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.PosiNotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">起始日期</label>
|
||||
@@ -46,7 +48,7 @@
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="formlabel">平仓总额</label>
|
||||
<input class="text-box" v-model="deal.SwapCloseAmount" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.SwapCloseAmount)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,7 +75,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestFee" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -101,7 +103,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestFee" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
</script>
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/unwindLongShort.js?v=@HtmlUtil.JsVersion"></script>
|
||||
}
|
||||
<div class="pb-3" id="vueDiv">
|
||||
@@ -27,20 +29,20 @@
|
||||
<div id="commomArea" class="row" style="height:140px">
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">成交名义本金</label>
|
||||
<input class="text-box" v-model="deal.NotionalValue" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.NotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">持仓名义本金</label>
|
||||
<input class="text-box" v-model="deal.PosiNotionalValue" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.PosiNotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">成交数量</label>
|
||||
<input class="text-box" v-model="deal.NotionalQty" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatQuantity(deal.NotionalQty)" readonly="readonly" type="text" />
|
||||
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">持仓数量</label>
|
||||
<input class="text-box" v-model="deal.PositionQty" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatQuantity(deal.PositionQty)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">起始日期</label>
|
||||
@@ -60,7 +62,7 @@
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">平仓总额</label>
|
||||
<input class="text-box" v-model="deal.SwapCloseAmount" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.SwapCloseAmount)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,7 +91,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -113,7 +115,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatEqvNotional" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -33,20 +33,20 @@
|
||||
<div id="commomArea" class="row" style="height:157px">
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">成交名义本金</label>
|
||||
<input class="text-box" v-model="deal.NotionalValue" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.NotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">持仓名义本金</label>
|
||||
<input class="text-box" v-model="deal.PosiNotionalValue" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.PosiNotionalValue)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">成交数量</label>
|
||||
<input class="text-box" v-model="deal.NotionalQty" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatQuantity(deal.NotionalQty)" readonly="readonly" type="text" />
|
||||
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">持仓数量</label>
|
||||
<input class="text-box" v-model="deal.PositionQty2" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatQuantity(deal.PositionQty2)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">平仓方式</label>
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
<div class="form-group col-md-3" v-show="deal.CloseType==1">
|
||||
<label class="formlabel">平仓数量</label>
|
||||
<vue-number-input v-model="deal.CloseQty" v-on:input="changeCloseQty" v-bind:format="inputFormatTradeAmount"></vue-number-input>
|
||||
<vue-number-input v-model="deal.CloseQty" v-on:input="changeCloseQty" v-bind:format="getQuantityInputFormat()"></vue-number-input>
|
||||
</div>
|
||||
<div class="form-group col-md-3" v-show="deal.CloseType==2">
|
||||
<label class="formlabel">平仓比例</label>
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="formlabel">平仓总额</label>
|
||||
<input class="text-box" v-model="deal.SwapCloseAmount" readonly="readonly" type="text" />
|
||||
<input class="text-box" :value="formatAmount(deal.SwapCloseAmount)" readonly="readonly" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,7 +125,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatCloseAmount" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -165,7 +165,7 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.InterestAmount" v-bind:format="inputFormatCloseAmount" v-on:input="changeInterestAmount(item)"></vue-number-input>
|
||||
</td>
|
||||
<td>{{item.InterestClosePnL}}</td>
|
||||
<td>{{formatAmount(item.InterestClosePnL)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -214,7 +214,7 @@
|
||||
<span title="使用系统标的价格" class="glyphicon glyphicon-refresh"></span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{deal.CloseQty}}</td>
|
||||
<td>{{formatQuantity(deal.CloseQty)}}</td>
|
||||
<td>
|
||||
<vue-number-input v-model="floatPosition.TradingFee" v-bind:format="inputFormatCloseAmount" v-on:input="changeTradingFee"></vue-number-input>
|
||||
<div class="bubble-box">我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用</div>
|
||||
@@ -222,8 +222,8 @@
|
||||
<td>
|
||||
<vue-number-input v-model="floatPosition.TradingFeePending" v-bind:format="inputFormatEqvNotional" disabled></vue-number-input>
|
||||
</td>
|
||||
<td>{{floatPosition.DividendIn}}</td>
|
||||
<td style="font-size:18px;">{{floatPosition.FloatPnlSum}}</td>
|
||||
<td>{{formatAmount(floatPosition.DividendIn)}}</td>
|
||||
<td style="font-size:18px;">{{formatAmount(floatPosition.FloatPnlSum)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
<script src="@HtmlUtil.BasicDataJs("客户")"></script>
|
||||
<script src="~/Scripts/fast/fastVue.components.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/tradeHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/front/swappriceprecision?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/swaptrade/swapPricePrecisionHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/trade/tradeUpload.js?v=2"></script>
|
||||
<script src="~/Scripts/app/swaptrade/step.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/SwapflowList.js?v=@HtmlUtil.JsVersion"></script>
|
||||
@@ -231,15 +233,15 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="formlabel ">成交数量</label>
|
||||
<vue-number-input v-model="swapflow.TradingQty" v-bind:format="inputFormatTradeAmount"></vue-number-input>
|
||||
<vue-number-input v-model="swapflow.TradingQty" v-bind:format="getQuantityInputFormat()"></vue-number-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="formlabel ">成交金额</label>
|
||||
<vue-number-input v-model="swapflow.TradingAmount" v-bind:format="inputFormatTradePrice"></vue-number-input>
|
||||
<vue-number-input v-model="swapflow.TradingAmount" v-bind:format="inputFormatSwapAmount"></vue-number-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="formlabel ">交易费用</label>
|
||||
<vue-number-input v-model="swapflow.TradingFee" v-bind:format="inputFormatTradePrice"></vue-number-input>
|
||||
<vue-number-input v-model="swapflow.TradingFee" v-bind:format="inputFormatSwapAmount"></vue-number-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="formlabel ">成交均价</label>
|
||||
|
||||
@@ -493,7 +493,7 @@
|
||||
<vue-swap-price-input :key="getPosiPriceFormatKey(item,'normalPosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="getPosiPriceInputFormat(item,'grossPrice')" v-on:input="changeSpotPrice(item)"></vue-swap-price-input>
|
||||
</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.PosiQuantity" v-on:input="changeQuantity(item)" v-bind:format="inputFormatPositionQuantityFixed2"></vue-number-input>{{item.underlying!=null?item.underlying.QuoteUnitString:''}}
|
||||
<vue-number-input v-model="item.PosiQuantity" v-on:input="changeQuantity(item)" v-bind:format="getQuantityInputFormat(item)"></vue-number-input>{{item.underlying!=null?item.underlying.QuoteUnitString:''}}
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="posiFeeModePercent">
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
var sr = trade.trade_extend.ExtendObj.SettlementRules;
|
||||
bool hideFloatingIncomeDirection = PS.Config.ErpElement.SwapFloatingIncomeReceiveOnlyMode;
|
||||
string SwapPriceData(decimal? value) => value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
string SwapCommonData(object value) => value == null ? string.Empty : Convert.ToString(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
@section CSS{
|
||||
<link href="~/Style/Css/swapTradeView.css?@HtmlUtil.JsVersion" rel="stylesheet" />
|
||||
@@ -134,7 +135,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>名义本金</td>
|
||||
<td class="color-bule">@trade.OriginalStockEqvNotional.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(trade.OriginalStockEqvNotional)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
@*<tr>
|
||||
<td>初始预付金</td>
|
||||
@@ -277,9 +278,9 @@
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@((InterestModeEnum)item.InterestMode)</td>
|
||||
<td>@item.HappenDate.OtcFormatDate()</td>
|
||||
<td>@item.InterestPrincipalFix.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestPrincipalFix)" data-kind="amount"></span></td>
|
||||
<td>@item.Currency</td>
|
||||
<td>@item.InterestRateDefault.OtcFormatPercent(4)</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestRateDefault)" data-kind="rate"></span></td>
|
||||
<td>@(item.IsAnnualized ? "是" : "否")</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" onclick="showSwapRate('@(item.InterestSwapInterval)', true)" style="height:22px;">查看</button>
|
||||
@@ -342,11 +343,11 @@
|
||||
<tr class="color-bule">
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@((InterestModeEnum)item.InterestMode)</td>
|
||||
<td>@interestPrice?.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(interestPrice)" data-kind="amount"></span></td>
|
||||
<td>
|
||||
@(string.IsNullOrEmpty(item.FloatRateUnderlyingCode) ? "无" : item.FloatRateUnderlyingCode)
|
||||
@sign
|
||||
@item.InterestRateDefault.OtcFormatPercent(4)
|
||||
<span class="js-swap-common" data-value="@SwapCommonData(item.InterestRateDefault)" data-kind="rate"></span>
|
||||
</td>
|
||||
<td>@(item.IsAnnualized ? "是" : "否")</td>
|
||||
<td>
|
||||
@@ -422,7 +423,7 @@
|
||||
</td>
|
||||
}
|
||||
<td>
|
||||
@item.PosiQuantity.OtcFormat(OtcFormatFlag.StockEqvNotional)
|
||||
<span class="js-swap-common" data-value="@SwapCommonData(item.PosiQuantity)" data-kind="quantity" data-instrument-type="@item.UnderlyingInstrumentType"></span>
|
||||
</td>
|
||||
<td>
|
||||
@if (item.PosiFeeType == 0)
|
||||
@@ -435,7 +436,7 @@
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@item.PosiTradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional)
|
||||
<span class="js-swap-common" data-value="@SwapCommonData(item.PosiTradingFeePending)" data-kind="amount"></span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -507,9 +508,9 @@
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@((InterestModeEnum)item.InterestMode)</td>
|
||||
<td>@item.HappenDate.OtcFormatDate()</td>
|
||||
<td>@item.InterestPrincipalFix.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestPrincipalFix)" data-kind="amount"></span></td>
|
||||
<td>@item.Currency</td>
|
||||
<td>@item.InterestRateDefault.OtcFormatPercent(4)</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestRateDefault)" data-kind="rate"></span></td>
|
||||
<td>@(item.IsAnnualized ? "是" : "否")</td>
|
||||
<td><button class="btn btn-sm btn-outline-danger" type="button" onclick="showSwapRate('@(item.InterestSwapInterval)')" style="height:22px;">查看</button></td>
|
||||
</tr>
|
||||
@@ -567,11 +568,11 @@
|
||||
<tr class="color-bule">
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@((InterestModeEnum)item.InterestMode)</td>
|
||||
<td>@interestPrice?.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(interestPrice)" data-kind="amount"></span></td>
|
||||
<td>
|
||||
@(string.IsNullOrEmpty(item.FloatRateUnderlyingCode) ? "无" : item.FloatRateUnderlyingCode)
|
||||
@sign
|
||||
@item.InterestRateDefault.OtcFormatPercent(4)
|
||||
<span class="js-swap-common" data-value="@SwapCommonData(item.InterestRateDefault)" data-kind="rate"></span>
|
||||
</td>
|
||||
<td>@(item.IsAnnualized ? "是" : "否")</td>
|
||||
<td>@(item.InterestType == 0 ? "单利" : "复利")</td>
|
||||
@@ -645,10 +646,10 @@
|
||||
</td>
|
||||
}
|
||||
<td>
|
||||
@item.PosiQuantity.OtcFormat(OtcFormatFlag.StockEqvNotional)
|
||||
<span class="js-swap-common" data-value="@SwapCommonData(item.PosiQuantity)" data-kind="quantity" data-instrument-type="@item.UnderlyingInstrumentType"></span>
|
||||
</td>
|
||||
<td>@item.PosiNotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td>@item.PosiTradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional)</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.PosiNotionalValue)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.PosiTradingFeePending)" data-kind="amount"></span></td>
|
||||
<td>@item.PosiStartDate.OtcFormatDate()</td>
|
||||
<td>@item.PosiMatuirityDate.OtcFormatDate()</td>
|
||||
</tr>
|
||||
@@ -709,14 +710,14 @@
|
||||
@if (tc.CloseType == 1)
|
||||
{
|
||||
<td class="tdRight">平仓数量</td>
|
||||
<td class="color-bule">@((PS.Config.IsUseDisplayNotional ? tc.CloseQty * (trade.CountRatio ?? 1) : tc.CloseQty).OtcFormatNotional())</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(PS.Config.IsUseDisplayNotional ? tc.CloseQty * (trade.CountRatio ?? 1) : tc.CloseQty)" data-kind="quantity" data-instrument-type="@trade.UnderlyingInstrumentType"></span></td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="tdRight">平仓比例</td>
|
||||
<td class="color-bule">@(tc.ClosePercent.OtcFormatPercent(4))</td>
|
||||
<td class="tdRight">平仓名义本金</td>
|
||||
<td class="color-bule">@(tc.CloseNotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(tc.CloseNotionalValue)" data-kind="amount"></span></td>
|
||||
}
|
||||
|
||||
</tr>
|
||||
@@ -734,9 +735,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tdRight">实现盈亏</td>
|
||||
<td class="color-bule">@(tc.SwapRealizedPnL.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(tc.SwapRealizedPnL)" data-kind="amount"></span></td>
|
||||
<td class="tdRight">平仓总额</td>
|
||||
<td class="color-bule">@(tc.SwapCloseAmount.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(tc.SwapCloseAmount)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -762,9 +763,9 @@
|
||||
<tr class="color-bule">
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@(item.InterestModeStr)</td>
|
||||
<td>@(item.InterestPrincipal.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(item.InterestAmount.OtcFormatMoney())</td>
|
||||
<td>@(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestPrincipal)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestAmount)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestClosePnL)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -798,9 +799,9 @@
|
||||
<td>@(item.InterestStartDate.OtcFormatDate())</td>
|
||||
<td>@(item.InterestEndDate.OtcFormatDate())</td>
|
||||
<td>@(item.Rate.OtcFormatPercent())</td>*@
|
||||
<td>@(item.InterestFee.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(item.InterestAmount.OtcFormatMoney())</td>
|
||||
<td>@(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestFee)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestAmount)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestClosePnL)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -855,11 +856,11 @@
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.PosiGrossPrice)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.TradingAmountAvg)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
}
|
||||
<td>@(closeFloat.Quantity.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.TradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.DividendIn.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td style="font-size:18px;">@(closeFloat.FloatPnlSum.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.Quantity)" data-kind="quantity" data-instrument-type="@closeFloat.UnderlyingInstrumentType"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.TradingFee)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.TradingFeePending)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.DividendIn)" data-kind="amount"></span></td>
|
||||
<td style="font-size:18px;"><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.FloatPnlSum)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -893,9 +894,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tdRight">平仓总额</td>
|
||||
<td class="color-bule">@(eod.NotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(eod.NotionalValue)" data-kind="amount"></span></td>
|
||||
<td class="tdRight">实现盈亏</td>
|
||||
<td class="color-bule">@(eod.TdRealizedPnL.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(eod.TdRealizedPnL)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
@@ -918,7 +919,7 @@
|
||||
<td class="tdRight">互换序号</td>
|
||||
<td class="color-bule">@(index++)</td>
|
||||
<td class="tdRight">互换名义本金</td>
|
||||
<td class="color-bule">@(tc.PosiNotionalValue.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(tc.PosiNotionalValue)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tdRight">起始日期</td>
|
||||
@@ -934,9 +935,9 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tdRight">平仓总额</td>
|
||||
<td class="color-bule">@(tc.SwapCloseAmount.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(tc.SwapCloseAmount)" data-kind="amount"></span></td>
|
||||
<td class="tdRight">实现盈亏</td>
|
||||
<td class="color-bule">@(tc.SwapRealizedPnL.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td class="color-bule"><span class="js-swap-common" data-value="@SwapCommonData(tc.SwapRealizedPnL)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -962,9 +963,9 @@
|
||||
var bgclass = item.InterestDirection == (int)SwapDirectionEnum.收取 ? "swapget" : "swappay";
|
||||
<tr class="color-bule">
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@(item.InterestFee.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(item.InterestAmount.OtcFormatMoney())</td>
|
||||
<td>@(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestFee)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestAmount)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestClosePnL)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -994,9 +995,9 @@
|
||||
var bgclass = item.InterestDirection == (int)SwapDirectionEnum.收取 ? "swapget" : "swappay";
|
||||
<tr class="color-bule">
|
||||
<td class="@bgclass" style="width:116px !important;">@((SwapDirectionEnum)item.InterestDirection)</td>
|
||||
<td>@(item.InterestFee.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(item.InterestAmount.OtcFormatMoney())</td>
|
||||
<td>@(item.InterestClosePnL.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestFee)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestAmount)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(item.InterestClosePnL)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -1057,10 +1058,10 @@
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.TradingAmountAvg * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
}
|
||||
<td><span class="js-swap-price" data-value="@SwapPriceData(closeFloat.TradingAmountAvg * multiplier)" data-instrument-type="@closeFloat.UnderlyingInstrumentType" data-field="grossPrice"></span></td>
|
||||
<td>@((closeFloat.PositionQty??0).OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.TradingFee.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.DividendIn.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td>@(closeFloat.FloatPnlSum.OtcFormat(OtcFormatFlag.StockEqvNotional))</td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.PositionQty ?? 0)" data-kind="quantity" data-instrument-type="@closeFloat.UnderlyingInstrumentType"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.TradingFee)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.DividendIn)" data-kind="amount"></span></td>
|
||||
<td><span class="js-swap-common" data-value="@SwapCommonData(closeFloat.FloatPnlSum)" data-kind="amount"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* _proof_short_income.js — 红测试原型(纯 Node 可跑,无需 jest 依赖)
|
||||
* ============================================================================
|
||||
* 目的:证明"income 页 MarkClosePnl 缺 longRatio"是一个真实存在、但当前测试未覆盖的不一致。
|
||||
*
|
||||
* 公式逐字抄录自团队金标准 YLErpDAL/Helpers/FrontendCalcReference.cs:
|
||||
* CalcUnwind (unwindSwapTrade.js) : 含 longRatio
|
||||
* CalcIncome (incomeSwapTrade.js) : 无 longRatio ← 差异点
|
||||
*
|
||||
* 跑法:node YLErpWeb/fe-tests/_proof_short_income.js
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// ---- unwind 页公式(对应 FrontendCalcReference.CalcUnwind:42-47,含 longRatio)----
|
||||
function calcUnwindMarkClosePnl(input) {
|
||||
const scale = input.multiplier === 100 ? 0.01 : 1;
|
||||
const floatRatio = input.payDirection === 1 ? 1 : -1;
|
||||
const longRatio = input.positionType === 1 ? 1 : -1;
|
||||
const v = input.closeQty * (input.tradingAmountAvg * scale - input.posiGrossPrice) * floatRatio * longRatio * 10000;
|
||||
const rounded = Math.round(v) / 10000;
|
||||
return Math.round(rounded * 100) / 100; // toFixed(2)
|
||||
}
|
||||
|
||||
// ---- income 页公式(对应 FrontendCalcReference.CalcIncome:105-108,无 longRatio)----
|
||||
function calcIncomeMarkClosePnl(input) {
|
||||
const scale = input.multiplier === 100 ? 0.01 : 1;
|
||||
const floatRatio = input.payDirection === 1 ? 1 : -1;
|
||||
// 注意:此处按照当前生产代码,没有乘以 longRatio
|
||||
const v = input.positionQty * input.contractSize * (input.tradingAmountAvg * scale - input.posiGrossPrice) * floatRatio;
|
||||
return Math.round(v * 100) / 100;
|
||||
}
|
||||
|
||||
// ---- 修复后 income 公式(补上 longRatio,与 unwind / 后端一致)----
|
||||
function calcIncomeMarkClosePnlFixed(input) {
|
||||
const scale = input.multiplier === 100 ? 0.01 : 1;
|
||||
const floatRatio = input.payDirection === 1 ? 1 : -1;
|
||||
const longRatio = input.positionType === 1 ? 1 : -1;
|
||||
const v = input.positionQty * input.contractSize * (input.tradingAmountAvg * scale - input.posiGrossPrice) * floatRatio * longRatio;
|
||||
return Math.round(v * 100) / 100;
|
||||
}
|
||||
|
||||
// 同一笔债券 TRS:期初全价 1.02,期末(互换/平仓价)105(×100形态→1.05),价差 0.03
|
||||
const base = { multiplier: 100, posiGrossPrice: 1.02, tradingAmountAvg: 105, contractSize: 1 };
|
||||
const longCase = { ...base, closeQty: 10000, positionQty: 10000, payDirection: 1, positionType: 1 };
|
||||
const shortCase = { ...base, closeQty: 10000, positionQty: 10000, payDirection: 1, positionType: 2 };
|
||||
|
||||
function check(name, cond) {
|
||||
console.log(` [${cond ? 'PASS' : 'FAIL'}] ${name}`);
|
||||
return cond;
|
||||
}
|
||||
|
||||
console.log('=== 场景A:多头(PositionType=1)—— 两页理应一致 ===');
|
||||
const aU = calcUnwindMarkClosePnl(longCase);
|
||||
const aI = calcIncomeMarkClosePnl(longCase);
|
||||
console.log(` unwind=${aU} income=${aI}`);
|
||||
let allPass = true;
|
||||
allPass &= check('多头:unwind == income', aU === aI);
|
||||
|
||||
console.log('=== 场景B:空头(PositionType=2)—— 当前代码两页符号相反(红)===');
|
||||
const bU = calcUnwindMarkClosePnl(shortCase);
|
||||
const bI = calcIncomeMarkClosePnl(shortCase);
|
||||
console.log(` unwind=${bU} income=${bI} (空头价格涨应亏损,unwind 正确为负,income 错为正)`);
|
||||
allPass &= check('空头:unwind == income (当前代码会 FAIL → 证明 bug 存在)', bU === bI);
|
||||
|
||||
console.log('=== 场景C:空头 + 修复后 income(补 longRatio)—— 应一致(绿)===');
|
||||
const bIf = calcIncomeMarkClosePnlFixed(shortCase);
|
||||
console.log(` unwind=${bU} incomeFixed=${bIf}`);
|
||||
allPass &= check('空头:unwind == incomeFixed (修复后 PASS → 证明改动可修复)', bU === bIf);
|
||||
|
||||
console.log('');
|
||||
if (allPass) {
|
||||
console.log('✅ 全部通过(若场景B也PASS,说明已修复或无空头场景)');
|
||||
} else {
|
||||
console.log('❌ 场景B 失败 = 当前代码在「空头+income」下两页算出相反符号 → 真实不一致,且现有 FC_001~009 全为多头未覆盖。');
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* markClosePnlShortConsistency.test.js — 空头场景下 unwind/income 两页 MarkClosePnl 一致性(红测试)
|
||||
* ============================================================================
|
||||
* 状态:当前为 RED(证明 income 页 MarkClosePnl 缺 longRatio 的真实不一致)。
|
||||
* 修复 incomeSwapTrade.js:207 与 YLErpDAL/Helpers/FrontendCalcReference.CalcIncome:107
|
||||
* 补上 longRatio 后,本文件应全部转 GREEN。
|
||||
*
|
||||
* 背景:
|
||||
* - 团队金标准 FrontendCalcReference 明确记录两页差异:unwind 含 longRatio,
|
||||
* income 无 longRatio(CalcIncome:107,注释"无 longRatio")。
|
||||
* - 现有特征化测试 FrontendCalcCharacterizationTest FC_001~009 的 income 场景
|
||||
* (FC_006~009)全部 PositionType=1(多头),唯一空头场景 FC_005 是 unwind,
|
||||
* 因此 income 的空头分支从未被覆盖 → bug 长期未被发现。
|
||||
* - 后端 ValidateFrontendPnL 用同一 CalcIncome 重算比对,公式同源故永远自洽,抓不到。
|
||||
*
|
||||
* 公式逐字抄录(来源见注释行号),与生产代码一致;不改动任何生产文件。
|
||||
*/
|
||||
const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js');
|
||||
|
||||
// unwind 页 MarkClosePnl(unwindSwapTrade.js:313,含 longRatio)
|
||||
function PROD_unwindMarkClosePnl({ closeQty, deliveryPrice, initPosiNetPrice, payDirection, positionType }) {
|
||||
const floatRatio = payDirection === 1 ? 1 : -1;
|
||||
const longRatio = positionType === 1 ? 1 : -1;
|
||||
let v = Math.round(closeQty * (deliveryPrice - initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000;
|
||||
return Number(v.toFixed(2));
|
||||
}
|
||||
|
||||
// income 页 MarkClosePnl(incomeSwapTrade.js:207,无 longRatio)
|
||||
function PROD_incomeMarkClosePnl({ positionAmount, deliveryPrice, initPosiGrossPrice, payDirection }) {
|
||||
const floatRatio = payDirection === 1 ? 1 : -1;
|
||||
let v = positionAmount * (deliveryPrice - initPosiGrossPrice) * floatRatio;
|
||||
return Number(v.toFixed(2));
|
||||
}
|
||||
|
||||
// 金标准(swapCalc.calcMarkClosePnl,对齐 C# FrontendCalcReference.CalcUnwind,含 longRatio)
|
||||
function GOLD({ closeQty, tradingAmountAvg, scale, entryPrice, payDirection, positionType }) {
|
||||
const floatRatio = payDirection === 1 ? 1 : -1;
|
||||
const longRatio = positionType === 1 ? 1 : -1;
|
||||
return SwapCalc.calcMarkClosePnl(closeQty, tradingAmountAvg, scale, entryPrice, floatRatio, longRatio);
|
||||
}
|
||||
|
||||
// 同一笔债券 TRS:期初全价 1.02,期末 105(×100形态→1.05),价差 0.03;数量 10000
|
||||
const CASE = {
|
||||
closeQty: 10000, positionAmount: 10000,
|
||||
deliveryPrice: 105, initPosiNetPrice: 1.02, initPosiGrossPrice: 1.02,
|
||||
tradingAmountAvg: 105, scale: 0.01, entryPrice: 1.02,
|
||||
payDirection: 1,
|
||||
};
|
||||
|
||||
describe('MarkClosePnl 两页一致性(多头,应一致)', () => {
|
||||
test('多头:unwind == income == gold', () => {
|
||||
const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 1 });
|
||||
const i = PROD_incomeMarkClosePnl({ ...CASE, });
|
||||
const g = GOLD({ ...CASE, positionType: 1 });
|
||||
expect(u).toBe(i);
|
||||
expect(i).toBe(g);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarkClosePnl 两页一致性(空头,当前 RED)', () => {
|
||||
test('空头:unwind == income(当前 FAIL → 证明 income 缺 longRatio 的 bug)', () => {
|
||||
const u = PROD_unwindMarkClosePnl({ ...CASE, positionType: 2 });
|
||||
const i = PROD_incomeMarkClosePnl({ ...CASE, });
|
||||
// 空头价格涨应亏损:unwind = -300,income 当前 = +300(符号反了)
|
||||
expect(u).toBe(i);
|
||||
});
|
||||
|
||||
test('空头:income 应等于 gold(补 longRatio 后才会 PASS)', () => {
|
||||
const i = PROD_incomeMarkClosePnl({ ...CASE, });
|
||||
const g = GOLD({ ...CASE, positionType: 2 });
|
||||
expect(i).toBe(g);
|
||||
});
|
||||
});
|
||||
@@ -182,6 +182,19 @@ describe('多次部分平仓:全部↔部分切换 CloseQty 不跳变(占期
|
||||
expectClose(closeQty, 25000000, '应=25000000 不受 JS 浮点偏差影响');
|
||||
expect(closeQty).not.toBe(24999999.999999996);
|
||||
});
|
||||
|
||||
test('trade2308: full close uses the remaining quantity after fixed6 percent formatting', () => {
|
||||
const notionalValue = 9812312.31;
|
||||
const posiNotionalValue = 4906156.15;
|
||||
const oriClosePercent = posiNotionalValue / notionalValue;
|
||||
const closePercent = 0.5;
|
||||
const positionQty = 5000000;
|
||||
|
||||
const closeQty = SwapCalc.calcCloseQtyByOriginalPercent(
|
||||
closePercent, oriClosePercent, positionQty);
|
||||
|
||||
expect(closeQty).toBe(positionQty);
|
||||
});
|
||||
});
|
||||
|
||||
describe('交叉校验:对齐 C# FrontendCalcCharacterizationTest 金标准', () => {
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const read = (relativePath) => fs.readFileSync(path.join(ROOT, relativePath), 'utf8');
|
||||
|
||||
const precisionConfigSrc = read('App_Data/Config/swappriceprecision.js');
|
||||
const precisionHelperSrc = read('wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js');
|
||||
|
||||
function loadConfiguredPrecision() {
|
||||
const context = { window: {} };
|
||||
vm.runInNewContext(precisionConfigSrc, context);
|
||||
return context.window.main.swapPricePrecision;
|
||||
}
|
||||
|
||||
function loadHelper(config) {
|
||||
const context = { window: { main: { swapPricePrecision: config } } };
|
||||
vm.runInNewContext(precisionHelperSrc, context);
|
||||
return context.swapPricePrecision;
|
||||
}
|
||||
|
||||
function activeViewSource(source) {
|
||||
return source.replace(/@\*[\s\S]*?\*@/g, '');
|
||||
}
|
||||
|
||||
describe('swap price precision common wiring', () => {
|
||||
test('configured and fallback common precision expose 2/2/4', () => {
|
||||
const configured = loadConfiguredPrecision();
|
||||
expect(configured.common).toEqual({
|
||||
amount: { precision: 2, grouping: true },
|
||||
quantity: { integerDigits: 16, precision: 2, grouping: true },
|
||||
rate: { precision: 4 }
|
||||
});
|
||||
|
||||
const helper = loadHelper(configured);
|
||||
expect(helper.getCommonPrecision('amount')).toBe(2);
|
||||
expect(helper.getCommonPrecision('quantity')).toBe(2);
|
||||
expect(helper.getCommonPrecision('rate')).toBe(4);
|
||||
|
||||
const expectedQuantityPrecisions = {
|
||||
Stock: 2,
|
||||
StockIndex: 2,
|
||||
StockIF: 2,
|
||||
CommodityFutures: 2,
|
||||
CommoditySpot: 2,
|
||||
NewOtcStock: 2,
|
||||
HKStock: 2,
|
||||
HKStockIndex: 2,
|
||||
Fund: 4,
|
||||
Bond: 0,
|
||||
TBonds: 0,
|
||||
CreditBonds: 0,
|
||||
OtherBonds: 0,
|
||||
TBFutures: 2,
|
||||
OtherFutures: 2,
|
||||
GoldSpot: 2,
|
||||
OtherSpot: 2,
|
||||
AbroadFutures: 2,
|
||||
AbroadSpot: 2,
|
||||
AbroadStock: 2,
|
||||
AbroadStockIndex: 2,
|
||||
ExRate: 8,
|
||||
Shibor: 2,
|
||||
FixingRepoRate: 2,
|
||||
RateYield: 2,
|
||||
BondIndex: 2
|
||||
};
|
||||
Object.entries(expectedQuantityPrecisions).forEach(([instrumentType, precision]) => {
|
||||
expect(configured[instrumentType].quantityPrecision).toBe(precision);
|
||||
expect(helper.getCommonPrecision('quantity', instrumentType)).toBe(precision);
|
||||
expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).precision).toBe(precision);
|
||||
});
|
||||
const expectedQuantityIntegerDigits = {
|
||||
Stock: 12, StockIndex: 12, StockIF: 12, CommodityFutures: 12, CommoditySpot: 12,
|
||||
NewOtcStock: 12, HKStock: 12, HKStockIndex: 12, Fund: 12, TBFutures: 12,
|
||||
OtherFutures: 12, GoldFutures: 12, GoldSpot: 12, OtherSpot: 12, AbroadFutures: 12,
|
||||
AbroadSpot: 12, AbroadStock: 12, AbroadStockIndex: 12, Shibor: 12,
|
||||
FixingRepoRate: 12, RateYield: 12, BondIndex: 12,
|
||||
Bond: 16, TBonds: 16, CreditBonds: 16, OtherBonds: 16, ExRate: 16
|
||||
};
|
||||
Object.entries(expectedQuantityIntegerDigits).forEach(([instrumentType, integerDigits]) => {
|
||||
expect(configured[instrumentType].quantityIntegerDigits).toBe(integerDigits);
|
||||
expect(helper.getCommonInputFormat('quantity', { append: '' }, instrumentType).integerDigits).toBe(integerDigits);
|
||||
});
|
||||
expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(2);
|
||||
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Fund').precision).toBe(4);
|
||||
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Bond').precision).toBe(0);
|
||||
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'OtherRate').integerDigits).toBe(16);
|
||||
|
||||
const input = helper.getCommonInputFormat('amount', { append: '', trimTailZeros: true });
|
||||
expect(input).toEqual(expect.objectContaining({ precision: 2, grouping: true, trimTailZeros: false }));
|
||||
expect(helper.getCommonInputFormat('rate', { append: '%' })).toEqual(
|
||||
expect.objectContaining({ precision: 4, grouping: false }));
|
||||
});
|
||||
|
||||
test('invalid common precision falls back and formatted values keep trailing zeros', () => {
|
||||
const helper = loadHelper({
|
||||
common: {
|
||||
amount: { precision: -1 },
|
||||
quantity: { precision: 14 },
|
||||
rate: { precision: 'invalid' }
|
||||
}
|
||||
});
|
||||
expect(helper.getCommonPrecision('amount')).toBe(2);
|
||||
expect(helper.getCommonPrecision('quantity')).toBe(2);
|
||||
expect(helper.getCommonPrecision('rate')).toBe(4);
|
||||
|
||||
const configured = loadHelper({
|
||||
common: {
|
||||
amount: { precision: 3 },
|
||||
quantity: { precision: 1 },
|
||||
rate: { precision: 2 }
|
||||
}
|
||||
});
|
||||
expect(configured.formatCommon('amount', '12.5')).toBe('12.500');
|
||||
expect(configured.formatCommon('quantity', 1)).toBe('1.0');
|
||||
expect(configured.formatCommon('rate', '0.0123')).toBe('1.23%');
|
||||
expect(configured.formatCommon('amount', '1234.5')).toBe('1,234.500');
|
||||
expect(configured.normalizeCommon('amount', '1234.5')).toBe('1234.500');
|
||||
expect(configured.formatCommon('amount', NaN)).toBe('');
|
||||
expect(configured.formatCommon('amount', Infinity)).toBe('');
|
||||
});
|
||||
|
||||
test('common grouping can be disabled independently from precision', () => {
|
||||
const helper = loadHelper({
|
||||
common: {
|
||||
amount: { precision: 2, grouping: false },
|
||||
quantity: { precision: 2, grouping: false },
|
||||
rate: { precision: 4, grouping: false }
|
||||
}
|
||||
});
|
||||
expect(helper.formatCommon('amount', '1234.5')).toBe('1234.50');
|
||||
expect(helper.getCommonInputFormat('quantity', { append: '' }).grouping).toBe(false);
|
||||
});
|
||||
|
||||
test('missing or invalid asset quantity precision falls back to common quantity precision', () => {
|
||||
const helper = loadHelper({
|
||||
common: { quantity: { precision: 3, grouping: true } },
|
||||
Stock: { quantityPrecision: 'invalid' },
|
||||
Bond: {}
|
||||
});
|
||||
expect(helper.getCommonPrecision('quantity', 'Stock')).toBe(3);
|
||||
expect(helper.getCommonPrecision('quantity', 'Bond')).toBe(3);
|
||||
expect(helper.getCommonPrecision('quantity', 'OtherRate')).toBe(3);
|
||||
expect(helper.getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(16);
|
||||
expect(loadHelper({
|
||||
common: { quantity: { integerDigits: 0, precision: 3 } },
|
||||
Stock: { quantityIntegerDigits: 'invalid' }
|
||||
}).getCommonInputFormat('quantity', { append: '' }, 'Stock').integerDigits).toBe(16);
|
||||
});
|
||||
|
||||
test('SwapTrade2 common fields use the price helper without changing price responsibilities', () => {
|
||||
const scripts = {
|
||||
edit: read('wwwroot/Scripts/app/swaptrade/swapTradeEdit.js'),
|
||||
income: read('wwwroot/Scripts/app/swaptrade/incomeSwapTrade.js'),
|
||||
unwind: read('wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js'),
|
||||
longShort: read('wwwroot/Scripts/app/swaptrade/swapLongShort.js'),
|
||||
unwindLongShort: read('wwwroot/Scripts/app/swaptrade/unwindLongShort.js'),
|
||||
view: read('wwwroot/Scripts/app/swaptrade/swapTradeView.js'),
|
||||
flow: read('wwwroot/Scripts/app/swaptrade/SwapflowList.js'),
|
||||
helper: precisionHelperSrc
|
||||
};
|
||||
const view = read('Views/SwapTrade2/TradeView.cshtml');
|
||||
const activeView = activeViewSource(view);
|
||||
|
||||
for (const script of Object.values(scripts)) {
|
||||
expect(script).not.toMatch(/otcformat\.trading\.swap(?:Amount|Quantity|RateP)/);
|
||||
}
|
||||
expect(scripts.edit).toMatch(/getCommonInputFormat\('amount'/);
|
||||
expect(scripts.edit).toMatch(/getCommonInputFormat\(\s*['"]quantity['"]/);
|
||||
expect(scripts.edit).toMatch(/getCommonInputFormat\('rate'/);
|
||||
expect(scripts.view).toContain("swapPricePrecision.formatCommon('rate', item.Rate)");
|
||||
expect(scripts.flow).toMatch(/name:\s*'ytm'[\s\S]*formatter:\s*otcformat\.trading\.marginRateP/);
|
||||
expect(scripts.flow).not.toMatch(/name:\s*'ytm'[\s\S]*formatCommon\('rate'/);
|
||||
|
||||
expect(scripts.unwind).toContain('ClosePercent = otcformat.fixed6');
|
||||
expect(scripts.unwind).toContain('_.round(tradingFee, 2)');
|
||||
expect(scripts.unwind).toContain('.toFixed(2)');
|
||||
expect(scripts.edit).toContain('observationRatePrecision = 12');
|
||||
expect(scripts.edit).toMatch(/roundObservationRate[\s\S]*swapPricePrecision\.roundDecimal/);
|
||||
expect(scripts.helper).toContain('getRule: getRule');
|
||||
expect(scripts.helper).toContain('getInputFormat: function');
|
||||
expect(scripts.helper).toContain('roundForSubmit: function');
|
||||
expect(scripts.helper).toContain('normalizeCommon: function');
|
||||
|
||||
expect(activeView).not.toContain('OtcFormatHelper');
|
||||
expect(activeView).not.toMatch(/OtcFormatMoney|OtcFormatNotional/);
|
||||
expect(activeView).toContain('SwapCommonData');
|
||||
expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="amount"/);
|
||||
expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="quantity"/);
|
||||
expect(activeView).toMatch(/js-swap-common[\s\S]*data-kind="rate"/);
|
||||
expect(scripts.view).toContain('formatSwapCommonElements');
|
||||
expect(scripts.edit).toContain('getQuantityInputFormat');
|
||||
expect(scripts.unwind).toContain('getQuantityInputFormat');
|
||||
expect(scripts.flow).toContain('getQuantityInputFormat');
|
||||
expect(scripts.flow).toMatch(/formatSwapQuantity = function \(value, options, rowObject\)/);
|
||||
expect(scripts.flow).toMatch(/row\.UnderlyingInstrumentType[\s\S]*row\.InstrumentType[\s\S]*row\.position[\s\S]*UnderlyingInstrumentType/);
|
||||
expect(scripts.flow).toContain("formatCommon('quantity', value, instrumentType)");
|
||||
expect(activeView).toMatch(/data-kind="quantity"[^>]*data-instrument-type=/);
|
||||
});
|
||||
});
|
||||
@@ -43,6 +43,9 @@ function loadUnwindHelpers() {
|
||||
vueDatePicker() { return {}; },
|
||||
vueNumberInput() { return {}; }
|
||||
},
|
||||
swapPricePrecision: {
|
||||
createVueInputComponent() { return {}; }
|
||||
},
|
||||
tradeHelper: { IsBond() { return false; } },
|
||||
main: {
|
||||
post() {
|
||||
@@ -112,14 +115,14 @@ describe('base-rate pending trading fee', () => {
|
||||
expectClose(result, 450.00);
|
||||
});
|
||||
|
||||
test('partial close fee and pending fee both use the rounded opening fee allocation', () => {
|
||||
const tradingFee = swapPosiFeeCalc.calcAllocatedTradingFee(
|
||||
113.46, consPosiFeeType.Percent, 1.1234, 4039.2, 4000, 10098, 10000);
|
||||
test('a manually adjusted pending fee does not override the base-rate close fee', () => {
|
||||
const tradingFee = swapPosiFeeCalc.calcTradingFee(
|
||||
consPosiFeeType.Unit, 0.123456, 0, 10000);
|
||||
const pendingFee = swapPosiFeeCalc.calcTradingFeePending(
|
||||
113.46, consPosiFeeType.Percent, 1.1234, 4039.2, 4000, 10098, 10000, 0.4);
|
||||
1235.56, consPosiFeeType.Unit, 0.123456, 0, 10000, 0, 10000, 1);
|
||||
|
||||
expectClose(tradingFee, 45.38);
|
||||
expectClose(pendingFee, 45.38);
|
||||
expectClose(tradingFee, 1234.56);
|
||||
expectClose(pendingFee, 1235.56);
|
||||
});
|
||||
|
||||
test('without a configured base rate, the legacy close-percent calculation remains', () => {
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
main.message("新到期日不能为空");
|
||||
return;
|
||||
}
|
||||
if (this.extenstion.extenstionData.NewMaturityDate <= this.extenstion.extenstionData.OldMaturityDate) {
|
||||
main.message("新到期日应晚于原到期日");
|
||||
return;
|
||||
}
|
||||
main.post("/swaptrade2/SaveExtension", { swap_Event: thisObj.extenstion })
|
||||
.done(function (res) {
|
||||
thisObj.extenstion.id = res.obj.id;
|
||||
@@ -80,4 +84,4 @@
|
||||
components: {
|
||||
'vue-datepicker': FastVue.vueDatePicker(),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
//window.otcformat.options.disableGrouping = true;
|
||||
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '' });
|
||||
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '' });
|
||||
const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
|
||||
const inputFormatSwapAmount = swapPricePrecision.getCommonInputFormat('amount', { negative: true, append: '' });
|
||||
const formatSwapAmount = value => swapPricePrecision.formatCommon('amount', value);
|
||||
const formatSwapQuantity = function (value, options, rowObject) {
|
||||
const row = rowObject || {};
|
||||
const instrumentType = row.UnderlyingInstrumentType
|
||||
|| row.InstrumentType
|
||||
|| (row.position && row.position.UnderlyingInstrumentType)
|
||||
|| '';
|
||||
return swapPricePrecision.formatCommon('quantity', value, instrumentType);
|
||||
};
|
||||
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
|
||||
var clients = ylotc.clients;
|
||||
const consUnderlyingFlag = (function () {
|
||||
@@ -268,19 +277,19 @@ function getColModelGridStep1() {
|
||||
label: '成交数量/张数',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.notional
|
||||
formatter: formatSwapQuantity
|
||||
}, {
|
||||
name: 'TradingAmount',
|
||||
label: '成交金额(元)',
|
||||
width: 210,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'TradingFee',
|
||||
label: '交易费用',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'TradingAmountAvg',
|
||||
label: '成交全价',
|
||||
@@ -372,13 +381,13 @@ function getColModelGridStep2() {
|
||||
label: '成交数量/张数',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.notional
|
||||
formatter: formatSwapQuantity
|
||||
}, {
|
||||
name: 'TradingAmount',
|
||||
label: '成交金额(元)',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'TradingAmountAvg',
|
||||
label: '成交均价',
|
||||
@@ -391,7 +400,7 @@ function getColModelGridStep2() {
|
||||
label: '交易费用',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'TradingAmountFeeAvg',
|
||||
label: '含费均价',
|
||||
@@ -521,13 +530,13 @@ function getColModelGridStep3() {
|
||||
label: '成交数量/张数',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.notional
|
||||
formatter: formatSwapQuantity
|
||||
}, {
|
||||
name: 'TradingAmount',
|
||||
label: '成交金额(元)',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'ContractSize',
|
||||
label: '乘数',
|
||||
@@ -540,31 +549,31 @@ function getColModelGridStep3() {
|
||||
label: '交易费用佣金',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'TradingFeePending',
|
||||
label: '待结算交易费用佣金',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'DividendPending',
|
||||
label: '待结算分红收益',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'MarkClosePnl',
|
||||
label: '浮动端平仓盈亏·浮动',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'DividendIn',
|
||||
label: '浮动端平仓盈亏·分红',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}
|
||||
];
|
||||
return col;
|
||||
@@ -664,26 +673,26 @@ function getColModelGridStep4() {
|
||||
label: '名义数量',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.notional
|
||||
formatter: formatSwapQuantity
|
||||
}, {
|
||||
name: 'position.PosiNotionalValue',
|
||||
label: '名义本金',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.StockEqvNotional
|
||||
formatter: formatSwapAmount
|
||||
}
|
||||
, {
|
||||
name: 'position.PosiTradingFee',
|
||||
label: '交易费用佣金',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'position.PosiTradingFeePending',
|
||||
label: '待实现交易费用佣金',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: formatSwapAmount
|
||||
}, {
|
||||
name: 'position.PosiStartDate',
|
||||
label: '起始日期',
|
||||
@@ -861,6 +870,7 @@ var vue = new Vue({
|
||||
SwapTradeNo: "",
|
||||
BsType: 1,
|
||||
UnderlyingCode: "",
|
||||
UnderlyingInstrumentType: "",
|
||||
TradingQty: 0,
|
||||
TradingAmount: 0,
|
||||
TradingFee: 0,
|
||||
@@ -940,8 +950,8 @@ var vue = new Vue({
|
||||
const thisObj = this;
|
||||
main.post(`/swapTrade2/SearchTodayWhetherFRData?dateTime=${this.FRData.date}`).done(function (resp) {
|
||||
if (resp.UnderlyingCode) { // 如果这天有fr007获取值和id,隐藏警告
|
||||
thisObj.FRData.value = (resp.ReferencePrice * 100).toFixed(4);
|
||||
thisObj.FRData.oldValue = (resp.ReferencePrice * 100).toFixed(4);
|
||||
thisObj.FRData.value = (resp.ReferencePrice * 100).toFixed(swapPricePrecision.getCommonPrecision('rate'));
|
||||
thisObj.FRData.oldValue = (resp.ReferencePrice * 100).toFixed(swapPricePrecision.getCommonPrecision('rate'));
|
||||
thisObj.FRData.id = resp.id;
|
||||
thisObj.isShowWarning = false
|
||||
thisObj.isFromArtifical = resp.DataSource === "人工"
|
||||
@@ -1014,6 +1024,12 @@ var vue = new Vue({
|
||||
this.tradeDate = page.valueDate; //将1970/08/08转化成1970-08-08
|
||||
return this.tradeDate;
|
||||
},
|
||||
getQuantityInputFormat() {
|
||||
return swapPricePrecision.getCommonInputFormat(
|
||||
'quantity',
|
||||
{ append: '' },
|
||||
this.swapflow.UnderlyingInstrumentType);
|
||||
},
|
||||
addNew() {
|
||||
this.tradeDate = page.valueDate;
|
||||
this.initSwapFlow();
|
||||
@@ -1029,6 +1045,7 @@ var vue = new Vue({
|
||||
SwapTradeNo: "",
|
||||
BsType: 1,
|
||||
UnderlyingCode: "",
|
||||
UnderlyingInstrumentType: "",
|
||||
TradingQty: 0,
|
||||
TradingAmount: 0,
|
||||
TradingFee: 0,
|
||||
@@ -1126,6 +1143,7 @@ var vue = new Vue({
|
||||
thisObj.swapflow.SwapTradeNo = item.SwapTradeNo;
|
||||
thisObj.swapflow.BsType = item.BsType;
|
||||
thisObj.swapflow.UnderlyingCode = item.UnderlyingCode;
|
||||
thisObj.swapflow.UnderlyingInstrumentType = item.UnderlyingInstrumentType || item.InstrumentType || "";
|
||||
thisObj.swapflow.TradingQty = item.TradingQty;
|
||||
thisObj.swapflow.TradingAmount = item.TradingAmount;
|
||||
thisObj.swapflow.TradingFee = item.TradingFee;
|
||||
@@ -1155,6 +1173,7 @@ var vue = new Vue({
|
||||
setUnderlyingCode(data) {
|
||||
this.swapflow.ContractSize = data.ContractSize;
|
||||
this.swapflow.UnderlyingName = data.Name;
|
||||
this.swapflow.UnderlyingInstrumentType = data.InstrumentType || data.UnderlyingInstrumentType || "";
|
||||
},
|
||||
changeClient: function (client) {
|
||||
this.swapflow.ClientId = client.id;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
//otcformat禁止千分位分组
|
||||
window.otcformat.options.disableGrouping = true;
|
||||
|
||||
const inputFormatSwapRate = Object.freeze({ precision: otcformat.trading.premiumRateP.precision, append: '%' });
|
||||
const inputFormatSwapRate = swapPricePrecision.getCommonInputFormat('rate', { append: '%' });
|
||||
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, append: '' });
|
||||
const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
|
||||
const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
|
||||
const inputFormatInterestAmount = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
|
||||
const inputFormatDividend = Object.freeze({ precision: 2, append: '', negative: true });
|
||||
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
|
||||
const inputFormatInterestAmount = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
|
||||
const inputFormatDividend = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
|
||||
const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
|
||||
const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
|
||||
const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
|
||||
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
|
||||
const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true });
|
||||
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, append: '', negative: true });
|
||||
@@ -60,6 +62,12 @@ const vue = new Vue({
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
formatAmount(value) {
|
||||
return swapPricePrecision.formatCommon('amount', value);
|
||||
},
|
||||
formatQuantity(value) {
|
||||
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
|
||||
},
|
||||
isAfterMaxIncomeValueDate(valueDate) {
|
||||
return valueDate && MaxIncomeValueDate && valueDate > MaxIncomeValueDate;
|
||||
},
|
||||
@@ -120,31 +128,31 @@ const vue = new Vue({
|
||||
return pricef;
|
||||
},
|
||||
dataFormat() {
|
||||
this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
|
||||
this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
|
||||
this.deal.PositionQty2 = otcformat.trading.notional(this.deal.PositionQty);
|
||||
this.floatPosition.Quantity = otcformat.trading.notional(this.floatPosition.Quantity);
|
||||
this.floatPosition.PositionQty = otcformat.trading.notional(this.floatPosition.PositionQty);
|
||||
this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
|
||||
this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
|
||||
this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
|
||||
this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
|
||||
this.deal.PositionQty2 = formatSwapQuantity(this.deal.PositionQty);
|
||||
this.floatPosition.Quantity = formatSwapQuantity(this.floatPosition.Quantity);
|
||||
this.floatPosition.PositionQty = formatSwapQuantity(this.floatPosition.PositionQty);
|
||||
this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
|
||||
//this.floatPosition.PosiNetPrice = otcformat.trading.umprice(this.floatPosition.PosiNetPrice);
|
||||
//this.floatPosition.PosiGrossPrice = otcformat.trading.umprice(this.floatPosition.PosiGrossPrice);
|
||||
this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee);
|
||||
this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2);
|
||||
this.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(this.floatPosition.MarkClosePnl);
|
||||
this.floatPosition.TradingFee = formatSwapAmount(this.floatPosition.TradingFee);
|
||||
this.floatPosition.DividendIn = formatSwapAmount(this.floatPosition.DividendIn);
|
||||
this.floatPosition.MarkClosePnl = formatSwapAmount(this.floatPosition.MarkClosePnl);
|
||||
this.interestList.forEach(x => {
|
||||
//x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
|
||||
//x.Principal = formatSwapAmount(x.Principal);
|
||||
//x.Rate = otcformat.fixed6(x.Rate);
|
||||
x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestFee = formatSwapAmount(x.InterestFee);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
|
||||
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestFee = formatSwapAmount(x.InterestFee);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
});
|
||||
},
|
||||
setValueDate(e) {//修改平仓日期
|
||||
@@ -183,7 +191,7 @@ const vue = new Vue({
|
||||
changeInterestAmount(item) {//修改利息金额
|
||||
let InterestFee = item.InterestFee == "" ? 0 : parseFloat(item.InterestFee);
|
||||
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
|
||||
item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio + InterestFee * interestRatio);
|
||||
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + InterestFee * interestRatio);
|
||||
this.calcCloseAmount();
|
||||
},
|
||||
calcFloatClosePnl() {//计算浮动端平仓盈亏
|
||||
@@ -197,7 +205,7 @@ const vue = new Vue({
|
||||
// CloseNotionalValue 是期初全价折算后的名义本金,直接乘价差会重复包含期初价格。
|
||||
let positionAmount = parseFloat(thisObj.floatPosition.Quantity) * parseFloat(thisObj.floatPosition.ContractSize || 1);
|
||||
thisObj.floatPosition.MarkClosePnl = positionAmount * (deliveryPrice - thisObj.initPosiGrossPrice) * floatRatio;
|
||||
thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红
|
||||
thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);//MarkClosePnl 纯盯市不要计算交易费用和分红
|
||||
// 守卫: 浮动盈亏合计必须保留 2 位小数 → 对应历史 bug 3c5f25a5(原代码缺精度保留)
|
||||
// 数值由 swapCalc.calcFloatPnlSum 计算, 此处 .toFixed(2) 仅保留字符串类型以兼容下游
|
||||
thisObj.floatPosition.FloatPnlSum = SwapCalc.calcFloatPnlSum(thisObj.floatPosition.MarkClosePnl, TradingFee, TradingFeePending, DividendIn).toFixed(2);
|
||||
@@ -205,7 +213,7 @@ const vue = new Vue({
|
||||
},
|
||||
//calcClosePnL() {//计算浮动端平仓盈亏
|
||||
// let pnl = parseFloat(this.floatPosition.ClosePnL) - parseFloat(this.floatPosition.TradingFee);
|
||||
// this.floatPosition.ClosePnL = otcformat.trading.StockEqvNotional(pnl);
|
||||
// this.floatPosition.ClosePnL = formatSwapAmount(pnl);
|
||||
// this.calcCloseAmount();
|
||||
//},
|
||||
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
|
||||
@@ -237,9 +245,9 @@ const vue = new Vue({
|
||||
thisObj.deal.SwapMarginRebatePnl = parseFloat(thisObj.deal.SwapMarginRebatePnl) + interestAmount;
|
||||
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
|
||||
});
|
||||
thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
|
||||
},
|
||||
getInterestList() {//根据平仓日期获取利息腿信息
|
||||
var thisObj = this;
|
||||
@@ -258,18 +266,14 @@ const vue = new Vue({
|
||||
},
|
||||
getDivindIn() {
|
||||
var thisObj = this;
|
||||
let ratio = this.floatPosition.PositionType == 1 ? 1 : -1;
|
||||
let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1;
|
||||
var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate }
|
||||
main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) {
|
||||
let totalDividend = parseFloat(thisObj.deal.NotionalQty) * resp.obj.totalInterest * ratio * floatRatio;//总的
|
||||
let consumedDividend = Math.abs(parseFloat(resp.obj.consumedDividend ?? 0)) * ratio * floatRatio;//已实现的
|
||||
// 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0
|
||||
thisObj.floatPosition.DividendIn = Math.abs(consumedDividend) > 0 ? parseFloat((totalDividend - consumedDividend).toFixed(2)) : parseFloat(totalDividend.toFixed(2));
|
||||
thisObj.floatPosition.DividendPending = 0;
|
||||
thisObj.calcFloatClosePnl();
|
||||
thisObj.dataFormat();
|
||||
});
|
||||
// 方案C:分红改由后端 InitIncome 读 EOD PosiDividendSum 填入 floatPosition.DividendIn(单一可信源)。
|
||||
// 前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓部分的重复计入。
|
||||
// floatPosition.DividendIn 保持后端返回值不动(=本次互换要落袋的全量待实现)。
|
||||
// DividendPending(待结算)互换页保持 0:互换=全量结清,结清后账上无待结算。
|
||||
// ↳ 与平仓页不同:平仓只拿走一部分,剩余持仓仍有待结算 → 平仓页 DividendPending=PosiDividendSum(后端值)。
|
||||
thisObj.floatPosition.DividendPending = 0;
|
||||
thisObj.calcFloatClosePnl();
|
||||
thisObj.dataFormat();
|
||||
},
|
||||
incomeTrade() {//互换
|
||||
var thisObj = this;
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
function calcCloseQtyByOriginalPercent(closePercent, oriClosePercent, positionQty) {
|
||||
var ori = Number(oriClosePercent);
|
||||
if (ori === 0) return 0;
|
||||
if (Number(closePercent) >= ori) return Number(positionQty);
|
||||
return roundHalfAwayFromZero(Number(positionQty) * (Number(closePercent) / ori), 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//otcformat禁止千分位分组
|
||||
window.otcformat.options.disableGrouping = true;
|
||||
const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
|
||||
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
|
||||
const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
|
||||
const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
|
||||
const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
|
||||
let ValueDate = model.ValueDate;
|
||||
const vue = new Vue({
|
||||
el: '#vueDiv',
|
||||
@@ -22,6 +25,12 @@ const vue = new Vue({
|
||||
this.setValueDate();
|
||||
},
|
||||
methods: {
|
||||
formatAmount(value) {
|
||||
return swapPricePrecision.formatCommon('amount', value);
|
||||
},
|
||||
formatQuantity(value) {
|
||||
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
|
||||
},
|
||||
initDeal() {
|
||||
this.interestList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
@@ -42,29 +51,29 @@ const vue = new Vue({
|
||||
}
|
||||
},
|
||||
dataFormat() {
|
||||
this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
|
||||
this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
|
||||
this.deal.PositionQty = otcformat.trading.notional(this.deal.PositionQty);
|
||||
this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
|
||||
this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
|
||||
this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
|
||||
this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
|
||||
this.deal.PositionQty = formatSwapQuantity(this.deal.PositionQty);
|
||||
this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
|
||||
this.interestList.forEach(x => {
|
||||
//x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
|
||||
//x.Principal = formatSwapAmount(x.Principal);
|
||||
//x.Rate = otcformat.fixed6(x.Rate);
|
||||
x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestFee = formatSwapAmount(x.InterestFee);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
|
||||
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
x.InterestFee = otcformat.trading.StockEqvNotional(x.InterestFee);
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestFee = formatSwapAmount(x.InterestFee);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
});
|
||||
},
|
||||
changeInterestAmount(item) {//修改利息金额
|
||||
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
|
||||
item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee));
|
||||
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee));
|
||||
this.calcCloseAmount();
|
||||
},
|
||||
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
|
||||
@@ -86,10 +95,10 @@ const vue = new Vue({
|
||||
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
|
||||
thisObj.deal.SwapMarginAmount = parseFloat(thisObj.deal.SwapMarginAmount) + parseFloat(x.InterestPrincipal);
|
||||
});
|
||||
thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginAmount);
|
||||
thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
|
||||
},
|
||||
getInterestList() {//根据平仓日期获取利息腿信息
|
||||
var thisObj = this;
|
||||
|
||||
@@ -1,47 +1,57 @@
|
||||
var swapPricePrecision = (function (global) {
|
||||
const defaults = Object.freeze({
|
||||
Stock: { integerDigits: 7, precision: 2 },
|
||||
StockIndex: { integerDigits: 7, precision: 2 },
|
||||
StockIF: { integerDigits: 7, precision: 4 },
|
||||
CommodityFutures: { integerDigits: 7, precision: 4 },
|
||||
CommoditySpot: { integerDigits: 7, precision: 4 },
|
||||
NewOtcStock: { integerDigits: 7, precision: 4 },
|
||||
HKStock: { integerDigits: 7, precision: 4 },
|
||||
HKStockIndex: { integerDigits: 7, precision: 4 },
|
||||
Fund: { integerDigits: 7, precision: 4 },
|
||||
common: {
|
||||
amount: { precision: 2, grouping: true },
|
||||
quantity: { integerDigits: 16, precision: 2, grouping: true },
|
||||
rate: { precision: 4 }
|
||||
},
|
||||
Stock: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
StockIndex: { integerDigits: 7, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
StockIF: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
CommodityFutures: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
CommoditySpot: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
NewOtcStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
HKStock: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
HKStockIndex: { integerDigits: 7, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
Fund: { integerDigits: 7, precision: 4, quantityPrecision: 4, quantityIntegerDigits: 12 },
|
||||
Bond: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
TBonds: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
CreditBonds: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
OtherBonds: {
|
||||
quantityPrecision: 0, quantityIntegerDigits: 16,
|
||||
grossPrice: { integerDigits: 6, precision: 9 },
|
||||
netPrice: { integerDigits: 6, precision: 9 },
|
||||
yield: { integerDigits: 2, precision: 4 }
|
||||
},
|
||||
TBFutures: { integerDigits: 8, precision: 4 },
|
||||
OtherFutures: { integerDigits: 8, precision: 4 },
|
||||
GoldSpot: { integerDigits: 8, precision: 4 },
|
||||
OtherSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadFutures: { integerDigits: 8, precision: 4 },
|
||||
AbroadSpot: { integerDigits: 8, precision: 4 },
|
||||
AbroadStock: { integerDigits: 8, precision: 2 },
|
||||
AbroadStockIndex: { integerDigits: 8, precision: 4 },
|
||||
ExRate: { integerDigits: 2, precision: 8 },
|
||||
Shibor: { integerDigits: 2, precision: 4 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4 },
|
||||
RateYield: {integerDigits: 6, precision: 8},
|
||||
BondIndex: {integerDigits: 6, precision: 4},
|
||||
TBFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
OtherFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
GoldFutures: { quantityIntegerDigits: 12 },
|
||||
GoldSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
OtherSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadFutures: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadSpot: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadStock: { integerDigits: 8, precision: 2, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
AbroadStockIndex: { integerDigits: 8, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
ExRate: { integerDigits: 2, precision: 8, quantityPrecision: 8, quantityIntegerDigits: 16 },
|
||||
Shibor: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12 },
|
||||
RateYield: {integerDigits: 6, precision: 8, quantityPrecision: 2, quantityIntegerDigits: 12},
|
||||
BondIndex: {integerDigits: 6, precision: 4, quantityPrecision: 2, quantityIntegerDigits: 12},
|
||||
|
||||
});
|
||||
|
||||
@@ -121,6 +131,18 @@ var swapPricePrecision = (function (global) {
|
||||
return { integerDigits: integerDigits, precision: precision };
|
||||
}
|
||||
|
||||
function normalizeCommonRule(rule) {
|
||||
if (!rule || typeof rule !== 'object') return null;
|
||||
const precision = Number(rule.precision);
|
||||
if (!Number.isInteger(precision) || precision < 0 || precision > 13) return null;
|
||||
const integerDigits = Number(rule.integerDigits);
|
||||
return {
|
||||
precision: precision,
|
||||
integerDigits: Number.isInteger(integerDigits) && integerDigits >= 1 && integerDigits <= 18 ? integerDigits : undefined,
|
||||
grouping: typeof rule.grouping === 'boolean' ? rule.grouping : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function findRule(source, instrumentType, field) {
|
||||
const typeRule = source && source[instrumentType];
|
||||
return typeRule ? normalizeRule(typeRule[field] || typeRule) : null;
|
||||
@@ -132,6 +154,73 @@ var swapPricePrecision = (function (global) {
|
||||
return findRule(global.main && global.main.swapPricePrecision, instrumentType, field) || fallback;
|
||||
}
|
||||
|
||||
function formatFixed(value, precision) {
|
||||
const normalized = normalizeDecimal(roundDecimal(value, precision));
|
||||
if (!normalized) return '';
|
||||
const negative = normalized.charAt(0) === '-';
|
||||
const parts = (negative ? normalized.substring(1) : normalized).split('.');
|
||||
const integerPart = parts[0];
|
||||
if (precision === 0) return (negative ? '-' : '') + integerPart;
|
||||
return (negative ? '-' : '') + integerPart + '.' + (parts[1] || '').padEnd(precision, '0');
|
||||
}
|
||||
|
||||
function groupDecimal(value) {
|
||||
if (!value) return value;
|
||||
const negative = value.charAt(0) === '-';
|
||||
const source = negative ? value.substring(1) : value;
|
||||
const parts = source.split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return (negative ? '-' : '') + parts.join('.');
|
||||
}
|
||||
|
||||
function getQuantityPrecision(instrumentType, fallback) {
|
||||
if (!instrumentType) return fallback;
|
||||
const configured = global.main && global.main.swapPricePrecision;
|
||||
const typeRule = (configured || defaults)[instrumentType];
|
||||
if (!typeRule || !Object.prototype.hasOwnProperty.call(typeRule, 'quantityPrecision')) return fallback;
|
||||
const precision = Number(typeRule.quantityPrecision);
|
||||
return Number.isInteger(precision) && precision >= 0 && precision <= 13 ? precision : fallback;
|
||||
}
|
||||
|
||||
function getQuantityIntegerDigits(instrumentType, fallback) {
|
||||
if (!instrumentType) return fallback;
|
||||
const configured = global.main && global.main.swapPricePrecision;
|
||||
const typeRule = (configured || defaults)[instrumentType];
|
||||
if (!typeRule || !Object.prototype.hasOwnProperty.call(typeRule, 'quantityIntegerDigits')) return fallback;
|
||||
const integerDigits = Number(typeRule.quantityIntegerDigits);
|
||||
return Number.isInteger(integerDigits) && integerDigits >= 1 && integerDigits <= 18 ? integerDigits : fallback;
|
||||
}
|
||||
|
||||
function getCommonRule(kind, instrumentType) {
|
||||
const fallback = normalizeCommonRule(defaults.common[kind]) || { precision: 2, grouping: false };
|
||||
const configured = global.main && global.main.swapPricePrecision
|
||||
&& global.main.swapPricePrecision.common;
|
||||
const configuredRule = normalizeCommonRule(configured && configured[kind]);
|
||||
const rule = configuredRule || fallback;
|
||||
if (rule.grouping === undefined) rule.grouping = fallback.grouping;
|
||||
if (rule.integerDigits === undefined) rule.integerDigits = fallback.integerDigits;
|
||||
if (kind === 'quantity') {
|
||||
rule.precision = getQuantityPrecision(instrumentType, rule.precision);
|
||||
rule.integerDigits = getQuantityIntegerDigits(instrumentType, rule.integerDigits);
|
||||
}
|
||||
return rule;
|
||||
}
|
||||
|
||||
function formatCommon(kind, value, instrumentType, options) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
if (instrumentType && typeof instrumentType === 'object') {
|
||||
options = instrumentType;
|
||||
instrumentType = options.instrumentType;
|
||||
}
|
||||
const rule = getCommonRule(kind, instrumentType);
|
||||
const displayValue = kind === 'rate' ? shiftDecimal(value, 2) : value;
|
||||
const formatted = formatFixed(displayValue, rule.precision);
|
||||
if (!formatted) return '';
|
||||
const grouping = options && options.grouping !== undefined ? !!options.grouping : rule.grouping;
|
||||
const text = grouping ? groupDecimal(formatted) : formatted;
|
||||
return kind === 'rate' ? text + '%' : text;
|
||||
}
|
||||
|
||||
function format(value, instrumentType, field) {
|
||||
const rule = getRule(instrumentType, field);
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
@@ -260,6 +349,26 @@ var swapPricePrecision = (function (global) {
|
||||
|
||||
return Object.freeze({
|
||||
getRule: getRule,
|
||||
getCommonPrecision: function (kind, instrumentType) {
|
||||
return getCommonRule(kind, instrumentType).precision;
|
||||
},
|
||||
getCommonInputFormat: function (kind, options, instrumentType) {
|
||||
const inputOptions = Object.assign({}, options || {});
|
||||
const type = instrumentType || inputOptions.instrumentType;
|
||||
delete inputOptions.instrumentType;
|
||||
const rule = getCommonRule(kind, type);
|
||||
const result = Object.assign(inputOptions, {
|
||||
precision: rule.precision,
|
||||
grouping: inputOptions.grouping === undefined ? !!rule.grouping : !!inputOptions.grouping,
|
||||
trimTailZeros: false
|
||||
});
|
||||
if (rule.integerDigits !== undefined) result.integerDigits = rule.integerDigits;
|
||||
return result;
|
||||
},
|
||||
formatCommon: formatCommon,
|
||||
normalizeCommon: function (kind, value, instrumentType) {
|
||||
return formatCommon(kind, value, instrumentType, { grouping: false });
|
||||
},
|
||||
getInputFormat: function (instrumentType, field, options) {
|
||||
const rule = getRule(instrumentType, field);
|
||||
return rule ? Object.assign({}, options, rule) : Object.assign({}, options);
|
||||
|
||||
@@ -10,13 +10,11 @@ const consAssetUnits = page.canAddNewTrader ? ylotc.assetunits
|
||||
: ylotc.assetunits.filter(x => x.TraderIds.includes(page.Trade.TraderId));
|
||||
|
||||
const inputFormatInteger = Object.freeze({ precision: 0, append: '' });
|
||||
const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
|
||||
const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
|
||||
const inputFormatPositionQuantityFixed2 = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
|
||||
const inputFormatSwapRate = Object.freeze({ precision: 4, negative: true, append: '%', trimTailZeros: false });
|
||||
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
|
||||
const inputFormatSwapRate = swapPricePrecision.getCommonInputFormat('rate', { negative: true, append: '%' });
|
||||
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, negative: true, append: '' });
|
||||
const inputFormatTradeSinglePrice = Object.freeze({ precision: otcformat.trading.tradeSinglePrice.precision, negative: true, append: '', percent: false });
|
||||
const inputFormatTradeSinglePriceFixed2 = Object.freeze({ precision: 2, negative: true, append: '', percent: false, trimTailZeros: false });
|
||||
const inputFormatTradeSinglePrice = swapPricePrecision.getCommonInputFormat('amount', { negative: true, append: '', percent: false });
|
||||
const inputFormatTradeSinglePriceFixed2 = swapPricePrecision.getCommonInputFormat('amount', { negative: true, append: '', percent: false });
|
||||
const inputFormatPosiFeePercent = Object.freeze({ precision: 4, negative: true, append: '%' });
|
||||
const inputFormatPosiFeeUnit = Object.freeze({ precision: 6, negative: true, append: '' });
|
||||
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
|
||||
@@ -37,7 +35,7 @@ const observationRateTextFromPercent = function (value) {
|
||||
const formatObservationRate = function (value) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(4) : value;
|
||||
return Number.isFinite(number) ? number.toFixed(swapPricePrecision.getCommonPrecision('rate')) : value;
|
||||
};
|
||||
const swapPosiFeeCalc = Object.freeze({
|
||||
normalizeFeeType(feeType) {
|
||||
@@ -270,6 +268,12 @@ const vue = new Vue({
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getQuantityInputFormat(item) {
|
||||
return swapPricePrecision.getCommonInputFormat(
|
||||
'quantity',
|
||||
{ append: '' },
|
||||
item && item.UnderlyingInstrumentType);
|
||||
},
|
||||
getPosiPriceInputFormat(item, field) {
|
||||
return swapPricePrecision.getInputFormat(
|
||||
item && item.UnderlyingInstrumentType,
|
||||
@@ -563,7 +567,7 @@ const vue = new Vue({
|
||||
},
|
||||
//变更名义本金(仅格式化,不反算数量)
|
||||
changeStockEqvNotional() {
|
||||
this.trade.StockEqvNotional = otcformat.trading.StockEqvNotional(this.trade.StockEqvNotional);
|
||||
this.trade.StockEqvNotional = swapPricePrecision.normalizeCommon('amount', this.trade.StockEqvNotional);
|
||||
this.refreshPayTradingFeesByUnit();
|
||||
//计算数量
|
||||
// if (this.paySwapList.length > 0) {
|
||||
@@ -651,7 +655,7 @@ const vue = new Vue({
|
||||
// 守卫: 名义本金必须 round 到 2 位 → 对应历史 bug f873239a(缺 _.round); 外置到 swapCalc.calcStockEqvNotional
|
||||
var deliveryPrice = this.roundStoragePrice(payItem, payItem.PosiGrossPrice, 'grossPrice');
|
||||
var stockEqvNotional = SwapCalc.calcStockEqvNotional(deliveryPrice, national);//名义本金=期初价格*数量*乘数
|
||||
this.trade.StockEqvNotional = otcformat.trading.stockEqvNotional(stockEqvNotional);
|
||||
this.trade.StockEqvNotional = swapPricePrecision.normalizeCommon('amount', stockEqvNotional);
|
||||
payItem.PosiNotionalValue = this.trade.StockEqvNotional;
|
||||
this.refreshPayTradingFeesByUnit();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,15 @@ function formatSwapPriceElements() {
|
||||
});
|
||||
}
|
||||
|
||||
function formatSwapCommonElements() {
|
||||
$('.js-swap-common').each(function () {
|
||||
this.textContent = swapPricePrecision.formatCommon(
|
||||
this.dataset.kind,
|
||||
this.dataset.value,
|
||||
this.dataset.instrumentType);
|
||||
});
|
||||
}
|
||||
|
||||
function deletetrade(id) { //无效化
|
||||
main.confirm(page.ConfirmInfo, function () {
|
||||
$.ajax({
|
||||
@@ -502,7 +511,7 @@ var vueDetails = new Vue({
|
||||
if (swapIntervals && swapIntervals.length>0) {
|
||||
that.SwapIntervalList = JSON.parse(swapIntervals);
|
||||
that.SwapIntervalList.forEach((item, index) => {
|
||||
that.SwapIntervalList[index].Rate = otcformat.fixed4P(item.Rate);
|
||||
that.SwapIntervalList[index].Rate = swapPricePrecision.formatCommon('rate', item.Rate);
|
||||
// 如果结算日期为空,默认等于观察日期
|
||||
if (!item.SettlementDate) {
|
||||
that.SwapIntervalList[index].SettlementDate = item.Date;
|
||||
@@ -553,6 +562,7 @@ function chk_onclick(obj) {
|
||||
|
||||
$(function () {
|
||||
formatSwapPriceElements();
|
||||
formatSwapCommonElements();
|
||||
refreshEntryExit();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//otcformat禁止千分位分组
|
||||
window.otcformat.options.disableGrouping = true;
|
||||
const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', trimTailZeros: false });
|
||||
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '' });
|
||||
const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
|
||||
const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
|
||||
const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
|
||||
let dealDate = model.DealDate;
|
||||
const vue = new Vue({
|
||||
el: '#vueDiv',
|
||||
@@ -21,6 +24,12 @@ const vue = new Vue({
|
||||
this.dataFormat();
|
||||
},
|
||||
methods: {
|
||||
formatAmount(value) {
|
||||
return swapPricePrecision.formatCommon('amount', value);
|
||||
},
|
||||
formatQuantity(value) {
|
||||
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
|
||||
},
|
||||
initDeal() {
|
||||
this.interestList = model.FlowEvents.filter((item) => {
|
||||
return item.InterestMode == 1 || item.InterestMode == 2 || item.InterestMode == 7 || item.InterestMode == 8 || item.InterestMode == 9;
|
||||
@@ -30,27 +39,27 @@ const vue = new Vue({
|
||||
});
|
||||
},
|
||||
dataFormat() {
|
||||
this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
|
||||
this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
|
||||
this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
|
||||
this.deal.PositionQty = otcformat.trading.notional(this.deal.PositionQty);
|
||||
this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
|
||||
this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
|
||||
this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
|
||||
this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
|
||||
this.deal.PositionQty = formatSwapQuantity(this.deal.PositionQty);
|
||||
this.interestList.forEach(x => {
|
||||
//x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
|
||||
//x.Principal = formatSwapAmount(x.Principal);
|
||||
//x.Rate = otcformat.fixed6(x.Rate);
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
|
||||
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
});
|
||||
},
|
||||
changeInterestAmount(item) {//修改利息金额
|
||||
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
|
||||
item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio+ parseFloat(item.InterestFee));
|
||||
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio+ parseFloat(item.InterestFee));
|
||||
this.calcCloseAmount();
|
||||
},
|
||||
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
|
||||
@@ -73,10 +82,10 @@ const vue = new Vue({
|
||||
thisObj.deal.SwapRealizedPnL = parseFloat(thisObj.deal.SwapRealizedPnL) + interestAmount;
|
||||
thisObj.deal.SwapMarginAmount = parseFloat(thisObj.deal.SwapMarginAmount) + parseFloat(x.InterestPrincipal) * interestRatio;
|
||||
});
|
||||
thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginAmount);
|
||||
thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
|
||||
},
|
||||
closeTrade() {//平仓
|
||||
var thisObj = this;
|
||||
|
||||
@@ -3,9 +3,11 @@ window.otcformat.options.disableGrouping = true;
|
||||
|
||||
const inputFormatSwapRate = Object.freeze({ precision: 4, append: '%', trimTailZeros: false });
|
||||
const inputFormatTradePrice = Object.freeze({ precision: otcformat.trading.tradePrice.precision, append: '' });
|
||||
const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.notional.precision, append: '' });
|
||||
const inputFormatEqvNotional = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
|
||||
const inputFormatCloseAmount = Object.freeze({ precision: 2, append: '', negative: true, trimTailZeros: false });
|
||||
const inputFormatEqvNotional = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
|
||||
const inputFormatCloseAmount = swapPricePrecision.getCommonInputFormat('amount', { append: '', negative: true });
|
||||
const swapInstrumentType = (model.FlowEvents || []).find(item => item && item.UnderlyingInstrumentType)?.UnderlyingInstrumentType || model.UnderlyingInstrumentType || '';
|
||||
const formatSwapAmount = value => swapPricePrecision.normalizeCommon('amount', value);
|
||||
const formatSwapQuantity = value => swapPricePrecision.normalizeCommon('quantity', value, swapInstrumentType);
|
||||
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
|
||||
const inputFormatMarginRateNoPercent = Object.freeze({ precision: otcformat.trading.umpriceP.precision, append: '', percent: true });
|
||||
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, append: '', negative: true });
|
||||
@@ -22,7 +24,7 @@ const swapPosiFeeCalc = {
|
||||
const tradingFee = normalizedFeeType === consPosiFeeType.Unit
|
||||
? normalizedFeeUnit * normalizedCloseQty
|
||||
: normalizedFeeUnit / 100 * normalizedCloseNotionalValue;
|
||||
return otcformat.trading.StockEqvNotional(_.round(tradingFee, 2));
|
||||
return formatSwapAmount(_.round(tradingFee, 2));
|
||||
},
|
||||
calcAllocatedTradingFee(totalFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty) {
|
||||
const normalizedFeeUnit = Number(feeUnit) || 0;
|
||||
@@ -37,7 +39,7 @@ const swapPosiFeeCalc = {
|
||||
return null;
|
||||
}
|
||||
|
||||
return otcformat.trading.StockEqvNotional(_.round((Number(totalFee) || 0) * closeBase / originalBase, 2));
|
||||
return formatSwapAmount(_.round((Number(totalFee) || 0) * closeBase / originalBase, 2));
|
||||
},
|
||||
calcTradingFeePending(beforeCloseFee, feeType, feeUnit, closeNotionalValue, closeQty, notionalValue, notionalQty, closePercent) {
|
||||
const allocatedFee = this.calcAllocatedTradingFee(
|
||||
@@ -79,6 +81,15 @@ const vue = new Vue({
|
||||
this.setUnwindDate();
|
||||
},
|
||||
methods: {
|
||||
formatAmount(value) {
|
||||
return swapPricePrecision.formatCommon('amount', value);
|
||||
},
|
||||
formatQuantity(value) {
|
||||
return swapPricePrecision.formatCommon('quantity', value, swapInstrumentType);
|
||||
},
|
||||
getQuantityInputFormat() {
|
||||
return swapPricePrecision.getCommonInputFormat('quantity', { append: '' }, swapInstrumentType);
|
||||
},
|
||||
getDeliveryPriceInputFormat() {
|
||||
return swapPricePrecision.getInputFormat(
|
||||
this.floatPosition && this.floatPosition.UnderlyingInstrumentType,
|
||||
@@ -124,37 +135,37 @@ const vue = new Vue({
|
||||
return pricef;
|
||||
},
|
||||
dataFormat() {
|
||||
this.deal.NotionalValue = otcformat.fixed2(parseFloat(this.deal.NotionalValue));
|
||||
this.deal.PosiNotionalValue = otcformat.fixed2(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.CloseNotionalValue = otcformat.fixed2(parseFloat(this.deal.CloseNotionalValue));
|
||||
this.deal.NotionalQty = otcformat.trading.notional(this.deal.NotionalQty);
|
||||
this.deal.PositionQty2 = otcformat.trading.notional(this.deal.PositionQty);
|
||||
this.floatPosition.Quantity = otcformat.trading.notional(this.floatPosition.Quantity);
|
||||
this.floatPosition.PositionQty = otcformat.trading.notional(this.floatPosition.PositionQty);
|
||||
this.deal.CloseQty = otcformat.trading.notional(this.deal.CloseQty);
|
||||
this.deal.NotionalValue = formatSwapAmount(this.deal.NotionalValue);
|
||||
this.deal.PosiNotionalValue = formatSwapAmount(this.deal.PosiNotionalValue);
|
||||
this.deal.CloseNotionalValue = formatSwapAmount(this.deal.CloseNotionalValue);
|
||||
this.deal.NotionalQty = formatSwapQuantity(this.deal.NotionalQty);
|
||||
this.deal.PositionQty2 = formatSwapQuantity(this.deal.PositionQty);
|
||||
this.floatPosition.Quantity = formatSwapQuantity(this.floatPosition.Quantity);
|
||||
this.floatPosition.PositionQty = formatSwapQuantity(this.floatPosition.PositionQty);
|
||||
this.deal.CloseQty = formatSwapQuantity(this.deal.CloseQty);
|
||||
this.deal.ClosePercent = otcformat.fixed6(this.deal.ClosePercent);
|
||||
this.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(this.deal.SwapCloseAmount);
|
||||
this.deal.SwapCloseAmount = formatSwapAmount(this.deal.SwapCloseAmount);
|
||||
//this.floatPosition.PosiNetPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiNetPrice);
|
||||
//this.floatPosition.PosiGrossPrice = otcformat.trading.tradeSinglePrice(this.floatPosition.PosiGrossPrice);
|
||||
this.floatPosition.TradingAmountAvg = swapPricePrecision.roundForSubmit(
|
||||
this.floatPosition.TradingAmountAvg,
|
||||
this.floatPosition.UnderlyingInstrumentType,
|
||||
'grossPrice');
|
||||
this.floatPosition.TradingFee = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFee);
|
||||
this.floatPosition.TradingFeePending = otcformat.trading.StockEqvNotional(this.floatPosition.TradingFeePending);
|
||||
this.floatPosition.DividendIn = parseFloat(this.floatPosition.DividendIn).toFixed(2);
|
||||
this.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(this.floatPosition.MarkClosePnl);
|
||||
this.floatPosition.TradingFee = formatSwapAmount(this.floatPosition.TradingFee);
|
||||
this.floatPosition.TradingFeePending = formatSwapAmount(this.floatPosition.TradingFeePending);
|
||||
this.floatPosition.DividendIn = formatSwapAmount(this.floatPosition.DividendIn);
|
||||
this.floatPosition.MarkClosePnl = formatSwapAmount(this.floatPosition.MarkClosePnl);
|
||||
this.interestList.forEach(x => {
|
||||
//x.Principal = otcformat.trading.StockEqvNotional(x.Principal);
|
||||
//x.Principal = formatSwapAmount(x.Principal);
|
||||
//x.Rate = otcformat.fixed6(x.Rate);
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
//x.InterestStartDate = x.InterestStartDate ? x.InterestStartDate.substr(0, 10) : "";
|
||||
//x.InterestEndDate = x.InterestEndDate ? x.InterestEndDate.substr(0, 10) : "";
|
||||
});
|
||||
this.marginList.forEach(x => {
|
||||
x.InterestAmount = otcformat.trading.StockEqvNotional(x.InterestAmount);
|
||||
x.InterestClosePnL = otcformat.trading.StockEqvNotional(x.InterestClosePnL);
|
||||
x.InterestAmount = formatSwapAmount(x.InterestAmount);
|
||||
x.InterestClosePnL = formatSwapAmount(x.InterestClosePnL);
|
||||
});
|
||||
},
|
||||
setValueDate(e) {//修改平仓日期
|
||||
@@ -183,7 +194,7 @@ const vue = new Vue({
|
||||
changeCloseMethod() {//修改平仓类型
|
||||
if (this.deal.CloseMethod == 1) {
|
||||
this.deal.ClosePercent = this.oriClosePercent;
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.PosiNotionalValue));
|
||||
this.deal.CloseQty = this.deal.PositionQty;
|
||||
} else {
|
||||
// ClosePercent 是占期初口径(A),需除以 oriClosePercent 转占剩余(B) 再乘剩余数量
|
||||
@@ -213,15 +224,7 @@ const vue = new Vue({
|
||||
this.deal.ClosePercent);
|
||||
},
|
||||
refreshTradingFeeByUnit() {
|
||||
const allocatedFee = swapPosiFeeCalc.calcAllocatedTradingFee(
|
||||
this.floatPosition.BeforeCloseFee,
|
||||
this.floatPosition.PosiFeeType,
|
||||
this.floatPosition.PosiTradingFeeUnit,
|
||||
this.deal.CloseNotionalValue,
|
||||
this.deal.CloseQty,
|
||||
this.deal.NotionalValue,
|
||||
this.deal.NotionalQty);
|
||||
this.floatPosition.TradingFee = allocatedFee !== null ? allocatedFee : swapPosiFeeCalc.calcTradingFee(
|
||||
this.floatPosition.TradingFee = swapPosiFeeCalc.calcTradingFee(
|
||||
this.floatPosition.PosiFeeType,
|
||||
this.floatPosition.PosiTradingFeeUnit,
|
||||
this.deal.CloseNotionalValue,
|
||||
@@ -241,7 +244,7 @@ const vue = new Vue({
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
this.calcTradingFeePending();
|
||||
this.refreshTradingFeeByUnit();
|
||||
this.getInterestList();
|
||||
@@ -255,7 +258,7 @@ const vue = new Vue({
|
||||
}
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
this.deal.CloseNotionalValue = formatSwapAmount(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
if (parseFloat(this.deal.ClosePercent) == parseFloat(this.oriClosePercent)) {
|
||||
this.deal.CloseMethod = 1;
|
||||
} else {
|
||||
@@ -309,7 +312,7 @@ const vue = new Vue({
|
||||
let deliveryPrice = thisObj.getStorageDeliveryPrice();
|
||||
thisObj.floatPosition.MarkClosePnl = Math.round(thisObj.deal.CloseQty * (deliveryPrice - thisObj.initPosiNetPrice) * floatRatio * longRatio * 10000) / 10000;
|
||||
thisObj.floatPosition.MarkClosePnl = Number(thisObj.floatPosition.MarkClosePnl.toFixed(2));//MarkClosePnl 纯盯市不要计算交易费用和分红
|
||||
thisObj.floatPosition.MarkClosePnl = otcformat.trading.StockEqvNotional(thisObj.floatPosition.MarkClosePnl);
|
||||
thisObj.floatPosition.MarkClosePnl = formatSwapAmount(thisObj.floatPosition.MarkClosePnl);
|
||||
thisObj.floatPosition.FloatPnlSum = (parseFloat(thisObj.floatPosition.MarkClosePnl) + TradingFee + TradingFeePending + parseFloat(thisObj.floatPosition.DividendIn)).toFixed(2);
|
||||
thisObj.calcCloseAmount();
|
||||
|
||||
@@ -319,12 +322,12 @@ const vue = new Vue({
|
||||
},
|
||||
changeInterestAmount(item) {//修改利息金额
|
||||
let interestRatio = item.InterestDirection == 1 ? 1 : -1;
|
||||
item.InterestClosePnL = otcformat.trading.StockEqvNotional(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee) * interestRatio);
|
||||
item.InterestClosePnL = formatSwapAmount(parseFloat(item.InterestAmount) * interestRatio + parseFloat(item.InterestFee) * interestRatio);
|
||||
this.calcCloseAmount();
|
||||
},
|
||||
//calcClosePnL() {//计算浮动端平仓盈亏
|
||||
// let pnl = parseFloat(this.floatPosition.ClosePnL) - parseFloat(this.floatPosition.TradingFee);
|
||||
// this.floatPosition.ClosePnL = otcformat.trading.StockEqvNotional(pnl);
|
||||
// this.floatPosition.ClosePnL = formatSwapAmount(pnl);
|
||||
// this.calcCloseAmount();
|
||||
//},
|
||||
calcCloseAmount() {//计算平仓总额=浮动收取+利息收取-浮动支付-利息支付
|
||||
@@ -361,10 +364,10 @@ const vue = new Vue({
|
||||
});
|
||||
thisObj.deal.SwapRealizedPnL = Number(thisObj.deal.SwapRealizedPnL.toFixed(2));
|
||||
thisObj.deal.SwapCloseAmount = Number(thisObj.deal.SwapCloseAmount.toFixed(2));
|
||||
thisObj.deal.SwapCloseAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = otcformat.trading.StockEqvNotional(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = otcformat.trading.StockEqvNotional(thisObj.deal.SwapMarginAmount);
|
||||
thisObj.deal.SwapCloseAmount = formatSwapAmount(thisObj.deal.SwapCloseAmount);
|
||||
thisObj.deal.SwapRealizedPnL = formatSwapAmount(thisObj.deal.SwapRealizedPnL);
|
||||
thisObj.deal.SwapMarginRebatePnl = formatSwapAmount(thisObj.deal.SwapMarginRebatePnl);
|
||||
thisObj.deal.SwapMarginAmount = formatSwapAmount(thisObj.deal.SwapMarginAmount);
|
||||
},
|
||||
getInterestList() {//根据平仓日期获取利息腿信息
|
||||
var thisObj = this;
|
||||
@@ -384,27 +387,15 @@ const vue = new Vue({
|
||||
},
|
||||
getDivindIn() {
|
||||
var thisObj = this;
|
||||
let ratio = this.floatPosition.PositionType == 1 ? 1 : -1;
|
||||
let floatRatio = this.floatPosition.PayDirection == 1 ? 1 : -1;
|
||||
var postData = { startDate: thisObj.TradeStartDate, endDate: thisObj.deal.UnwindDate, underlyingCode: thisObj.floatPosition.UnderlyingCode, tradeId: thisObj.deal.SwapTradeId, unwindDate: thisObj.deal.UnwindDate }
|
||||
main.post("/BondPayment/GetBondPayMentInterest", postData, { async: false }).done(function (resp) {
|
||||
let consumedDividend = Math.abs(parseFloat(resp.obj.consumedDividend ?? 0)) * ratio * floatRatio;//已实现的
|
||||
let totalDividend = parseFloat(thisObj.deal.NotionalQty) * resp.obj.totalInterest * ratio * floatRatio;
|
||||
let remainDividend = totalDividend - consumedDividend;
|
||||
let dividendIn = 0;
|
||||
if (remainDividend != 0) {
|
||||
dividendIn = parseFloat(thisObj.deal.CloseQty) / parseFloat(thisObj.floatPosition.Quantity) * remainDividend
|
||||
}
|
||||
// 这里计算已经分红的利息,从totalInterest里扣除
|
||||
|
||||
// 互换是全量消费,consumedDividend>0 表示分红已被当天互换消费,归0
|
||||
// 现在可能做了纯分红的互换结算,所以不能直接归0
|
||||
thisObj.floatPosition.DividendIn = parseFloat(dividendIn.toFixed(2));
|
||||
var posiQty = parseFloat(thisObj.floatPosition.Quantity) - parseFloat(thisObj.deal.CloseQty);
|
||||
thisObj.floatPosition.DividendPending = parseFloat((posiQty * resp.obj.totalInterest * ratio * floatRatio).toFixed(2));
|
||||
thisObj.calcFloatClosePnl();
|
||||
thisObj.dataFormat();
|
||||
});
|
||||
// 方案C:分红改由后端 InitUnwind 读 EOD PosiDividendSum 填入 floatPosition.DividendIn 与 DividendPending
|
||||
// (单一可信源)。前端不再调用 GetBondPayMentInterest 自算——消除"期初持仓×totalInterest"对已平仓
|
||||
// 部分的重复计入(GLMS-20260105-0004 平仓前部分平仓40%后,再平仓时分红误显 -36,160,应为 0)。
|
||||
// ⚠ floatPosition.DividendIn / DividendPending 均保持后端返回值不动,前端不得覆盖:
|
||||
// - DividendIn(本次落袋)、DividendPending(待结算存量=PosiDividendSum 全量口径)。
|
||||
// - 互换页 DividendPending 保持 0(互换=全量结清,结清后待结算归0),见 incomeSwapTrade.js。
|
||||
// - 历史:曾硬编码 DividendPending=0,对 PosiDividendSum≠0 的部分平仓会落库错误的 0(回归)。
|
||||
thisObj.calcFloatClosePnl();
|
||||
thisObj.dataFormat();
|
||||
},
|
||||
closeTrade() {//平仓
|
||||
var thisObj = this;
|
||||
|
||||
@@ -297,6 +297,8 @@
|
||||
!options.append && (options.append = '');
|
||||
let precision = parseInt(options.precision) || 0;
|
||||
options.precision = precision < 0 ? 0 : precision;
|
||||
let integerDigits = parseInt(options.integerDigits) || 0;
|
||||
options.integerDigits = integerDigits > 0 ? integerDigits : 0;
|
||||
options.negative = !!options.negative;
|
||||
_options = options;
|
||||
setValue(getValue());
|
||||
@@ -336,6 +338,35 @@
|
||||
return this.value.substring(0, this.selectionStart).indexOf('.') < 0 && this.value.substring(this.selectionEnd, this.value.length).indexOf('.') < 0;
|
||||
}
|
||||
|
||||
function checkIntegerDigitsInput() {
|
||||
if (!_options.integerDigits) return true;
|
||||
let valueEnd = this.value.length;
|
||||
if (_options.append && this.value.endsWith(_options.append)) valueEnd -= _options.append.length;
|
||||
const dotIndex = this.value.indexOf('.');
|
||||
const integerEnd = dotIndex < 0 ? valueEnd : dotIndex;
|
||||
if (this.selectionStart > integerEnd) return true;
|
||||
const selectionEnd = Math.min(this.selectionEnd, integerEnd);
|
||||
const integerText = this.value.substring(0, this.selectionStart) + this.value.substring(selectionEnd, integerEnd);
|
||||
return integerText.replace(/\D/g, '').length < _options.integerDigits;
|
||||
}
|
||||
|
||||
function limitIntegerDigits(value) {
|
||||
if (!_options.integerDigits) return value;
|
||||
let digits = 0;
|
||||
let hasDot = false;
|
||||
let result = '';
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const ch = value.charAt(index);
|
||||
if (ch === '.' || ch === '。') hasDot = true;
|
||||
if (ch >= '0' && ch <= '9' && !hasDot) {
|
||||
if (digits >= _options.integerDigits) continue;
|
||||
digits++;
|
||||
}
|
||||
result += ch;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function __keyHandle(event) {
|
||||
if (!event) return false;
|
||||
|
||||
@@ -385,6 +416,7 @@
|
||||
if (charCode < 48 || charCode > 57 && (charCode < 96 || charCode > 105)) {
|
||||
return false;
|
||||
}
|
||||
if (!checkIntegerDigitsInput.call(this)) return false;
|
||||
if (this.selectionStart === 0) {
|
||||
return !this.value || this.value.charAt(this.selectionEnd) !== '-';
|
||||
}
|
||||
@@ -445,6 +477,7 @@
|
||||
// 粘贴进来的是显示值(可能带 %/‱ 后缀),先按 __change 同款口径解析为模型值再回显,
|
||||
// 避免 setValue 把显示值再乘以 100/10000(#EQD-5914 债券价格粘贴 ×100)
|
||||
let f = '';
|
||||
this.value = limitIntegerDigits(this.value);
|
||||
if (this.value && this.value !== _options.append) {
|
||||
f = parseFloat(this.value.replaceAll(",", "")) || 0;
|
||||
if (_options.append === '%' || _options.percent == true) f /= 100;
|
||||
@@ -455,6 +488,7 @@
|
||||
if (_chnInput >= 0) {
|
||||
__onChineseInput.call(this, _chnInput);
|
||||
}
|
||||
this.value = limitIntegerDigits(this.value);
|
||||
if (!_options.append || !this.value) return;
|
||||
let appended = true;
|
||||
if (this.value !== _options.append) {
|
||||
|
||||
@@ -14354,6 +14354,8 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
|
||||
!options.append && (options.append = '');
|
||||
let precision = parseInt(options.precision) || 0;
|
||||
options.precision = precision < 0 ? 0 : precision;
|
||||
let integerDigits = parseInt(options.integerDigits) || 0;
|
||||
options.integerDigits = integerDigits > 0 ? integerDigits : 0;
|
||||
options.negative = !!options.negative;
|
||||
_options = options;
|
||||
setValue(getValue());
|
||||
@@ -14393,6 +14395,35 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
|
||||
return this.value.substring(0, this.selectionStart).indexOf('.') < 0 && this.value.substring(this.selectionEnd, this.value.length).indexOf('.') < 0;
|
||||
}
|
||||
|
||||
function checkIntegerDigitsInput() {
|
||||
if (!_options.integerDigits) return true;
|
||||
let valueEnd = this.value.length;
|
||||
if (_options.append && this.value.endsWith(_options.append)) valueEnd -= _options.append.length;
|
||||
const dotIndex = this.value.indexOf('.');
|
||||
const integerEnd = dotIndex < 0 ? valueEnd : dotIndex;
|
||||
if (this.selectionStart > integerEnd) return true;
|
||||
const selectionEnd = Math.min(this.selectionEnd, integerEnd);
|
||||
const integerText = this.value.substring(0, this.selectionStart) + this.value.substring(selectionEnd, integerEnd);
|
||||
return integerText.replace(/\D/g, '').length < _options.integerDigits;
|
||||
}
|
||||
|
||||
function limitIntegerDigits(value) {
|
||||
if (!_options.integerDigits) return value;
|
||||
let digits = 0;
|
||||
let hasDot = false;
|
||||
let result = '';
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const ch = value.charAt(index);
|
||||
if (ch === '.' || ch === '。') hasDot = true;
|
||||
if (ch >= '0' && ch <= '9' && !hasDot) {
|
||||
if (digits >= _options.integerDigits) continue;
|
||||
digits++;
|
||||
}
|
||||
result += ch;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function __keyHandle(event) {
|
||||
if (!event) return false;
|
||||
|
||||
@@ -14442,6 +14473,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
|
||||
if (charCode < 48 || charCode > 57 && (charCode < 96 || charCode > 105)) {
|
||||
return false;
|
||||
}
|
||||
if (!checkIntegerDigitsInput.call(this)) return false;
|
||||
if (this.selectionStart === 0) {
|
||||
return !this.value || this.value.charAt(this.selectionEnd) !== '-';
|
||||
}
|
||||
@@ -14502,6 +14534,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
|
||||
// 粘贴进来的是显示值(可能带 %/‱ 后缀),先按 __change 同款口径解析为模型值再回显,
|
||||
// 避免 setValue 把显示值再乘以 100/10000(#EQD-5914 债券价格粘贴 ×100)
|
||||
let f = '';
|
||||
this.value = limitIntegerDigits(this.value);
|
||||
if (this.value && this.value !== _options.append) {
|
||||
f = parseFloat(this.value.replaceAll(",", "")) || 0;
|
||||
if (_options.append === '%' || _options.percent == true) f /= 100;
|
||||
@@ -14512,6 +14545,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
|
||||
if (_chnInput >= 0) {
|
||||
__onChineseInput.call(this, _chnInput);
|
||||
}
|
||||
this.value = limitIntegerDigits(this.value);
|
||||
if (!_options.append || !this.value) return;
|
||||
let appended = true;
|
||||
if (this.value !== _options.append) {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
# 互换(TRS)计算口径一致性:残留问题与治理路线
|
||||
|
||||
> 配套文档:`互换分红损益字段语义与重复计算分析.md`(2026-06,根因分析)
|
||||
> 本次分析目的:在最新提交 `e3c473ba`(分红收益改由 EOD 单一可信源)之后,
|
||||
> 复核"分散计算 / 前后端重复算 / 精度口径不统一"这类 BUG 是否仍在其它地方存在,
|
||||
> 并给出"如何避免 + 后续逐渐解决"的路线。
|
||||
> 分析日期:2026-08-06
|
||||
|
||||
---
|
||||
|
||||
## 一、这类 BUG 的本质(统一定义)
|
||||
|
||||
最新修复的"分红收益误显 -36,160",根因不是某一个 if 写错,而是一种**结构性缺陷**:
|
||||
|
||||
> **金融计算缺乏「单一可信源 / 统一口径」** —— 同一个业务量在
|
||||
> **前端 JS / 后端 C# / EOD 批处理** 三条路径里被**各自重算**,
|
||||
> 且价格基准、方向符号、精度位数都零散硬编码,导致:
|
||||
> 1. 前后端(或同一功能的两页)对同一量算出**不同值**;
|
||||
> 2. 分红等分量被**重复计入**(MarkClosePnl 含分红 → 汇总翻倍);
|
||||
> 3. 金额/价格**精度不统一**,相邻环节差 1~2 位小数。
|
||||
|
||||
它表现为 3 个子模式:
|
||||
|
||||
| 子模式 | 例子 | 状态 |
|
||||
|--------|------|------|
|
||||
| ① 分散计算、无单一可信源 | 分红前端自算(`getDivindIn`)vs EOD `PosiDividendSum` | **最新提交已根治(方案C)** |
|
||||
| ② 字段语义重叠→重复累加 | `MarkClosePnl` 含分红,汇总 `RealizedPnl` 翻倍 | 自动路径已按方向A改造;**前端两页仍有口径差** |
|
||||
| ③ 硬编码精度/魔法数字 | `Math.Round(...,4/10)`、`ToString("F10")` vs `ConsGlobal.*` | **大量残留** |
|
||||
|
||||
---
|
||||
|
||||
## 二、最新提交(e3c473ba)做了什么(已修复)
|
||||
|
||||
- 把"浮动端平仓盈亏·分红 `DividendIn`"从**前端 `getDivindIn` 自算**(`期初持仓×totalInterest`)
|
||||
改为**后端 `SwapDealService.GetPreEodDividendSum` 读 EOD `PosiDividendSum`**(单一可信源)。
|
||||
- 前端 `unwindSwapTrade.js` / `incomeSwapTrade.js` 的 `getDivindIn` 删除自算逻辑,只保留后端值。
|
||||
- `GetBondPayMentInterest` 标记废弃(保留接口供历史调用)。
|
||||
- 配套绿灯验收测试 `GLMS20260105PartialCloseDividendBugTest.cs`。
|
||||
|
||||
**结论**:子模式①(分红分散计算)在"预览页展示"这条链路已根治。但**②④③ 仍在**。
|
||||
|
||||
---
|
||||
|
||||
## 三、同类问题在别处是否还存在(带 file:line 证据)
|
||||
|
||||
### A. 前端两页 `MarkClosePnl` 口径不一致 —— 最危险、最具体的残留 ⚠️
|
||||
|
||||
> **2026-08-06 修正**:初版称"平仓页用净价、互换页用全价"——经核对代码**不准确**。
|
||||
> 实际两页都用**全价**:`unwindSwapTrade.js:111` 把 `initPosiNetPrice` 绑到 `PosiGrossPrice`(变量名有误导性),
|
||||
> `incomeSwapTrade.js:103/207` 用 `initPosiGrossPrice`(也是全价)。真正差异见下表与 A.1。
|
||||
|
||||
同一字段 `MarkClosePnl`(盯市盈亏),实现对照(来源:团队金标准 `YLErpDAL/Helpers/FrontendCalcReference.cs`):
|
||||
|
||||
| 实现 | 位置 | 公式 | 价格基准 | 方向 | 数量基准 |
|
||||
|------|------|------|----------|------|----------|
|
||||
| 平仓页 | `unwindSwapTrade.js:313` | `CloseQty × (价 − 期初全价) × floatRatio × longRatio` | 全价(变量名骗人) | 含 `longRatio` | `CloseQty` |
|
||||
| 互换页 | `incomeSwapTrade.js:207` | `positionAmount × (价 − 期初全价) × floatRatio` | 全价 | **无 `longRatio`** | `positionAmount=PositionQty×ContractSize` |
|
||||
| 后端 | `SwapDealService.cs:1613` | `(价 − PosiGrossPrice) × unwindQty × floatRatio × longRatio` | 全价 | 含 `longRatio` | `unwindQty` |
|
||||
| 金标准 | `FrontendCalcReference.CalcUnwind:44` / `CalcIncome:107` | 同上(`CalcIncome` 注释明示"无 longRatio") | 全价 | unwind 有 / income 无 | — |
|
||||
|
||||
**真正的风险点(已用红测试证明,见 A.1):**
|
||||
1. **互换页缺 `longRatio`(多空方向)** —— 这是实打实的 bug。一旦 `PositionType=2`(空头),
|
||||
unwind 页与后端会乘 `−1`,而 income 页不乘 → **同一笔空头两页算出相反符号**。
|
||||
债券 TRS(国联民生等)普遍支持空头,并非边缘场景。
|
||||
2. **数量基准不同**(设计使然,非 bug):平仓页用 `CloseQty`(本次平仓量),互换页用 `PositionQty×ContractSize`
|
||||
(剩余持仓量,因互换是全额置换剩余持仓)。语义不同但各自自洽,需业务确认是否期望一致。
|
||||
3. **中间取整**:income 无 `×10000/10000` 步骤(`FrontendCalcReference` 注释明示),但末端都保留 2 位,
|
||||
干净输入下等价(parity 已证明),属低风险。
|
||||
|
||||
**共享的 `swapCalc.calcMarkClosePnl`(`swapCalc.js:113`,全价+`longRatio`+`scale`)已存在,
|
||||
但两个生产页面都没调用**——仍是"单一可信源已有却不采纳"。
|
||||
|
||||
**会进库吗?** 会。手动平仓/互换的 `MarkClosePnl` 由前端算好传入,后端**直接存库不重算**
|
||||
(`SwapDealService:1500/1991` 的 `ValidateFrontendPnL` 仅"只读告警、不阻断")。
|
||||
因此 income 页空头符号错误会直接落到 `swap_flow_event.MarkClosePnl`,并带偏 `FloatPnlSum`/`SwapRealizedPnL`。
|
||||
|
||||
### A.1 为什么一直没暴露?—— 红测试证据(2026-08-06 补)
|
||||
|
||||
三层叠加导致这个 bug 长期潜伏:
|
||||
1. **校验同源、永远自洽**:`ValidateFrontendPnL` 用 `FrontendCalcReference.CalcIncome` 重算比对,
|
||||
而 `CalcIncome` 本身就没 `longRatio`,所以"被校验的前端"和"校验用的公式"完全一致,永远不告警。
|
||||
2. **特征化测试全是多头**:`FrontendCalcCharacterizationTest` 的 income 场景 `FC_006~009`
|
||||
**全部 `PositionType=1`(多头)**;唯一空头场景 `FC_005` 是 unwind。income 的空头分支从未被触发。
|
||||
3. **告警不阻断** + 生产数据里"空头做互换"相对少见,进一步降低暴露概率。
|
||||
|
||||
**已落地红测试(证明 bug 真实存在 + 证明改动可修复):**
|
||||
- `YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js`(jest,与 `parity.test.js` 同风格)
|
||||
- `YLErpWeb/fe-tests/_proof_short_income.js`(纯 Node 可跑,无需依赖)
|
||||
- 运行结果:多头场景两页一致(PASS,掩盖了问题);**空头场景 `unwind=−300` / `income=+300`(FAIL,符号相反)**;
|
||||
给 income 补 `longRatio` 后 `income=−300` 与 unwind 一致(PASS)。
|
||||
→ 既证明"当前代码对空头不一致(有问题)",也证明"给 income 补 `longRatio` 即可修复"。
|
||||
|
||||
### B. 后端硬编码精度魔法数字 —— 与集中常量冲突/不绑定的残留 ⚠️
|
||||
|
||||
集中常量(`Framework/YLErp.Core/ConsGlobal.cs`):`PriceRound=11`、`SwapDeliveryPriceRound=9`、
|
||||
`MoneyRound=2`;模块内 `InterestCalculationPrecision=12`、`EodInterestStoragePrecision=12`。
|
||||
|
||||
残留的裸数字(不引用上述常量):
|
||||
|
||||
| 位置 | 写法 | 应参照 | 问题 |
|
||||
|------|------|--------|------|
|
||||
| `SwapFlowService.cs:116-118` | `Math.Round(price, 4)`(ClosePrice/SettlePrice/ReferencePrice) | `PriceRound=11` / `SwapDeliveryPriceRound=9` | 价格存储 4 位 vs 全系统 9~11 位,**口径不一致** |
|
||||
| `SwapDealService.cs:1543` | `unwindPriceFee.ToString("F10")` | — | 硬编码 10 位 |
|
||||
| `SwapTradeAutoService.cs:458/460` | `Math.Round(..., 10)`(净均价) | `PriceRound=11` | 差 1 位 |
|
||||
| `SwapTradeAutoService.cs:788/1409` | `Math.Round(..., 4)`(费用/平仓费) | `MoneyRound=2` | **费用 4 位 vs 全系统金额 2 位,错配** |
|
||||
| 分红链路多处 `Math.Round(...,2)`:`SwapEodPositionService.cs:1647/1711/1713/2003`、`SwapDealService.cs:1658/1882/1883` | 裸 `2` | `ConsGlobal.MoneyRound` | 目前恰等于 2,但属硬编码,`MoneyRound` 一旦配置化即失真 |
|
||||
|
||||
说明:第 4 行(费用 4 位 vs 金额 2 位)是**真实精度错配**,不是巧合一致;其余价格/净均价是"差 1 位"的隐患。
|
||||
|
||||
### C. 交叉校验基础设施已建,但未"落地到生产" —— 治理杠杆闲置
|
||||
|
||||
- `swapCalc.js` 的 `calcUnwind` / `calcIncome`(`:135`/`:170`)是**"参考规格,未接入生产代码"**——
|
||||
注释明确写着生产 Vue 只调用 4 个叶子函数,聚合逻辑仍是各页内联。
|
||||
- `fe-tests/parity.test.js` + `swapCalc.test.js` 已能冻结 8 个 FC 场景,但只覆盖已迁移的 4 个叶子函数。
|
||||
- 这正是"避免反复打补丁"的关键设施,**却没把生产聚合逻辑迁过去**。
|
||||
|
||||
### D. 横向同类风险(建议扫描,本次未深入)
|
||||
|
||||
互换之外的 期货 / 期权 / 定价引擎(greeks)/ 估值报告 等模块,
|
||||
很可能也存在"前端重算 + 后端重算""硬编码精度"的同构问题,需专项扫描。
|
||||
|
||||
---
|
||||
|
||||
## 四、如何避免(规范 / 治理层)
|
||||
|
||||
1. **单一可信源原则(最高优先级)**
|
||||
- 每个金融量只允许**一个权威计算点**:分红→EOD `PosiDividendSum`;盯市→`swapCalc.calcMarkClosePnl`;
|
||||
金额聚合→`swapCalc.calcFloatPnlSum` / `calcUnwind` / `calcIncome`。
|
||||
- 前端**只展示、不重算**;手动平仓/互换落库时由后端**重算**而非信任前端传值。
|
||||
2. **精度集中化(红线)**
|
||||
- 任何 `Math.Round` / `ToString("F")` 必须引用 `ConsGlobal.*` 或 `GetStorageDeliveryPriceRound(underlyingInstrumentType, code)`,
|
||||
**禁止裸数字**。代码评审把"裸精度数字"列为 blocking 项。
|
||||
3. **字段语义单一职责(已定义,需固化)**
|
||||
- `MarkClosePnl`=纯价差盯市、`DividendIn`=纯分红、`RealizedPnl`=不重叠分量之和。
|
||||
- 写进评审清单 + 用集成测试守护。
|
||||
4. **自动守护测试(把已建设施用起来)**
|
||||
- 扩展 `parity.test.js` 的"生产表达式逐字抄录 vs `SwapCalc`"模式到**所有关键公式**;
|
||||
- 后端补"分红守恒 / 持仓守恒"集成测试:`ΔSwapPositionValue + ΔRealizedPnl == 0`
|
||||
(参考分析文档附录 A.2 的 SQL 思路,转成 `SwapEodPositionServiceIntegrationTest` 断言)。
|
||||
5. **防回归**:将 `swapCalc.calcUnwind/calcIncome` 接入生产,删除两页内联实现。
|
||||
|
||||
---
|
||||
|
||||
## 五、后续逐渐解决的路线(分阶段、低风险)
|
||||
|
||||
- **阶段 0(已具备)**:`parity.test.js` / `swapCalc.test.js` / `GLMS20260105PartialCloseDividendBugTest.cs` 框架。
|
||||
- **阶段 1(低风险、先消除最危险口径)**:
|
||||
让 `incomeSwapTrade.js:207` 与 `unwindSwapTrade.js:313` 统一调用 `swapCalc.calcMarkClosePnl`
|
||||
(全价 + `longRatio` + `scale`),并确认与后端 `PosiGrossPrice` 口径一致;
|
||||
新增"净价≠全价""空头 longRatio"两个 parity 场景守护。**改动小、收益高。**
|
||||
- **阶段 2(消除魔法数字)**:
|
||||
把 `SwapFlowService` / `SwapTradeAutoService` / `SwapDealService` 里的裸 `Math.Round(...,4/10)`、
|
||||
`ToString("F10")` 替换为 `ConsGlobal.*` / `GetStorageDeliveryPriceRound`;分红的裸 `2` 改为 `ConsGlobal.MoneyRound`。
|
||||
逐文件改,每改一处跑 parity + 现有单测。
|
||||
- **阶段 3(后端收口)**:
|
||||
手动平仓/互换落库时,后端对 `MarkClosePnl` / `FloatPnlSum` / `SwapRealizedPnL` **重算**(与 `swapCalc` 金标准一致),
|
||||
不再信任前端;同时跑"持仓守恒"SQL 校验历史数据是否已被两页口径差污染。
|
||||
- **阶段 4(横向扫描)**:
|
||||
用脚本/Explore 扫描 期货、期权、定价引擎、估值报告 模块,查找"前端重算+后端重算""硬编码精度"同类结构,建立清单逐个治理。
|
||||
- **阶段 5(文档固化)**:
|
||||
更新 `互换分红损益字段语义与重复计算分析.md`,把"已修复 / 待修复"状态机化,作为新人评审清单与回归基线。
|
||||
|
||||
---
|
||||
|
||||
## 六、一句话结论
|
||||
|
||||
> 最新修复根治了"分红预览"这一条链路的分散计算;但**同类结构依然存在**——
|
||||
> 前端平仓页/互换页的 `MarkClosePnl` 都用全价,真正的差异是**互换页(income)漏乘 `longRatio`(多空方向)**,
|
||||
> 对空头会算出相反符号并直接落库(后端对 income 不重算、原样存前端值);共享的 `swapCalc.calcMarkClosePnl` 两个页面都没用;
|
||||
> 后端仍有**费用 4 位 vs 金额 2 位**等硬编码精度错配。
|
||||
> 治理的关键不是再打补丁,而是**把已建好的 `swapCalc` 单一可信源 + parity 守护真正接入生产**,
|
||||
> 并按上述 5 个阶段低风险推进。
|
||||
|
||||
---
|
||||
|
||||
## 七、如何确认"income 错 / unwind 对"(而非相反)+ 改动安全性
|
||||
|
||||
> 这一章回答一个关键质疑:两页口径不同,凭什么断定是 income 漏了 `longRatio`、而不是 unwind 多算了?
|
||||
> 以及:给 income 补 `longRatio` 会不会把正确逻辑改坏、或造成"双重翻转"?
|
||||
|
||||
### 7.1 两个方向乘子是**独立轴**(这是避免误判的前提)
|
||||
|
||||
- `floatRatio = PayDirection==1(收取) ? +1 : -1` —— 跟随**收付方向**(`FrontendCalcReference.cs:35`)
|
||||
- `longRatio = PositionType==1(多头) ? +1 : -1` —— 跟随**多空方向**(`FrontendCalcReference.cs:36`)
|
||||
|
||||
二者在本系统里**互不决定**:一个"空头"完全可以 `PayDirection=收取`。
|
||||
|
||||
### 7.2 裁决性证据:FC_005(空头平仓)冻结基线
|
||||
|
||||
`UnitTestProject/.../FrontendCalcCharacterizationTest.cs` 的 `FC_005`:
|
||||
|
||||
```
|
||||
输入:PositionType=2(空头) + PayDirection=1(收取)
|
||||
注释:floatRatio=1(收取), longRatio=-1(空头)
|
||||
期望:MarkClosePnl = 1000×(105−100)×1×(−1) = −5000 (空头涨价=亏损,经济正确)
|
||||
```
|
||||
|
||||
**它证明两件事**:
|
||||
1. 空头下 `floatRatio` 仍是 **+1**(方向不靠 `floatRatio` 编码)→ 所以 income 只乘 `floatRatio(+1)` 而漏 `longRatio(-1)`,
|
||||
对同一个空头会算出 **+5000**,与权威 −5000 相反 → **income 错、unwind 对**。
|
||||
2. 因为两轴独立,**给 income 补 `longRatio` 是纠正、不是双重翻转**("floatRatio 已编码方向"的担忧不成立)。
|
||||
|
||||
### 7.3 三条互相独立的证据链(任一都足以定罪)
|
||||
|
||||
| # | 证据 | 来源 | 结论 |
|
||||
|---|------|------|------|
|
||||
| 1 | 盯市盈亏空头必须翻转符号(空头跌价才盈利)——会计不变式 | 业务数学,独立于代码 | income 漏方向乘子→空头符号必错 |
|
||||
| 2 | 后端平仓结算 `:1620` 重算并**覆写** `MarkClosePnl=…*floatRatio*longRatio`(入库存后端口径);`SwapIncome:1980-2013` **不重算**、原样存前端值 | `SwapDealService.cs` | 系统自身的权威定义含 longRatio,income 偏离它 |
|
||||
| 3 | `SwapIncome:1994` 用前端 `SwapRealizedPnl`(由 `MarkClosePnl` 派生)做 `AddClientCash(-SwapRealizedPnl)` | `SwapDealService.cs` | 空头 income 不仅显示错,**实际现金流方向也错**(非纯展示) |
|
||||
|
||||
### 7.4 下游是否会"双重翻转 / 补偿性 hack"?
|
||||
|
||||
- `SwapFlowEventService.cs:589` 对所有流水事件的 `MarkClosePnl/DividendIn/CloseFee/...` **统一取反**(全局视角翻转,firm book→client view),
|
||||
**不针对 income 或空头**。修复后 income 与 unwind 走同一套取反,对称性不变 → 安全。
|
||||
- 全仓检索仅此一处 `MarkClosePnl=-MarkClosePnl`,无针对 income/空头的补偿性符号翻转 → 不存在"加了 longRatio 反而翻错"的 hack。
|
||||
|
||||
### 7.5 改动安全性论证(如何确保不影响现有正确逻辑)
|
||||
|
||||
修复 = 给 income 的 `MarkClosePnl`(含 `FrontendCalcReference.CalcIncome:107`)补 `× longRatio`,使其与权威 unwind 公式口径一致。
|
||||
|
||||
1. **静态保证(零回归)**:对所有**多头**(现有 `FC_006~009` 及全部生产多头 income)`longRatio=+1`,
|
||||
乘积不变 → 数值**逐位相同**,现有正确行为完全不动。改动是"对多头的恒等变换 + 对空头的纠错",是严格的超集。
|
||||
2. **回归护栏**:
|
||||
- 跑现有 `FC_001~009` + `SwapFrontendPnlValidateTest` + `SwapIncomeScenarioTest` → 全绿(均为多头,不受影响)。
|
||||
- 新增 `FC_010`(空头 income,`PayDirection=1, PositionType=2`)镜像 `FC_005`,锁定纠正后行为,并断言 `income(空头)==unwind(空头)`(parity)。
|
||||
- jest 红测试 `markClosePnlShortConsistency.test.js` 的"空头"用例由 FAIL 转 PASS,多头用例保持 PASS。
|
||||
3. **剩余风险(非逻辑正确性,需业务决策)**:
|
||||
- **历史脏数据**:过去"空头+互换"事件已用错符号落库;修复后新事件正确,跨时间对比会出现不连续。需决定:回溯校正(改 `MarkClosePnl`+重算 `SwapRealizedPnl`+对账 `AddClientCash` 历史)还是标注留痕。
|
||||
- **改动范围**:严格限定在 income 页 `:207` 与 `CalcIncome:107` 补 `longRatio`,切忌顺手改 `floatRatio` 或其他页面。
|
||||
4. **前置确认**:建议先让业务/量化签字"income 的 `MarkClosePnl` 应含多空方向(与平仓一致)",再动手——因为结论虽由代码+基线铁证支撑,但涉及客户现金流,需业务背书。
|
||||
Reference in New Issue
Block a user