Merge remote-tracking branch 'dest/glms/feature/1.4.2' into feature/p132_74-risk-engine
This commit is contained in:
@@ -228,6 +228,16 @@ namespace YLErp.DBModels
|
||||
[NotMapped]
|
||||
public decimal BeforeCloseFee { get; set; }
|
||||
/// <summary>
|
||||
/// 基础费率(仅前端展示,不存库)
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public decimal PosiTradingFeeUnit { get; set; }
|
||||
/// <summary>
|
||||
/// 基础费率模式 0=百分比 1=单位数量(仅前端展示,不存库)
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public int PosiFeeType { get; set; }
|
||||
/// <summary>
|
||||
/// 持仓腿id
|
||||
/// </summary>
|
||||
[DisplayName("持仓腿id")]
|
||||
|
||||
@@ -113,6 +113,11 @@ namespace YLErp.DBModels
|
||||
[DataChange]
|
||||
public decimal PosiTradingFeeUnit { get; set; }
|
||||
/// <summary>
|
||||
/// 单位交易费用模式 0=百分比 1=单位数量
|
||||
/// </summary>
|
||||
[DataChange]
|
||||
public int PosiFeeType { get; set; }
|
||||
/// <summary>
|
||||
/// 起始日
|
||||
/// </summary>
|
||||
[DisplayName("起始日")]
|
||||
@@ -246,6 +251,10 @@ namespace YLErp.DBModels
|
||||
/// </summary>
|
||||
public int? interest_rule { get; set; }
|
||||
/// <summary>
|
||||
/// 利息端类别
|
||||
/// </summary>
|
||||
public string category_tag { get; set; }
|
||||
/// <summary>
|
||||
/// 互换观察日集合
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<dictionary name="业务部门编号" catalog="客户"></dictionary>
|
||||
<dictionary name="自定义交易要素" catalog="交易"></dictionary>
|
||||
<dictionary name="自定义结构类型" catalog="交易"></dictionary>
|
||||
<dictionary name="保证金模板名称" catalog="交易"></dictionary>
|
||||
<dictionary name="实际控制主体" catalog="客户"></dictionary>
|
||||
<dictionary name="银行信用评级" catalog="客户"></dictionary>
|
||||
<dictionary name="指数类型" catalog="客户"></dictionary>
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -241,11 +241,21 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
|
||||
dic["参考标的期初全价%"] = ((double)swapPosition.PosiGrossPrice * 100).ToString("N4");
|
||||
dic["参考标的期初净价%"] = ((double)(swapPosition.PosiNetNoFeePrice ?? 0m) * 100).ToString("N4");
|
||||
|
||||
// 固定收益率(年化)- 债券期初到期收益率
|
||||
//dic["固定收益率(年化)"] = swapPosition.InitYtm.HasValue
|
||||
// ? ((double)swapPosition.InitYtm.Value * 100).ToString("N4")
|
||||
// : "0.0000";
|
||||
dic["固定收益率(年化)"] = "0.0000"; //需求说直接都是0
|
||||
// 固定收益率(年化)- ETF默认取"增强收益"腿的计息利率
|
||||
bool isEtf = IsBondEtf(underlying?.UnderlyingCode ?? string.Empty);
|
||||
if (isEtf)
|
||||
{
|
||||
var enhancePosition = swapPositions
|
||||
.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode) && x.category_tag == "增强收益")
|
||||
.FirstOrDefault();
|
||||
dic["固定收益率"] = enhancePosition != null
|
||||
? ((double)enhancePosition.InterestRateDefault * 100).ToString("N4")
|
||||
: "0.0000";
|
||||
}
|
||||
else
|
||||
{
|
||||
dic["固定收益率"] = "0.0000";
|
||||
}
|
||||
|
||||
// 获取客户适用的保证金率
|
||||
var clientMarginRate = UnderlyingHelper.GetApplicableMarginRate(
|
||||
@@ -405,9 +415,18 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
|
||||
: "0.0000";
|
||||
|
||||
// 利率类型判断(固定/浮动)
|
||||
var interestMargin = swapPositions
|
||||
.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode) && x.interest_rest_days != null)
|
||||
.FirstOrDefault();
|
||||
swap_position interestMargin = null;
|
||||
// ETF: 优先取"互换利率"腿
|
||||
if (isEtf)
|
||||
{
|
||||
interestMargin = swapPositions
|
||||
.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode) && x.category_tag == "互换利率")
|
||||
.FirstOrDefault();
|
||||
}
|
||||
if (interestMargin == null)
|
||||
interestMargin = swapPositions
|
||||
.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode) && x.interest_rest_days != null)
|
||||
.FirstOrDefault();
|
||||
if (interestMargin == null)
|
||||
interestMargin = swapPositions
|
||||
.Where(x => ConsTrade.InterestModels.Contains(x.InterestMode) && string.IsNullOrWhiteSpace(x.FloatRateUnderlyingCode))
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 平仓比例口径A(占期初名义本金)产品契约测试
|
||||
/// ================================================================================
|
||||
/// 产品需求(不可违背):
|
||||
/// ClosePercent 永远是"占期初名义本金(NotionalValue)的比例",即口径A。
|
||||
/// 例:期初本金 5000 万,首次平 35%(ClosePercent=0.35),剩余 3250 万。
|
||||
/// 第二次想平掉剩余的 50%,ClosePercent 应 = 1625万/5000万 = 0.325(占期初),
|
||||
/// 而非 0.50(占剩余)。
|
||||
///
|
||||
/// 前后端协作约定:
|
||||
/// - 前端(unwindSwapTrade.js):ClosePercent 始终以 NotionalValue 为分母计算/显示
|
||||
/// - 前端传给后端:ClosePercent 为口径A
|
||||
/// - 后端入口(SwapUnwind/ApplySwapTrade):ToRemainingClosePercent 将 A→B 供内部计算
|
||||
/// - 后端落库(SaveSwapDealInternal):ToOriginalClosePercent 将 B→A 还原存储
|
||||
/// - 后端 InitUnwind 默认值:CalcDefaultInitClosePercent = PosiNotionalValue/NotionalValue(口径A)
|
||||
///
|
||||
/// 本测试守护的回归场景(c9071a4e 曾犯的错误):
|
||||
/// 1. 前端把 oriClosePercent 硬编码为 1(应为 PosiNotionalValue/NotionalValue)
|
||||
/// 2. 前端把 CloseNotionalValue 分母从 NotionalValue 改为 PosiNotionalValue
|
||||
/// 3. 前端把 ClosePercent 分母从 NotionalValue 改为 PosiNotionalValue
|
||||
/// 4. 前端把 getInterestList 的 notionalValue/posiNotionalValue 参数去掉
|
||||
/// 5. 前端把 calcCloseQtyByPercent 从 SwapCalc.calcCloseQtyByOriginalPercent 改为直接乘
|
||||
/// 6. 后端 InitUnwind 默认 ClosePercent 改为 1 而非剩余比例
|
||||
///
|
||||
/// 与既有测试的关系:
|
||||
/// - ApplySwapTradeClosePercentBugTest:测 A→B 转换函数正确性(函数级)
|
||||
/// - InitUnwindDefaultClosePercentTest:测默认值函数正确性(函数级)
|
||||
/// - 本测试:测完整多步场景的口径A契约(场景级),补齐"装配测试"盲区
|
||||
/// ================================================================================
|
||||
[TestClass]
|
||||
public class ClosePercentProductContractTest
|
||||
{
|
||||
// GLMS-20260701-0006 真实数据
|
||||
private const decimal OriginalNotional = 50_000_000m; // 期初名义本金(NotionalValue)
|
||||
private const decimal FirstClosePercentA = 0.35m; // 第一次平仓35%(口径A)
|
||||
private const decimal RemainingAfter1st = 32_500_000m; // 首次平35%后剩余(PosiNotionalValue)
|
||||
// 第二次想平掉剩余的 50% → 平仓额=16,250,000 → ClosePercent(A)=1625万/5000万=0.325
|
||||
private const decimal SecondCloseNotional = 16_250_000m;
|
||||
private const decimal SecondClosePercentA = 0.325m; // 口径A:占期初
|
||||
private const decimal SecondClosePercentB = 0.50m; // 口径B:占剩余
|
||||
|
||||
// ================================================================
|
||||
// 契约1:ClosePercent = CloseNotionalValue / NotionalValue(口径A)
|
||||
// 如果有人把分母改成 PosiNotionalValue,此测试会红
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_001_平仓比例必须用期初名义本金为分母_而非剩余名义本金()
|
||||
{
|
||||
// 正确:口径A = 平仓名义本金 / 期初名义本金
|
||||
decimal correctA = SecondCloseNotional / OriginalNotional;
|
||||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentA, correctA, 1e-10m,
|
||||
"口径A:16,250,000 / 50,000,000 = 0.325");
|
||||
|
||||
// 错误:口径B = 平仓名义本金 / 剩余名义本金
|
||||
decimal buggyB = SecondCloseNotional / RemainingAfter1st;
|
||||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentB, buggyB, 1e-10m,
|
||||
"口径B(错误):16,250,000 / 32,500,000 = 0.50");
|
||||
|
||||
// 两者必须不同——如果相同说明测试场景退化(首次平仓 remaining==original)
|
||||
Assert.AreNotEqual(correctA, buggyB,
|
||||
"口径A(0.325) 和 口径B(0.50) 在多次部分平仓后必须不同,否则测试场景退化");
|
||||
Console.WriteLine($"口径A={correctA}(正确),口径B={buggyB}(错误)");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约2:CloseNotionalValue = ClosePercent × NotionalValue(口径A)
|
||||
// 如果有人把乘数改成 PosiNotionalValue,此测试会红
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_002_平仓名义本金必须用期初名义本金为乘数_而非剩余名义本金()
|
||||
{
|
||||
// 正确:口径A → CloseNotionalValue = ClosePercent(A) × NotionalValue
|
||||
decimal correctNotional = SecondClosePercentA * OriginalNotional;
|
||||
SwapDealTestFactory.AssertDecimalEqual(SecondCloseNotional, correctNotional, 1e-6m,
|
||||
"口径A:0.325 × 50,000,000 = 16,250,000");
|
||||
|
||||
// 错误:口径B → CloseNotionalValue = ClosePercent(A) × PosiNotionalValue
|
||||
// 如果前端传口径A的0.325但误用 PosiNotionalValue 做乘数
|
||||
decimal buggyNotional = SecondClosePercentA * RemainingAfter1st;
|
||||
// 0.325 × 32,500,000 = 10,562,500 ≠ 16,250,000
|
||||
Assert.AreNotEqual(SecondCloseNotional, buggyNotional,
|
||||
"口径A的0.325 × 剩余本金32,500,000 = 10,562,500 ≠ 16,250,000,乘数错了");
|
||||
Console.WriteLine($"正确={correctNotional},错误(用剩余)={buggyNotional}");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约3:oriClosePercent = PosiNotionalValue / NotionalValue(不能硬编码为1)
|
||||
// 如果有人把 oriClosePercent 改成 1,此测试会红
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_003_最多可平比例必须为剩余除以期初_不能硬编码为1()
|
||||
{
|
||||
// 正确:oriClosePercent = PosiNotionalValue / NotionalValue
|
||||
decimal correctOri = RemainingAfter1st / OriginalNotional;
|
||||
SwapDealTestFactory.AssertDecimalEqual(0.65m, correctOri, 1e-10m,
|
||||
"oriClosePercent = 32,500,000 / 50,000,000 = 0.65");
|
||||
|
||||
// 错误:硬编码为 1(c9071a4e 的错误)
|
||||
decimal buggyOri = 1m;
|
||||
Assert.AreNotEqual(correctOri, buggyOri,
|
||||
"多次部分平仓后 oriClosePercent 必须小于 1,硬编码 1 会允许平超过剩余持仓");
|
||||
|
||||
// 首次平仓时 oriClosePercent 才等于 1(remaining == original)
|
||||
decimal firstTimeOri = OriginalNotional / OriginalNotional;
|
||||
SwapDealTestFactory.AssertDecimalEqual(1m, firstTimeOri, 1e-10m,
|
||||
"首次平仓时 oriClosePercent = 1(remaining == original)");
|
||||
Console.WriteLine($"多次部分平仓后:oriClosePercent={correctOri}(≠1),首次:{firstTimeOri}(=1)");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约4:CloseQty 必须经过 A→B 转换,不能直接 PositionQty × ClosePercent(A)
|
||||
// 如果有人删除 calcCloseQtyByOriginalPercent 调用改为直接乘,此测试会红
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_004_平仓数量必须经过口径A到B转换_不能直接乘()
|
||||
{
|
||||
// 场景:期初数量 32,500,000(=剩余数量),oriClosePercent=0.65
|
||||
// 用户输入 ClosePercent(A) = 0.325(想平剩余的 50%)
|
||||
decimal positionQty = RemainingAfter1st; // 32,500,000
|
||||
decimal oriClosePercent = RemainingAfter1st / OriginalNotional; // 0.65
|
||||
|
||||
// 正确:CloseQty = PositionQty × (ClosePercent(A) / oriClosePercent)
|
||||
// = 32,500,000 × (0.325 / 0.65) = 32,500,000 × 0.5 = 16,250,000
|
||||
decimal correctQty = positionQty * (SecondClosePercentA / oriClosePercent);
|
||||
SwapDealTestFactory.AssertDecimalEqual(16_250_000m, correctQty, 1e-6m,
|
||||
"正确:32,500,000 × (0.325/0.65) = 16,250,000");
|
||||
|
||||
// 错误:CloseQty = PositionQty × ClosePercent(A)(直接乘,不做转换)
|
||||
// = 32,500,000 × 0.325 = 10,562,500 ❌
|
||||
decimal buggyQty = positionQty * SecondClosePercentA;
|
||||
Assert.AreNotEqual(correctQty, buggyQty,
|
||||
"直接乘会得到 10,562,500 而非 16,250,000,数量算少 35%");
|
||||
|
||||
// JS 浮点精度守卫:32500000×(0.5/0.65) 可能 = 24999999.999999996
|
||||
// SwapCalc.calcCloseQtyByOriginalPercent 用 roundHalfAwayFromZero 修复
|
||||
decimal jsFloatTrap = (decimal)((double)positionQty * ((double)SecondClosePercentA / (double)oriClosePercent));
|
||||
Console.WriteLine($"正确={correctQty},错误(直接乘)={buggyQty},JS浮点陷阱={jsFloatTrap}");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约5:后端 A→B→A 往返转换必须还原(多次部分平仓场景)
|
||||
// 守护 SwapUnwind/ApplySwapTrade 入口的 ToRemainingClosePercent + SaveSwapDealInternal 的 ToOriginalClosePercent
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_005_多次部分平仓_A到B到A往返转换必须还原原值()
|
||||
{
|
||||
// 第二次部分平仓:前端传 ClosePercent(A) = 0.325
|
||||
decimal closePercentA = SecondClosePercentA;
|
||||
|
||||
// 后端入口:A → B
|
||||
decimal closePercentB = SwapDealService.ToRemainingClosePercent(
|
||||
closePercentA, OriginalNotional, RemainingAfter1st);
|
||||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentB, closePercentB, 1e-10m,
|
||||
"A→B:0.325 × 50,000,000 / 32,500,000 = 0.50");
|
||||
|
||||
// 后端落库:B → A 还原
|
||||
decimal restoredA = SwapDealService.ToOriginalClosePercent(
|
||||
closePercentB, OriginalNotional, RemainingAfter1st);
|
||||
SwapDealTestFactory.AssertDecimalEqual(closePercentA, restoredA, 1e-10m,
|
||||
"B→A 还原:0.50 × 32,500,000 / 50,000,000 = 0.325(必须等于原始A)");
|
||||
|
||||
Console.WriteLine($"A={closePercentA} → B={closePercentB} → A'={restoredA} ✅ 往返还原");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约6:后端 InitUnwind 默认 ClosePercent = PosiNotionalValue / NotionalValue(口径A)
|
||||
// 不能硬编码为 1(c9071a4e 前端硬编码1的错误在后端等价于此)
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_006_InitUnwind默认值必须为剩余除以期初_不能硬编码为1()
|
||||
{
|
||||
// 多次部分平仓后:期初 50M,剩余 32.5M
|
||||
decimal defaultValue = SwapDealService.CalcDefaultInitClosePercent(
|
||||
OriginalNotional, RemainingAfter1st);
|
||||
|
||||
// 正确:0.65(占期初的"平剩余全部"比例)
|
||||
SwapDealTestFactory.AssertDecimalEqual(0.65m, defaultValue, 1e-10m,
|
||||
"CalcDefaultInitClosePercent(50M, 32.5M) = 0.65(口径A)");
|
||||
|
||||
// 不能是 1(硬编码错误)
|
||||
Assert.AreNotEqual(1m, defaultValue,
|
||||
"多次部分平仓后默认值不能为1,否则意味着'平掉原始全部'而非'平剩余全部'");
|
||||
|
||||
Console.WriteLine($"InitUnwind 默认 ClosePercent(A) = {defaultValue}(≠1)✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约7:SwapUnwind 第二次部分平仓必须正确做 A→B 转换
|
||||
// 这是"装配测试"——验证后端入口确实执行了转换,而不只是函数本身正确
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_007_SwapUnwind第二次部分平仓_入口必须将ClosePercent从A转为B()
|
||||
{
|
||||
// 模拟 GLMS-20260701-0006 第二次部分平仓
|
||||
var td = new trade
|
||||
{
|
||||
id = 2001,
|
||||
TradeNumber = "CPC-TEST-007",
|
||||
TradeType = "收益互换",
|
||||
TradeStatus = "确认成交",
|
||||
ValidState = "Valid",
|
||||
StockEqvNotional = (double)RemainingAfter1st, // 32,500,000
|
||||
OriginalStockEqvNotional = (double)OriginalNotional, // 50,000,000
|
||||
Notional = (double)RemainingAfter1st,
|
||||
TradeAmount = (double)RemainingAfter1st
|
||||
};
|
||||
|
||||
var service = new TestableSwapDealService(td);
|
||||
|
||||
// 前端传 ClosePercent = 0.325(口径A,占期初)
|
||||
var unwindData = new UnwindData
|
||||
{
|
||||
SwapTradeId = td.id,
|
||||
SwapRealizedPnL = 1000m,
|
||||
SwapCloseAmount = 1000m,
|
||||
CloseMethod = (int)CloseMethodEnum.部分平仓,
|
||||
ClosePercent = SecondClosePercentA, // 0.325(口径A)
|
||||
CloseQty = 16_250_000m,
|
||||
CloseNotionalValue = SecondCloseNotional, // 16,250,000
|
||||
PositionQty = RemainingAfter1st, // 32,500,000
|
||||
NotionalValue = OriginalNotional, // 50,000,000(期初)
|
||||
PosiNotionalValue = RemainingAfter1st, // 32,500,000(剩余)
|
||||
ValueDate = new DateTime(2026, 7, 14),
|
||||
UnwindDate = new DateTime(2026, 7, 15),
|
||||
StartDate = new DateTime(2026, 7, 1)
|
||||
};
|
||||
|
||||
service.SwapUnwind(unwindData);
|
||||
|
||||
// 验证 SwapUnwind 被调用
|
||||
Assert.AreEqual(1, service.SaveSwapDealCalls.Count, "SwapUnwind 应调用 SaveSwapDeal");
|
||||
|
||||
// 验证传给 SaveSwapDeal 的 ClosePercent 已转为口径B
|
||||
var savedData = service.SaveSwapDealCalls[0].data;
|
||||
decimal expectedB = SwapDealService.ToRemainingClosePercent(
|
||||
SecondClosePercentA, OriginalNotional, RemainingAfter1st);
|
||||
|
||||
SwapDealTestFactory.AssertDecimalEqual(expectedB, savedData.ClosePercent, 1e-10m,
|
||||
"SwapUnwind 应将 ClosePercent 从口径A(0.325)转为口径B(0.50)");
|
||||
|
||||
// 口径B 不等于口径A(验证转换确实发生了)
|
||||
Assert.AreNotEqual(SecondClosePercentA, savedData.ClosePercent,
|
||||
"口径B(0.50) 不应等于口径A(0.325),否则说明转换缺失");
|
||||
|
||||
Console.WriteLine($"SwapUnwind: 输入A={SecondClosePercentA} → 输出B={savedData.ClosePercent} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约8:完整多步场景——3次平仓合计应等于期初本金
|
||||
// 首次35%(A) → 第二次平剩余50%(A=0.325) → 第三次全平剩余(A=0.325)
|
||||
// 合计 CloseNotionalValue = 17.5M + 16.25M + 16.25M = 50M = 原始本金
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_008_三次部分平仓合计本金必须等于期初名义本金()
|
||||
{
|
||||
decimal remaining = OriginalNotional; // 50,000,000
|
||||
decimal totalClosed = 0m;
|
||||
|
||||
// Step1: 平 35%(首次,remaining == original, A = B = 0.35)
|
||||
decimal step1A = 0.35m;
|
||||
decimal step1Notional = step1A * OriginalNotional; // 17,500,000
|
||||
totalClosed += step1Notional;
|
||||
remaining -= step1Notional; // 32,500,000
|
||||
|
||||
// Step2: 平剩余的 50% → A = 16,250,000 / 50,000,000 = 0.325
|
||||
decimal step2Notional = 16_250_000m;
|
||||
decimal step2A = step2Notional / OriginalNotional; // 0.325
|
||||
// 后端 A→B 转换
|
||||
decimal step2B = SwapDealService.ToRemainingClosePercent(
|
||||
step2A, OriginalNotional, remaining);
|
||||
SwapDealTestFactory.AssertDecimalEqual(0.50m, step2B, 1e-10m,
|
||||
"Step2: A=0.325 → B=0.50(平剩余50%)");
|
||||
totalClosed += step2Notional;
|
||||
remaining -= step2Notional; // 16,250,000
|
||||
|
||||
// Step3: 全平剩余 → A = 16,250,000 / 50,000,000 = 0.325
|
||||
decimal step3Notional = remaining;
|
||||
decimal step3A = step3Notional / OriginalNotional; // 0.325
|
||||
decimal step3B = SwapDealService.ToRemainingClosePercent(
|
||||
step3A, OriginalNotional, remaining);
|
||||
SwapDealTestFactory.AssertDecimalEqual(1.0m, step3B, 1e-10m,
|
||||
"Step3: A=0.325 → B=1.0(全平剩余)");
|
||||
totalClosed += step3Notional;
|
||||
remaining -= step3Notional; // 0
|
||||
|
||||
// 守恒:合计 = 期初
|
||||
SwapDealTestFactory.AssertDecimalEqual(OriginalNotional, totalClosed, 1e-6m,
|
||||
"三次平仓合计必须 = 期初名义本金 50,000,000");
|
||||
SwapDealTestFactory.AssertDecimalEqual(0m, remaining, 1e-6m,
|
||||
"三次平仓后剩余必须 = 0");
|
||||
|
||||
Console.WriteLine($"Step1: A=0.35, Notional=17,500,000");
|
||||
Console.WriteLine($"Step2: A=0.325→B=0.50, Notional=16,250,000");
|
||||
Console.WriteLine($"Step3: A=0.325→B=1.00, Notional=16,250,000");
|
||||
Console.WriteLine($"合计={totalClosed} = 期初{OriginalNotional} ✅");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约9:GetUnwindInterestList 必须接收 notionalValue 和 posiNotionalValue
|
||||
// 前端 getInterestList 传这两个参数给后端做 A→B 转换
|
||||
// 如果前端删掉这两个参数,后端 ToRemainingClosePercent 在 posiNotionalValue=0 时会跳过转换
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_009_ToRemainingClosePercent_PosiNotionalValue为零时跳过转换_前端必须传值()
|
||||
{
|
||||
// 模拟前端不传 notionalValue/posiNotionalValue(默认0)
|
||||
decimal result = SwapDealService.ToRemainingClosePercent(
|
||||
SecondClosePercentA, notionalValue: 0, posiNotionalValue: 0);
|
||||
|
||||
// posiNotionalValue <= 0 时直接返回原值(不转换)
|
||||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentA, result, 1e-10m,
|
||||
"posiNotionalValue=0 时不做转换——所以前端必须传 notionalValue/posiNotionalValue");
|
||||
|
||||
// 正确:前端传值后转换正常
|
||||
decimal resultWithValues = SwapDealService.ToRemainingClosePercent(
|
||||
SecondClosePercentA, OriginalNotional, RemainingAfter1st);
|
||||
SwapDealTestFactory.AssertDecimalEqual(SecondClosePercentB, resultWithValues, 1e-10m,
|
||||
"前端传值后:A=0.325 → B=0.50 ✅");
|
||||
|
||||
// 两者必须不同
|
||||
Assert.AreNotEqual(result, resultWithValues,
|
||||
"传 vs 不传 notionalValue 结果不同——前端必须传,否则利息计算用错口径");
|
||||
|
||||
Console.WriteLine($"不传值(默认0):{result}(未转换,错误地用A算利息)");
|
||||
Console.WriteLine($"传值:{resultWithValues}(正确转换为B)");
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 契约10:首次平仓 A==B(退化场景,不应误报)
|
||||
// 首次平仓时 NotionalValue == PosiNotionalValue,A=B,转换系数=1
|
||||
// 这是既有测试全绿的原因——必须用非退化场景才能捕获回归
|
||||
// ================================================================
|
||||
[TestMethod]
|
||||
public void CPC_010_首次平仓A等于B_退化场景_不能作为唯一测试()
|
||||
{
|
||||
decimal closePercentA = 0.35m;
|
||||
decimal firstRemaining = OriginalNotional; // 首次 remaining == original
|
||||
|
||||
decimal convertedB = SwapDealService.ToRemainingClosePercent(
|
||||
closePercentA, OriginalNotional, firstRemaining);
|
||||
|
||||
// 首次平仓:A == B(转换系数 = 1)
|
||||
SwapDealTestFactory.AssertDecimalEqual(closePercentA, convertedB, 1e-10m,
|
||||
"首次平仓 remaining==original → A==B==0.35(退化场景)");
|
||||
|
||||
// 退化场景下即使不做转换结果也一样——这就是既有测试全绿的原因
|
||||
decimal noConversion = closePercentA;
|
||||
Assert.AreEqual(noConversion, convertedB,
|
||||
"退化场景:做不做转换结果一样 → 无法发现'转换缺失'的bug");
|
||||
|
||||
Console.WriteLine($"⚠ 退化场景:A={closePercentA} == B={convertedB}(首次平仓,无法暴露双重转换bug)");
|
||||
Console.WriteLine($"✅ 非退化场景见 CPC_005/007:A=0.325 ≠ B=0.50(多次部分平仓后才能暴露)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Reflection;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
[TestClass]
|
||||
public class InitUnwindTradingFeeTest
|
||||
{
|
||||
private static decimal InvokeCalcInitTradingFee(swap_position position, UnwindData unwindData)
|
||||
{
|
||||
var method = typeof(SwapDealService).GetMethod(
|
||||
"CalcInitTradingFee",
|
||||
BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.IsNotNull(method, "未找到 CalcInitTradingFee 私有静态方法");
|
||||
|
||||
return (decimal)method.Invoke(null, new object[] { position, unwindData });
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 百分比模式_按平仓名义本金计算并四舍五入到两位()
|
||||
{
|
||||
var position = new swap_position
|
||||
{
|
||||
PosiFeeType = 0,
|
||||
PosiTradingFeeUnit = 0.1234m
|
||||
};
|
||||
var unwindData = new UnwindData
|
||||
{
|
||||
CloseNotionalValue = 1_000_000m,
|
||||
CloseQty = 8888m
|
||||
};
|
||||
|
||||
var fee = InvokeCalcInitTradingFee(position, unwindData);
|
||||
|
||||
Assert.AreEqual(1234.00m, fee);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 单位数量模式_按平仓数量计算并四舍五入到两位()
|
||||
{
|
||||
var position = new swap_position
|
||||
{
|
||||
PosiFeeType = 1,
|
||||
PosiTradingFeeUnit = 1.235m
|
||||
};
|
||||
var unwindData = new UnwindData
|
||||
{
|
||||
CloseNotionalValue = 1_000_000m,
|
||||
CloseQty = 10m
|
||||
};
|
||||
|
||||
var fee = InvokeCalcInitTradingFee(position, unwindData);
|
||||
|
||||
Assert.AreEqual(12.35m, fee);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void 空入参_返回零()
|
||||
{
|
||||
Assert.AreEqual(0m, InvokeCalcInitTradingFee(null, new UnwindData()));
|
||||
Assert.AreEqual(0m, InvokeCalcInitTradingFee(new swap_position(), null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using BaseOUDAL;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Reflection;
|
||||
using YLErp.Core.DBModels;
|
||||
using YLErp.Model;
|
||||
|
||||
@@ -14,6 +17,19 @@ namespace YLErp.BLL
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var clientBalanceMoneyConverter = new ValueConverter<double?, decimal?>(
|
||||
value => value.HasValue ? Convert.ToDecimal(value.Value) : null,
|
||||
value => value.HasValue ? (double)value.Value : null);
|
||||
var clientBalanceDaily = modelBuilder.Entity<ClientBalanceDaily>();
|
||||
foreach (var property in typeof(ClientBalanceDaily).GetProperties()
|
||||
.Where(property => property.PropertyType == typeof(double?)
|
||||
&& property.GetCustomAttribute<NotMappedAttribute>() == null))
|
||||
{
|
||||
clientBalanceDaily.Property<double?>(property.Name)
|
||||
.HasConversion(clientBalanceMoneyConverter)
|
||||
.HasColumnType("decimal(20,6)");
|
||||
}
|
||||
|
||||
modelBuilder.Entity<AppConfig>().HasKey(c => new { c.PGroup, c.PName });
|
||||
modelBuilder.Entity<ExchangeOptionVol>().HasKey(c => new { c.ValueDate, c.OptionCode });
|
||||
modelBuilder.Entity<dicForTranslation>().HasKey(c => new { c.From, c.Key });
|
||||
@@ -414,4 +430,4 @@ namespace YLErp.BLL
|
||||
public DbSet<glms_risk_variable> glms_risk_variable { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,6 @@ namespace YLErp.Model
|
||||
|
||||
public string OptLog { get; set; }
|
||||
|
||||
public decimal? InitYtm { get; set; }
|
||||
public string InitYtm { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,14 +306,13 @@ namespace YLErp.Modules.AppModule
|
||||
//-----------------------------------------------
|
||||
configService.AddDataIfNotExists("ProjectConfig", "Erp.IsAutoSealAfterGeneratedBook", "false", "bool", "确认书生成时是否自动用印(IsAutoSealAndUploadFiles勾选时生效)");
|
||||
configService.AddDataIfNotExists("ProjectConfig", "Erp.ReportFileBeginNumber", "0", "int", "报送文件开始编号");
|
||||
//-----------------------------------------------
|
||||
// 删除不再使用的
|
||||
//-----------------------------------------------
|
||||
RemoveUnUsed(configService);
|
||||
}
|
||||
|
||||
private static void RemoveUnUsed(InnerAppConfigService configService)
|
||||
{
|
||||
configService.RemoveData("ProjectConfig", "Trade.SwapMarginTemplateConfig");
|
||||
|
||||
if (AppManager.Version.Major < 3)
|
||||
{
|
||||
configService.RemoveData("ProjectConfig", "Erp.TradeConfirmBookEmailTPL");
|
||||
@@ -453,6 +452,38 @@ namespace YLErp.Modules.AppModule
|
||||
}
|
||||
|
||||
adminDb.SaveChanges();
|
||||
|
||||
var marginTemplateDictionary = adminDb.Dictionaries.FirstOrDefault(item => item.Name == YLErp.Modules.SwapModule.SwapMarginTemplateConfigService.DictionaryName);
|
||||
if (marginTemplateDictionary == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existingNames = adminDb.DictionaryItems
|
||||
.Where(item => item.DictId == marginTemplateDictionary.Id)
|
||||
.Select(item => item.Name)
|
||||
.ToHashSet();
|
||||
var nextIndex = adminDb.DictionaryItems
|
||||
.Where(item => item.DictId == marginTemplateDictionary.Id)
|
||||
.Select(item => item.IndexNum)
|
||||
.DefaultIfEmpty(-1)
|
||||
.Max();
|
||||
foreach (var templateName in YLErp.Modules.SwapModule.SwapMarginTemplateConfigService.InitialTemplateNames)
|
||||
{
|
||||
if (existingNames.Contains(templateName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
adminDb.DictionaryItems.Add(new BaseOUDAL.DictionaryItem
|
||||
{
|
||||
DictId = marginTemplateDictionary.Id,
|
||||
Name = templateName,
|
||||
ShortName = templateName,
|
||||
IndexNum = ++nextIndex
|
||||
});
|
||||
}
|
||||
adminDb.SaveChanges();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -455,7 +455,7 @@ namespace YLErp.Modules.EodModule
|
||||
Lots = swapFlow.Lots,
|
||||
IsNight = swapFlow.IsNight,
|
||||
OpponentRole = "甲方",
|
||||
MarginTemplateName = "系统默认",
|
||||
MarginTemplateName = null,
|
||||
MarginType = MarginTypeEnum.DEFAULT,
|
||||
IsGroup = isSingleTrade ? 0 : 2
|
||||
};
|
||||
@@ -743,7 +743,7 @@ namespace YLErp.Modules.EodModule
|
||||
td.OriginalNotional = td.Notional;
|
||||
td.OriginalStockEqvNotional = td.StockEqvNotional;
|
||||
td.StockEqvNotionalReal = td.StockEqvNotionalReal;
|
||||
td.MarginTemplateName = "系统默认";
|
||||
td.MarginTemplateName = null;
|
||||
td.MarginType = MarginTypeEnum.DEFAULT;
|
||||
td.IsTradePricePayType = true;
|
||||
td.TradeSource = TradeSourceEnum.导入交易.ToString();
|
||||
|
||||
@@ -291,6 +291,9 @@ namespace YLErp.Modules.SwapModule
|
||||
floatEvent.UnderlyingInstrumentType = position.UnderlyingInstrumentType;
|
||||
floatEvent.CloseFee = 0;
|
||||
floatEvent.BeforeCloseFee = oriPosition.PosiTradingFeePending;
|
||||
floatEvent.TradingFee = CalcInitTradingFee(oriPosition, unwindData);
|
||||
floatEvent.PosiTradingFeeUnit = oriPosition?.PosiTradingFeeUnit ?? 0;
|
||||
floatEvent.PosiFeeType = oriPosition?.PosiFeeType ?? 0;
|
||||
floatEvent.MarkClosePnl = 0;
|
||||
floatEvent.PayDirection = position.PosiDirection;
|
||||
floatEvent.PosiGrossPrice = position.PosiGrossPrice;
|
||||
@@ -313,6 +316,20 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
return unwindData;
|
||||
}
|
||||
private static decimal CalcInitTradingFee(swap_position oriPosition, UnwindData unwindData)
|
||||
{
|
||||
if (oriPosition == null || unwindData == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (oriPosition.PosiFeeType == 1)
|
||||
{
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit * unwindData.CloseQty, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
return Math.Round(oriPosition.PosiTradingFeeUnit / 100m * unwindData.CloseNotionalValue, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
/// <summary>
|
||||
/// 校验上日是否收盘
|
||||
/// </summary>
|
||||
|
||||
@@ -372,18 +372,18 @@ namespace YLErp.Modules.SwapModule
|
||||
exportModel.PositionType = item.PositionType==1?"多头":"空头";
|
||||
exportModel.UnderlyingCode = item.UnderlyingCode;
|
||||
exportModel.MatuirityDate = item.MatuirityDate.OtcFormatDate();
|
||||
exportModel.TradingAmountAvg = item.TradingAmountAvg.OtcFormat(OtcFormatFlag.umprice);
|
||||
exportModel.TradingAmountFeeAvg = item.TradingAmountFeeAvg.OtcFormat(OtcFormatFlag.umprice);
|
||||
exportModel.Quantity = item.Quantity.OtcFormatMoney(false, 4);
|
||||
exportModel.TradingAmount = item.TradingAmount.OtcFormatMoney(false, 4);
|
||||
exportModel.TradingAmountAvg = item.TradingAmountAvg.OtcFormatMoney(false, 2);
|
||||
exportModel.TradingAmountFeeAvg = item.TradingAmountFeeAvg.OtcFormatMoney(false, 2);
|
||||
exportModel.Quantity = item.Quantity.OtcFormatMoney(false, 2);
|
||||
exportModel.TradingAmount = item.TradingAmount.OtcFormatMoney(false, 2);
|
||||
exportModel.ContractSize = item.ContractSize.ToString();
|
||||
exportModel.TradingFee = item.TradingFee.OtcFormatMoney(false, 4);
|
||||
exportModel.TradingFeePending = item.TradingFeePending.OtcFormatMoney(false, 4);
|
||||
exportModel.DividendPending = item.DividendPending.OtcFormatMoney(false, 4);
|
||||
exportModel.MarkClosePnl = item.MarkClosePnl.OtcFormatMoney(false, 4);
|
||||
exportModel.DividendIn = item.DividendIn.OtcFormatMoney(false, 4);
|
||||
exportModel.TradingFee = item.TradingFee.OtcFormatMoney(false, 2);
|
||||
exportModel.TradingFeePending = item.TradingFeePending.OtcFormatMoney(false, 2);
|
||||
exportModel.DividendPending = item.DividendPending.OtcFormatMoney(false, 2);
|
||||
exportModel.MarkClosePnl = item.MarkClosePnl.OtcFormatMoney(false, 2);
|
||||
exportModel.DividendIn = item.DividendIn.OtcFormatMoney(false, 2);
|
||||
exportModel.OptLog = item.OptLog;
|
||||
exportModel.InitYtm = item.InitYtm;
|
||||
exportModel.InitYtm = item.InitYtm?.OtcFormatMoney(false, 9);
|
||||
list.Add(exportModel);
|
||||
}
|
||||
var tplFilePath = OtcAppContext.MapPath("/App_Docs");
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using BaseOUDAL;
|
||||
using YLErp.Models;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
public static class SwapMarginTemplateConfigService
|
||||
{
|
||||
public const string DictionaryName = "保证金模板名称";
|
||||
|
||||
public static readonly string[] InitialTemplateNames = { "现金保证金", "授信保证金" };
|
||||
|
||||
public static SwapMarginTemplateConfig GetConfig()
|
||||
{
|
||||
using var db = new ErpBaseContext();
|
||||
var dictionaryId = db.Dictionaries
|
||||
.Where(item => item.Name == DictionaryName)
|
||||
.Select(item => item.Id)
|
||||
.FirstOrDefault();
|
||||
var items = db.DictionaryItems
|
||||
.Where(item => item.DictId == dictionaryId && !string.IsNullOrWhiteSpace(item.Name))
|
||||
.OrderBy(item => item.IndexNum)
|
||||
.Select(item => new SelectItem { Text = item.Name, Value = item.Name })
|
||||
.ToArray();
|
||||
|
||||
return new SwapMarginTemplateConfig
|
||||
{
|
||||
options = items,
|
||||
defaultValue = items.FirstOrDefault()?.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class SwapMarginTemplateConfig
|
||||
{
|
||||
public IEnumerable<SelectItem> options { get; set; }
|
||||
|
||||
public string defaultValue { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BaseOUDAL;
|
||||
using BaseOUDAL;
|
||||
using ClosedXML.Report.Options;
|
||||
using Confluent.Kafka;
|
||||
using CsvHelper;
|
||||
@@ -341,7 +341,7 @@ namespace YLErp.Modules.SwapModule
|
||||
TradeDate = flowMerge.OccurTime,
|
||||
TraderId = asset.TraderIdsInt.FirstOrDefault(),
|
||||
TraderName = asset.TraderNamesList.FirstOrDefault(),
|
||||
MarginTemplateName = "系统默认",
|
||||
MarginTemplateName = null,
|
||||
OpponentRole = "乙方",
|
||||
StructureType = structureType,
|
||||
InitialMargin = 0,
|
||||
@@ -1375,6 +1375,7 @@ namespace YLErp.Modules.SwapModule
|
||||
position.PosiTradingFee = swap.PosiTradingFee;
|
||||
position.PosiTradingFee=Math.Round(position.PosiTradingFee, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.PosiTradingFeeUnit = swap.PosiTradingFeeUnit;
|
||||
position.PosiFeeType = swap.PosiFeeType;
|
||||
position.PosiTradingFeePending = swap.PosiTradingFeePending;
|
||||
position.PosiTradingFeePending = Math.Round(position.PosiTradingFeePending, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero);
|
||||
position.UnderlyingCode = swap.UnderlyingCode;
|
||||
@@ -1414,6 +1415,7 @@ namespace YLErp.Modules.SwapModule
|
||||
position.FloatRateUnderlyingCode = swap.FloatRateUnderlyingCode;
|
||||
position.interest_rest_days = swap.interest_rest_days;
|
||||
position.interest_rule = swap.interest_rule;
|
||||
position.category_tag = string.IsNullOrEmpty(swap.category_tag) ? "互换利率" : swap.category_tag;
|
||||
position.InitYtm = RoundSwapBondNetPriceAndYtm(swap.InitYtm);
|
||||
if (position.InitYtm != null && position.InitYtm > 0)
|
||||
{
|
||||
|
||||
@@ -378,7 +378,7 @@ namespace YLErp.Modules.TradeModule.SwapModule
|
||||
importTrade.StockEqvNotionalReal = importTrade.StockEqvNotionalReal;
|
||||
importTrade.trade_swap.RateCalcMode = "01";
|
||||
importTrade.IsUsePremiumRate = true;
|
||||
importTrade.MarginTemplateName = "系统默认";
|
||||
importTrade.MarginTemplateName = null;
|
||||
importTrade.MarginType = MarginTypeEnum.DEFAULT;
|
||||
importTrade.IsTradePricePayType = true;
|
||||
importTrade.TradeSource = TradeSourceEnum.导入交易.ToString();
|
||||
|
||||
@@ -519,7 +519,7 @@ namespace YLErp.Modules.TradeModule.SwapModule
|
||||
importTrade.ParticipationRate = 1;
|
||||
|
||||
//预付金
|
||||
importTrade.MarginTemplateName = "系统默认";
|
||||
importTrade.MarginTemplateName = null;
|
||||
importTrade.MarginType = MarginTypeEnum.DEFAULT;
|
||||
|
||||
SetDBModelCreator(importTrade);
|
||||
|
||||
@@ -726,7 +726,7 @@ namespace YLErp.Modules.TradeModule.SwapModule
|
||||
td.OriginalStockEqvNotional = td.StockEqvNotional;
|
||||
td.StockEqvNotionalReal = td.StockEqvNotionalReal;
|
||||
td.IsUsePremiumRate = true;
|
||||
td.MarginTemplateName = "系统默认";
|
||||
td.MarginTemplateName = null;
|
||||
td.MarginType = MarginTypeEnum.DEFAULT;
|
||||
td.IsTradePricePayType = true;
|
||||
td.TradeSource = TradeSourceEnum.导入交易.ToString();
|
||||
@@ -2815,7 +2815,7 @@ namespace YLErp.Modules.TradeModule.SwapModule
|
||||
td.SettlementDate = td.ExerciseDate;
|
||||
|
||||
td.StockEqvNotional = reader.GetDouble("名义本金(人民币)", true) ?? 0;
|
||||
td.MarginTemplateName = "系统默认";
|
||||
td.MarginTemplateName = null;
|
||||
td.UnWindDate = reader.GetDate("提前终止日/终止日", true);
|
||||
td.Comments = reader.GetString("备注", false);
|
||||
//td.UnderlyingName = reader.GetString("标的名称", true);
|
||||
|
||||
@@ -282,4 +282,4 @@ namespace YLErp.Web.Areas.Admin.Controllers
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,48 +45,26 @@ namespace YLErp.Web.Controllers
|
||||
ViewBag.observationDate = observationDate;
|
||||
return View();
|
||||
}
|
||||
public ActionResult TradeEdit(string enid, bool isUseApproval = false)
|
||||
public ActionResult TradeEdit(string enid, string renewEnid = null, bool isUseApproval = false)
|
||||
{
|
||||
ViewBag.isUseApproval = isUseApproval;
|
||||
var intid = DecryptInt(enid);
|
||||
// The new/renew flow uses the literal "0" to indicate that no trade exists yet.
|
||||
var intid = enid == "0" ? 0 : DecryptInt(enid);
|
||||
trade r = null;
|
||||
if (intid == 0)
|
||||
{
|
||||
TradeExtendJson tradeExtendJson = new TradeExtendJson()
|
||||
r = CreateNewTrade();
|
||||
var renewTradeId = DecryptInt(renewEnid);
|
||||
if (renewTradeId > 0)
|
||||
{
|
||||
FlowBookMode = (int)FlowBookModeEnum.否,
|
||||
FloatingPnlAnnualized = false,
|
||||
NeedOpenFee = false,
|
||||
OpenFeeType = 0,
|
||||
InterestCalcMode = "10",
|
||||
SettlementRules=0,
|
||||
DividendPayDate=0
|
||||
};
|
||||
var tradeDateCountry = GetBestCountry(valuedateBLL.ValueDate.Year);
|
||||
r = new trade()
|
||||
{
|
||||
TradeType = "收益互换",
|
||||
UnderlyingInstrumentType = "Stock",
|
||||
StartDate = valuedateBLL.ValueDate,
|
||||
TradeDate = QdpCalendarHelper.GetNonHolidayDefore(valuedateBLL.ValueDate.AddDays(-1), tradeDateCountry),
|
||||
TraderId = CurUser.UserId,
|
||||
TraderName = CurUser.UserName,
|
||||
MarginTemplateName = "系统默认",
|
||||
OpponentRole = "乙方",
|
||||
OriginalStockEqvNotional = 0,
|
||||
StructureType = "普通收益互换",
|
||||
InitialMargin = 0
|
||||
};
|
||||
r.StructureType = "普通债券类收益互换";
|
||||
tradeExtendJson.FlowBookMode = (int)FlowBookModeEnum.先进先出;
|
||||
r.trade_extend = new trade_extend()
|
||||
{
|
||||
ExtendJson = JsonHelper.Serialize(tradeExtendJson)
|
||||
};
|
||||
r.MetaDic = new Dictionary<string, string>
|
||||
{
|
||||
{ "清算机构", "甲方" }
|
||||
};
|
||||
var sourceTrade = new SwapTradeService(CurUser).GetSwapTrade(renewTradeId);
|
||||
if (sourceTrade == null)
|
||||
{
|
||||
return ShowError("没有找到交易数据");
|
||||
}
|
||||
|
||||
r = CreateRenewTrade(sourceTrade, r);
|
||||
}
|
||||
return View(r);
|
||||
}
|
||||
SwapTradeService swapTradeService = new SwapTradeService(CurUser);
|
||||
@@ -98,6 +76,130 @@ namespace YLErp.Web.Controllers
|
||||
|
||||
return View(r);
|
||||
}
|
||||
|
||||
private trade CreateNewTrade()
|
||||
{
|
||||
var defaultMarginTemplateName = SwapMarginTemplateConfigService.GetConfig().defaultValue;
|
||||
var tradeExtendJson = new TradeExtendJson()
|
||||
{
|
||||
FlowBookMode = (int)FlowBookModeEnum.先进先出,
|
||||
FloatingPnlAnnualized = false,
|
||||
NeedOpenFee = false,
|
||||
OpenFeeType = 0,
|
||||
InterestCalcMode = "10",
|
||||
SettlementRules = 0,
|
||||
DividendPayDate = 0
|
||||
};
|
||||
var tradeDateCountry = GetBestCountry(valuedateBLL.ValueDate.Year);
|
||||
return new trade()
|
||||
{
|
||||
TradeType = "收益互换",
|
||||
UnderlyingInstrumentType = "Stock",
|
||||
StartDate = valuedateBLL.ValueDate,
|
||||
TradeDate = QdpCalendarHelper.GetNonHolidayDefore(valuedateBLL.ValueDate.AddDays(-1), tradeDateCountry),
|
||||
TraderId = CurUser.UserId,
|
||||
TraderName = CurUser.UserName,
|
||||
MarginTemplateName = defaultMarginTemplateName,
|
||||
OpponentRole = "乙方",
|
||||
OriginalStockEqvNotional = 0,
|
||||
StructureType = "普通债券类收益互换",
|
||||
InitialMargin = 0,
|
||||
trade_extend = new trade_extend()
|
||||
{
|
||||
ExtendJson = JsonHelper.Serialize(tradeExtendJson)
|
||||
},
|
||||
MetaDic = new Dictionary<string, string>
|
||||
{
|
||||
{ "清算机构", "甲方" }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private trade CreateRenewTrade(trade sourceTrade, trade defaultTrade)
|
||||
{
|
||||
var renewTrade = sourceTrade.Clone();
|
||||
renewTrade.id = 0;
|
||||
renewTrade.TradeNumber = string.Empty;
|
||||
renewTrade.ParentTradeId = 0;
|
||||
renewTrade.TradeDate = defaultTrade.TradeDate;
|
||||
renewTrade.StartDate = defaultTrade.StartDate;
|
||||
renewTrade.ExerciseDate = null;
|
||||
renewTrade.MaturityDate = null;
|
||||
renewTrade.SettlementDate = null;
|
||||
renewTrade.UnWindDate = null;
|
||||
renewTrade.PremiumPayDate = null;
|
||||
renewTrade.SettlementFlagDate = null;
|
||||
renewTrade.HasPartialUnWind = null;
|
||||
renewTrade.TradeStatus = null;
|
||||
renewTrade.CheckStatus = null;
|
||||
renewTrade.ProcessStatus = null;
|
||||
renewTrade.ProcessOrderId = 0;
|
||||
renewTrade.ProcessOrderBranch = 0;
|
||||
renewTrade.ProcessOptDate = null;
|
||||
renewTrade.ValidState = null;
|
||||
renewTrade.CreateDate = null;
|
||||
renewTrade.TradeSource = null;
|
||||
// 恢复初始名义本金(源交易若有过部分平仓,StockEqvNotional/TradeAmount/Notional 已递减,
|
||||
// 但 OriginalStockEqvNotional 和 OriginalNotional 始终保留原始值不被递减)
|
||||
if (renewTrade.OriginalStockEqvNotional != null)
|
||||
{
|
||||
renewTrade.StockEqvNotional = (double)renewTrade.OriginalStockEqvNotional;
|
||||
}
|
||||
renewTrade.Notional = renewTrade.OriginalNotional ?? renewTrade.TradeAmount;
|
||||
renewTrade.TradeAmount = renewTrade.OriginalNotional ?? renewTrade.TradeAmount;
|
||||
// 结算标识 — 源交易可能为"延期结算",续做时重置为正常结算
|
||||
renewTrade.SettlementFlag = 0;
|
||||
renewTrade.SettlementFlagOptId = null;
|
||||
// trade_swap — 重置源交易遗留的 PK/FK 和运行时字段
|
||||
if (renewTrade.trade_swap != null)
|
||||
{
|
||||
renewTrade.trade_swap.id = 0;
|
||||
renewTrade.trade_swap.TradeId = 0;
|
||||
renewTrade.trade_swap.FlowId = null;
|
||||
}
|
||||
renewTrade.trade_extend = sourceTrade.trade_extend?.Clone() ?? defaultTrade.trade_extend;
|
||||
renewTrade.trade_extend.TradeId = 0;
|
||||
renewTrade.trade_Initial_Margin = sourceTrade.trade_Initial_Margin?.Clone() ?? new trade_initial_margin();
|
||||
renewTrade.trade_Initial_Margin.TradeId = 0;
|
||||
renewTrade.MetaDic = sourceTrade.MetaDic == null
|
||||
? new Dictionary<string, string>()
|
||||
: new Dictionary<string, string>(sourceTrade.MetaDic);
|
||||
// 只克隆初始持仓(IsInitial=true),避免将部分平仓后的实时持仓(名义本金已递减)带入续做交易
|
||||
renewTrade.swap_positions = sourceTrade.swap_positions
|
||||
?.Where(p => p.IsInitial)
|
||||
.Select(position =>
|
||||
{
|
||||
var renewPosition = position.Clone();
|
||||
renewPosition.id = 0;
|
||||
renewPosition.PositionId = 0;
|
||||
renewPosition.SwapTradeId = 0;
|
||||
renewPosition.PosiNumber = null;
|
||||
renewPosition.PosiStartDate = defaultTrade.StartDate.Value;
|
||||
renewPosition.PosiMatuirityDate = null;
|
||||
// 预付金腿的 HappenDate 用于后续生成资金流水(ResetMarginAmount 按 HappenDate 过滤),
|
||||
// 续做时设为新交易的起始日;非预付金腿的 HappenDate 无实际用途,置 null
|
||||
renewPosition.HappenDate =
|
||||
position.InterestMode == (int)YLErp.DBModels.InterestModeEnum.初始预付金
|
||||
? defaultTrade.StartDate
|
||||
: null;
|
||||
// 清空运行时累计字段(这些字段在源交易存续期间可能被累计)
|
||||
renewPosition.InterestAmount = 0;
|
||||
renewPosition.InterestFeePending = 0;
|
||||
renewPosition.PosiDividendIncome = 0;
|
||||
renewPosition.PosiTradingFeePending = 0;
|
||||
renewPosition.InterestSwapInterval = null;
|
||||
renewPosition.Obervation = null;
|
||||
return renewPosition;
|
||||
}).ToList() ?? new List<swap_position>();
|
||||
// 清空源交易的事件/持仓快照等集合,避免与源交易共享引用
|
||||
renewTrade.swap_Events = new List<swap_event>();
|
||||
renewTrade.swap_Flow_Events = new List<swap_flow_event>();
|
||||
renewTrade.eod_swaps = new List<eod_swap>();
|
||||
renewTrade.inital_eod_swap_positions = new List<eod_swap_position>();
|
||||
renewTrade.eod_swap_positions = new List<eod_swap_position>();
|
||||
renewTrade.ClientCashInCashOutList = new List<ClientCashInCashOut>();
|
||||
return renewTrade;
|
||||
}
|
||||
/// <summary>
|
||||
/// 详情
|
||||
/// </summary>
|
||||
@@ -115,6 +217,12 @@ namespace YLErp.Web.Controllers
|
||||
{
|
||||
return ShowError("没有找到交易数据");
|
||||
}
|
||||
var marginTemplateConfig = SwapMarginTemplateConfigService.GetConfig();
|
||||
if (string.IsNullOrWhiteSpace(tradeObj.MarginTemplateName)
|
||||
|| !marginTemplateConfig.options.Any(item => item.Value == tradeObj.MarginTemplateName))
|
||||
{
|
||||
tradeObj.MarginTemplateName = marginTemplateConfig.defaultValue;
|
||||
}
|
||||
TradeViewModel model;
|
||||
|
||||
model = new TradeViewModel(tradeObj)
|
||||
@@ -1111,4 +1219,4 @@ namespace YLErp.Web.Controllers
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ namespace YLErp.Web.Controllers
|
||||
TradeDate = valuedateBLL.ValueDate,
|
||||
TraderId = CurUser.UserId,
|
||||
TraderName = CurUser.UserName,
|
||||
MarginTemplateName = "系统默认",
|
||||
MarginTemplateName = null,
|
||||
OpponentRole = "乙方",
|
||||
trade_swap = new trade_swap()
|
||||
{
|
||||
@@ -1196,4 +1196,4 @@ namespace YLErp.Web.Controllers
|
||||
return JsonSuccess("", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</script>
|
||||
<script src="~/front/calendar?v=@(HtmlUtil.JsVersion)"></script>
|
||||
<script src="~/Scripts/app/tradeHelper.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/eventlist.js?v=@HtmlUtil.JsVersion"></script>
|
||||
<script src="~/Scripts/app/swaptrade/eventlist.js?v=@HtmlUtil.JsVersion&eventPriceFormatV=2"></script>
|
||||
}
|
||||
<div id="eventVue">
|
||||
<div class="searchdiv">
|
||||
@@ -28,4 +28,4 @@
|
||||
</div>
|
||||
<form target="_blank" method="post" id="exportForm" action="">
|
||||
<input type="hidden" name="" value="" />
|
||||
</form>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@using YLErp.Web.Models.JsModels;
|
||||
@using YLErp.Commons;
|
||||
@using YLErp.Modules.SwapModule;
|
||||
@model trade
|
||||
@{
|
||||
ViewBag.Title = "交易信息 | 编辑";
|
||||
@@ -13,12 +14,14 @@
|
||||
var jsClient = canChangeClient && Model.ClientId > 0 ? jsClients.FirstOrDefault(n => n.id == Model.ClientId) : null;
|
||||
var jsAssetUnits = JsDataModel.GetAssetUnits(CurUser);
|
||||
var tradeMarginTemplates = new tradeController().GetMarginTemplates();
|
||||
var swapMarginTemplateItems = SwapMarginTemplateConfigService.GetConfig().options;
|
||||
var tradeMarginTemplateItems = new tradeController().GetMarginTemplateItems();
|
||||
var jsAssetUnit = Model.AssetId > 0 ? jsAssetUnits.FirstOrDefault(n => n.id == Model.AssetId) : null;
|
||||
var assetunits = JsDataModel.GetAssetUnits(CurUser);
|
||||
var jsTraders = canAddNewTrader ? JsDataModel.GetTraders(assetunits) : Enumerable.Empty<TraderJsModel>();
|
||||
var jsTrader = canAddNewTrader && Model.TraderId > 0 ? jsTraders.FirstOrDefault(n => n.id == Model.TraderId) : null;
|
||||
var currencys = CurrencyController.getList();
|
||||
var categoryTagOptions = DictionaryBLL.GetList("利息端类别", false, "互换利率");
|
||||
List<string> places = new List<string>();
|
||||
List<string> agencys = new List<string>();
|
||||
var tradingPlaceMap = YLErp.DBModels.Consts.ConsReport.TradingPlaceMapDisplay;
|
||||
@@ -66,6 +69,7 @@
|
||||
jsTrader,
|
||||
jsTraders,
|
||||
tradeMarginTemplates = tradeMarginTemplates,
|
||||
swapMarginTemplateItems = swapMarginTemplateItems,
|
||||
tradeMarginTemplateItems = tradeMarginTemplateItems,
|
||||
needRemark = !isAdd && valuedateBLL.SystemDate.EditTradeNeedRemark,
|
||||
parentTradeId = ViewBag.ParentTradeId,
|
||||
@@ -266,6 +270,14 @@
|
||||
<option value=3>派息日+2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="formlabel half">保证金模板</label>
|
||||
<select v-model="trade.MarginTemplateName">
|
||||
<option v-for="item in page.swapMarginTemplateItems" :key="item.Value" :value="item.Value">
|
||||
{{ item.Text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -351,6 +363,7 @@
|
||||
<th>计息方式</th>
|
||||
<th>重置频率(天)</th>
|
||||
<th>利率准则</th>
|
||||
<th>类别</th>
|
||||
<th>结算规则</th>
|
||||
<th> <button class="btn btn-primary swapadd" type="button" v-on:click="addGetSwapRate">+</button></th>
|
||||
</tr>
|
||||
@@ -401,6 +414,14 @@
|
||||
<option value=-1>前一营业日</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="item.category_tag">
|
||||
@foreach (var option in categoryTagOptions)
|
||||
{
|
||||
<option value="@option.Value">@option.Text</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" v-on:click="initObservationDates(item,1)">设置观察日</button>
|
||||
</td>
|
||||
@@ -429,6 +450,7 @@
|
||||
<th v-if="trade.StructureType!='普通收益互换'">期初标的成交收益率%</th>
|
||||
<th v-if="trade.StructureType=='普通收益互换'">期初标的价格</th>
|
||||
<th>数量</th>
|
||||
<th>基础费率</th>
|
||||
<th>交易费用后付</th>
|
||||
</tr>
|
||||
<tr class="swapflowtr" v-for="item in paySwapList">
|
||||
@@ -452,13 +474,13 @@
|
||||
</a>
|
||||
</td>
|
||||
<td v-if="trade.StructureType!='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapBondDeliveryPrice" v-on:input="onDpPriceInput(item)"></vue-number-input>
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapBondDeliveryPrice" v-on:input="onDpPriceInput(item)"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='DP'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.DP" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span>
|
||||
</td>
|
||||
<td v-if="trade.StructureType!='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiNetNoFeePrice')" v-model="item.PosiNetNoFeePrice" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceInput(item,'CP')"></vue-number-input>
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'PosiNetNoFeePrice')" v-model="item.PosiNetNoFeePrice" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceInput(item,'CP')"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='CP'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.CP" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span>
|
||||
</td>
|
||||
<td v-if="trade.StructureType!='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'InitYtm')" v-model="item.InitYtm" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceInput(item,'YD')"></vue-number-input>
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'InitYtm')" v-model="item.InitYtm" v-bind:format="inputFormatSwapBondNetPriceAndYtm" v-on:input="onBondPriceInput(item,'YD')"></vue-number-input><span style="color:#2e7d32;font-size:11px;margin-left:4px;" v-if="item.isBond && item.bondDriverType==='YD'">源</span><span class="glyphicon glyphicon-pencil" style="color:#ef6c00;font-size:11px;margin-left:4px;cursor:default;" v-if="item.isBond && item.bondManual && item.bondManual.YD" title="手动编辑:该字段由您填写,不会被计算器反算覆盖"></span><a href="javascript:void(0);" v-on:click="resetBondCalc(item)" v-if="item.isBond" style="margin-left:6px;font-size:11px;color:#1565c0;">重算</a>
|
||||
</td>
|
||||
<td v-if="trade.StructureType=='普通收益互换'">
|
||||
<vue-number-input :key="getPosiPriceFormatKey(item,'normalPosiGrossPrice')" v-model="item.PosiGrossPrice" v-bind:format="inputFormatSwapDeliveryPrice" v-on:input="changeSpotPrice(item)"></vue-number-input>
|
||||
@@ -466,6 +488,16 @@
|
||||
<td>
|
||||
<vue-number-input v-model="item.PosiQuantity" v-on:input="changeQuantity(item)" v-bind:format="inputFormatTradeAmount"></vue-number-input>{{item.underlying!=null?item.underlying.QuoteUnitString:''}}
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="posiFeeModePercent">
|
||||
<vue-number-input v-model="item.PosiTradingFeeUnit" v-on:input="changeTradingFeeUnit(item)" v-bind:format="inputFormatPosiFeePercent"></vue-number-input>
|
||||
<a href="javascript:;" title="点击后切换成单位数量模式" v-on:click="showPayAbsPrice" class="yt-input-group-append" tabindex="-1">%</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<vue-number-input v-model="item.PosiTradingFeeUnit" v-on:input="changeTradingFeeUnit(item)" v-bind:format="inputFormatPosiFeeUnit"></vue-number-input>
|
||||
<a href="javascript:;" title="点击后切换成百分比模式" v-on:click="showPayPercentPrice" class="yt-input-group-append" tabindex="-1">¥</a>
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<vue-number-input v-model="item.PosiTradingFeePending" v-on:input="changeTradingFee(item)" v-bind:format="inputFormatTradeSinglePrice"></vue-number-input>
|
||||
<div class="bubble-box">我方{{item.PosiDirection==1?"支付":"收取"}}交易费用</div>
|
||||
|
||||
@@ -236,6 +236,10 @@
|
||||
<td>派息金额支付日</td>
|
||||
<td class="color-bule">@(trade.trade_extend.ExtendObj.DividendPayDate == 0 ? "到期结算日" : "派息日+" + (trade.trade_extend.ExtendObj.DividendPayDate - 1))</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>保证金模板</td>
|
||||
<td class="color-bule">@trade.MarginTemplateName</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -300,6 +304,7 @@
|
||||
<th>计息方式</th>
|
||||
<th>重置频率(天)</th>
|
||||
<th>利率准则</th>
|
||||
<th>类别</th>
|
||||
<th>结算规则</th>
|
||||
</tr>
|
||||
@if (trade.swap_positions != null)
|
||||
@@ -344,6 +349,7 @@
|
||||
</td>
|
||||
<td>@item.interest_rest_days</td>
|
||||
<td>@((item.interest_rule != null) ? (SwapInterestRule)item.interest_rule : "")</td>
|
||||
<td>@(string.IsNullOrEmpty(item.category_tag) ? "互换利率" : item.category_tag)</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button" onclick="showSwapRate('@(item.InterestSwapInterval)', false)" style="height:22px;">查看</button>
|
||||
</td>
|
||||
@@ -378,6 +384,7 @@
|
||||
<td>@initYtmTitle</td>
|
||||
}
|
||||
<td>数量</td>
|
||||
<td>基础费率</td>
|
||||
<td>交易费用后付</td>
|
||||
</tr>
|
||||
@foreach (var item in paySwapPositions)
|
||||
@@ -403,6 +410,16 @@
|
||||
<td>
|
||||
@item.PosiQuantity.OtcFormat(OtcFormatFlag.StockEqvNotional)
|
||||
</td>
|
||||
<td>
|
||||
@if (item.PosiFeeType == 0)
|
||||
{
|
||||
@(item.PosiTradingFeeUnit.ToString("0.0000") + "%")
|
||||
}
|
||||
else
|
||||
{
|
||||
@item.PosiTradingFeeUnit.ToString("0.00")
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@item.PosiTradingFeePending.OtcFormat(OtcFormatFlag.StockEqvNotional)
|
||||
</td>
|
||||
@@ -504,6 +521,7 @@
|
||||
<th>计息方式</th>
|
||||
<th>重置频率(天)</th>
|
||||
<th>利率准则</th>
|
||||
<th>类别</th>
|
||||
<th>结算规则</th>
|
||||
</tr>
|
||||
@{
|
||||
@@ -545,6 +563,7 @@
|
||||
<td>@(item.InterestType == 0 ? "单利" : "复利")</td>
|
||||
<td>@item.interest_rest_days</td>
|
||||
<td>@((item.interest_rule != null) ? (SwapInterestRule)item.interest_rule : "")</td>
|
||||
<td>@(string.IsNullOrEmpty(item.category_tag) ? "互换利率" : item.category_tag)</td>
|
||||
<td><button class="btn btn-sm btn-outline-danger" type="button" onclick="showSwapRate('@(item.InterestSwapInterval)')" style="height:22px;">查看</button></td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
{
|
||||
@MyControls.Btn("收益结算", string.Format("unWindLongShortSwap('{0}')", tradeModel.EncryptId))
|
||||
}
|
||||
@MyControls.Btn("续做", string.Format("renewTrade('{0}')", tradeModel.EncryptId))
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@
|
||||
return numeral(-1 * cellValue).format("0,0.00") === 'NaN' ? "0" : numeral(-1 * cellValue).format("0,0.00");
|
||||
}
|
||||
else {
|
||||
return numeral(cellValue).format("0,0.000") === 'NaN' ? "0" : numeral(cellValue).format("0,0.000");
|
||||
return numeral(cellValue).format("0,0.00") === 'NaN' ? "0" : numeral(cellValue).format("0,0.00");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* closePercentContract.test.js — 平仓比例口径A(占期初名义本金)前端接线守卫
|
||||
* ============================================================================
|
||||
* 产品需求(不可违背):ClosePercent 永远是"占期初名义本金(NotionalValue)的比例",即口径A。
|
||||
*
|
||||
* 本文件是"接线测试"(wiring test):读 unwindSwapTrade.js 源码文本,断言关键逻辑
|
||||
* 仍然使用口径A的公式。如果有人把公式改成口径B(如 c9071a4e 曾犯的错误),
|
||||
* 对应断言会立即变红。
|
||||
*
|
||||
* 与 swapCalc.test.js 的区别:
|
||||
* swapCalc.test.js 测 SwapCalc 纯函数本身正确性(零件级)
|
||||
* 本文件测 unwindSwapTrade.js 确实在调用这些函数/使用正确公式(装配级)
|
||||
* ============================================================================
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const unwindSrc = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'unwindSwapTrade.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
describe('口径A产品契约:unwindSwapTrade.js 接线守卫', () => {
|
||||
|
||||
// ====================================================================
|
||||
// 契约1:oriClosePercent 必须用 PosiNotionalValue / NotionalValue 计算
|
||||
// 不能硬编码为 1(c9071a4e 的错误)
|
||||
// ====================================================================
|
||||
describe('oriClosePercent 必须为剩余/期初', () => {
|
||||
test('源码中 oriClosePercent 必须包含 PosiNotionalValue / NotionalValue 公式', () => {
|
||||
// 正确代码:this.oriClosePercent = ... PosiNotionalValue / ... NotionalValue
|
||||
expect(unwindSrc).toMatch(/oriClosePercent.*PosiNotionalValue.*\/.*NotionalValue/s);
|
||||
});
|
||||
|
||||
test('源码中 oriClosePercent 不能被硬编码为 1', () => {
|
||||
// c9071a4e 的错误:this.deal.ClosePercent = 1 (直接覆盖)
|
||||
// 检查 initDeal 中不存在 oriClosePercent = 1 的硬编码
|
||||
const initDealSection = unwindSrc.match(/initDeal\(\)[\s\S]*?\},/);
|
||||
expect(initDealSection).toBeTruthy();
|
||||
// 不应出现 oriClosePercent = 1 或 ClosePercent = 1 的硬编码
|
||||
//(CloseMethod===1 时设置 ClosePercent=1 是允许的,但 oriClosePercent 不应被设为1)
|
||||
expect(initDealSection[0]).not.toMatch(/oriClosePercent\s*=\s*1\b/);
|
||||
});
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// 契约2:calcCloseQtyByPercent 必须调用 SwapCalc.calcCloseQtyByOriginalPercent
|
||||
// 不能直接 PositionQty × ClosePercent(c9071a4e 的错误)
|
||||
// ====================================================================
|
||||
describe('calcCloseQtyByPercent 必须调用 SwapCalc', () => {
|
||||
test('源码中 calcCloseQtyByPercent 必须调用 SwapCalc.calcCloseQtyByOriginalPercent', () => {
|
||||
expect(unwindSrc).toContain('SwapCalc.calcCloseQtyByOriginalPercent');
|
||||
});
|
||||
|
||||
test('changeCloseMethod 的部分平仓分支必须调用 calcCloseQtyByPercent', () => {
|
||||
// 正确代码:this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
expect(unwindSrc).toMatch(/calcCloseQtyByPercent\s*\(this\.deal\.ClosePercent\)/);
|
||||
});
|
||||
|
||||
test('changeClosePercent 必须调用 calcCloseQtyByPercent', () => {
|
||||
expect(unwindSrc).toMatch(/this\.deal\.CloseQty\s*=\s*this\.calcCloseQtyByPercent/);
|
||||
});
|
||||
|
||||
test('changeCloseNotionalValue 必须调用 calcCloseQtyByPercent', () => {
|
||||
expect(unwindSrc).toMatch(/this\.deal\.CloseQty\s*=\s*this\.calcCloseQtyByPercent/);
|
||||
});
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// 契约3:CloseNotionalValue 必须用 NotionalValue 做乘数/分母(口径A)
|
||||
// 不能用 PosiNotionalValue(c9071a4e 的错误)
|
||||
// ====================================================================
|
||||
describe('CloseNotionalValue 必须基于 NotionalValue(口径A)', () => {
|
||||
test('changeCloseQty: CloseNotionalValue = ClosePercent × NotionalValue', () => {
|
||||
// 正确:ClosePercent × parseFloat(this.deal.NotionalValue)
|
||||
// 错误:ClosePercent × parseFloat(this.deal.PosiNotionalValue)
|
||||
const changeCloseQtySection = unwindSrc.match(/changeCloseQty\(\)[\s\S]*?\n\s*\},/);
|
||||
expect(changeCloseQtySection).toBeTruthy();
|
||||
expect(changeCloseQtySection[0]).toMatch(/CloseNotionalValue.*NotionalValue/);
|
||||
expect(changeCloseQtySection[0]).not.toMatch(/CloseNotionalValue.*PosiNotionalValue/);
|
||||
});
|
||||
|
||||
test('changeClosePercent: CloseNotionalValue = ClosePercent × NotionalValue', () => {
|
||||
const changeClosePercentSection = unwindSrc.match(/changeClosePercent\(\)[\s\S]*?\n\s*\},/);
|
||||
expect(changeClosePercentSection).toBeTruthy();
|
||||
expect(changeClosePercentSection[0]).toMatch(/CloseNotionalValue.*NotionalValue/);
|
||||
expect(changeClosePercentSection[0]).not.toMatch(/CloseNotionalValue.*PosiNotionalValue/);
|
||||
});
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// 契约4:changeCloseQty 的 ClosePercent 必须乘以 oriClosePercent(口径A→B→A 转换)
|
||||
// 不能直接 CloseQty / PositionQty(c9071a4e 的错误)
|
||||
// ====================================================================
|
||||
describe('changeCloseQty 的 ClosePercent 必须乘以 oriClosePercent', () => {
|
||||
test('ClosePercent = (CloseQty/PositionQty) × oriClosePercent', () => {
|
||||
// 正确:× ori(把占剩余比例转回占期初口径)
|
||||
// 错误:不乘 ori(直接用占剩余比例作为 ClosePercent)
|
||||
const changeCloseQtySection = unwindSrc.match(/changeCloseQty\(\)[\s\S]*?\n\s*\},/);
|
||||
expect(changeCloseQtySection).toBeTruthy();
|
||||
expect(changeCloseQtySection[0]).toMatch(/oriClosePercent/);
|
||||
expect(changeCloseQtySection[0]).toMatch(/\*\s*ori/);
|
||||
});
|
||||
|
||||
test('changeCloseNotionalValue 的 ClosePercent 必须除以 NotionalValue', () => {
|
||||
// 正确:ClosePercent = CloseNotionalValue / NotionalValue
|
||||
// 错误:ClosePercent = CloseNotionalValue / PosiNotionalValue
|
||||
const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/);
|
||||
expect(changeCloseNotionalSection).toBeTruthy();
|
||||
expect(changeCloseNotionalSection[0]).toMatch(/ClosePercent.*NotionalValue/);
|
||||
expect(changeCloseNotionalSection[0]).not.toMatch(/ClosePercent.*PosiNotionalValue/);
|
||||
});
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// 契约5:getInterestList 必须传 notionalValue 和 posiNotionalValue 给后端
|
||||
// 后端 GetUnwindInterestList 需要这两个值做 A→B 转换
|
||||
// 如果删掉(c9071a4e 的错误),后端不转换,利息用错口径计算
|
||||
// ====================================================================
|
||||
describe('getInterestList 必须传 notionalValue/posiNotionalValue', () => {
|
||||
test('postData 必须包含 notionalValue', () => {
|
||||
expect(unwindSrc).toMatch(/notionalValue:\s*thisObj\.deal\.NotionalValue/);
|
||||
});
|
||||
|
||||
test('postData 必须包含 posiNotionalValue', () => {
|
||||
expect(unwindSrc).toMatch(/posiNotionalValue:\s*thisObj\.deal\.PosiNotionalValue/);
|
||||
});
|
||||
|
||||
test('getInterestList 的 postData 不能只有 closePercent 而缺少 notionalValue', () => {
|
||||
// 精确匹配 getInterestList 方法定义(以 getInterestList() { 开头,到 main.post 结束)
|
||||
// 匹配模式:方法名+参数列表+花括号开始,一直到包含 main.post 的 postData 定义
|
||||
const methodMatch = unwindSrc.match(
|
||||
/getInterestList\(\)\s*\{[\s\S]*?var\s+postData\s*=\s*\{[^}]*\}/
|
||||
);
|
||||
expect(methodMatch).toBeTruthy();
|
||||
const postData = methodMatch[0];
|
||||
expect(postData).toContain('notionalValue');
|
||||
expect(postData).toContain('posiNotionalValue');
|
||||
});
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// 契约6:CloseMethod 赋值对象必须是 deal(不是 floatPosition)
|
||||
// c9071a4e 在 changeClosePercent 中误赋值到 floatPosition.CloseMethod
|
||||
// ====================================================================
|
||||
describe('CloseMethod 必须赋值给 deal', () => {
|
||||
test('changeClosePercent 的 CloseMethod 必须赋值给 this.deal', () => {
|
||||
const changeClosePercentSection = unwindSrc.match(/changeClosePercent\(\)[\s\S]*?\n\s*\},/);
|
||||
expect(changeClosePercentSection).toBeTruthy();
|
||||
expect(changeClosePercentSection[0]).toMatch(/this\.deal\.CloseMethod\s*=/);
|
||||
expect(changeClosePercentSection[0]).not.toMatch(/this\.floatPosition\.CloseMethod\s*=/);
|
||||
});
|
||||
|
||||
test('changeCloseNotionalValue 的 CloseMethod 必须赋值给 this.deal', () => {
|
||||
const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/);
|
||||
expect(changeCloseNotionalSection).toBeTruthy();
|
||||
expect(changeCloseNotionalSection[0]).toMatch(/this\.deal\.CloseMethod\s*=/);
|
||||
expect(changeCloseNotionalSection[0]).not.toMatch(/this\.floatPosition\.CloseMethod\s*=/);
|
||||
});
|
||||
|
||||
test('changeCloseNotionalValue 必须设置 CloseMethod(不能删除)', () => {
|
||||
// c9071a4e 完全删除了 changeCloseNotionalValue 中的 CloseMethod 判断
|
||||
const changeCloseNotionalSection = unwindSrc.match(/changeCloseNotionalValue\(\)[\s\S]*?\n\s*\},/);
|
||||
expect(changeCloseNotionalSection).toBeTruthy();
|
||||
expect(changeCloseNotionalSection[0]).toMatch(/CloseMethod/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
function createNumberFormat(precision) {
|
||||
const formatter = (value) => Number(Number(value || 0).toFixed(precision));
|
||||
formatter.precision = precision;
|
||||
return formatter;
|
||||
}
|
||||
|
||||
function loadUnwindHelpers() {
|
||||
const filePath = path.join(__dirname, '../wwwroot/Scripts/app/swaptrade/unwindSwapTrade.js');
|
||||
const code = fs.readFileSync(filePath, 'utf8') + '\nmodule.exports = { swapPosiFeeCalc, consPosiFeeType };';
|
||||
|
||||
const stockEqvNotional = createNumberFormat(2);
|
||||
const sandbox = {
|
||||
module: { exports: {} },
|
||||
exports: {},
|
||||
console,
|
||||
require,
|
||||
window: { otcformat: { options: {} } },
|
||||
otcformat: {
|
||||
options: {},
|
||||
trading: {
|
||||
premiumRateP: { precision: 4 },
|
||||
tradePrice: { precision: 4 },
|
||||
notional: { precision: 6 },
|
||||
StockEqvNotional: stockEqvNotional,
|
||||
marginRateP: { precision: 4 },
|
||||
umpriceP: { precision: 4 }
|
||||
},
|
||||
fixed6: createNumberFormat(6)
|
||||
},
|
||||
model: {
|
||||
ValueDate: '2026-07-27',
|
||||
FlowEvents: [],
|
||||
StructureType: '',
|
||||
TradeStartDate: ''
|
||||
},
|
||||
isUseApproval: false,
|
||||
Vue: function (options) { return options; },
|
||||
FastVue: {
|
||||
vueDatePicker() { return {}; },
|
||||
vueNumberInput() { return {}; }
|
||||
},
|
||||
tradeHelper: { IsBond() { return false; } },
|
||||
main: {
|
||||
post() {
|
||||
return {
|
||||
done() { return this; }
|
||||
};
|
||||
},
|
||||
message() { }
|
||||
},
|
||||
SwapCalc: {
|
||||
roundHalfAwayFromZero(value) { return value; },
|
||||
calcCloseQtyByOriginalPercent() { return 0; }
|
||||
},
|
||||
_: {
|
||||
round(value, precision) {
|
||||
return Number(Number(value || 0).toFixed(precision || 0));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sandbox.window.otcformat = sandbox.otcformat;
|
||||
vm.runInNewContext(code, sandbox, { filename: filePath });
|
||||
return sandbox.module.exports;
|
||||
}
|
||||
|
||||
function expectClose(actual, expected, tolerance) {
|
||||
expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance || 1e-6);
|
||||
}
|
||||
|
||||
describe('unwindSwapTrade 基础费率计算', () => {
|
||||
const { swapPosiFeeCalc, consPosiFeeType } = loadUnwindHelpers();
|
||||
|
||||
test('百分比模式按平仓名义本金计算并保留两位', () => {
|
||||
const result = swapPosiFeeCalc.calcTradingFee(consPosiFeeType.Percent, 0.1234, 1000000, 5000);
|
||||
expectClose(result, 1234.00);
|
||||
});
|
||||
|
||||
test('单位数量模式按平仓数量计算并保留两位', () => {
|
||||
const result = swapPosiFeeCalc.calcTradingFee(consPosiFeeType.Unit, 1.235, 1000000, 10);
|
||||
expectClose(result, 12.35);
|
||||
});
|
||||
|
||||
test('未知模式默认按百分比模式处理', () => {
|
||||
const result = swapPosiFeeCalc.calcTradingFee(99, 0.1, 200000, 10);
|
||||
expectClose(result, 200.00);
|
||||
});
|
||||
});
|
||||
@@ -246,7 +246,7 @@ function getColModelDefault() {
|
||||
width: 85,
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
},
|
||||
{
|
||||
name: 'TradeType2',
|
||||
@@ -374,7 +374,7 @@ function getColModelDefault() {
|
||||
width: 65,
|
||||
align: 'center',
|
||||
sortable: false,
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
});
|
||||
|
||||
return tradeHelper.getAmountToNotional(colModelGrid);
|
||||
@@ -388,6 +388,14 @@ function ShowStructFormater(cellValue, options, rowObject) {
|
||||
return cellValue || '';
|
||||
}
|
||||
|
||||
function EventTwoDecimalFormat(cellValue) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
}
|
||||
var price = Number(cellValue);
|
||||
return isFinite(price) ? price.toFixed(2) : cellValue;
|
||||
}
|
||||
|
||||
function StrikeFormatter(cellValue, options, rowObject) {
|
||||
if (rowObject.TradeType === "自定义交易") {
|
||||
return "";
|
||||
@@ -396,20 +404,12 @@ function StrikeFormatter(cellValue, options, rowObject) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
if (rowObject.Strike) {
|
||||
if (rowObject.IsMoneynessOption === "是") {
|
||||
return otcformat.trading.premiumRateP(rowObject.Strike);
|
||||
} else {
|
||||
return otcformat.trading.umprice(rowObject.Strike);
|
||||
}
|
||||
} else {
|
||||
if (rowObject.Strike === 0) {
|
||||
return otcformat.trading.umprice(0);
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '' || cellValue === 'NaN') return '';
|
||||
var strike = Number(cellValue);
|
||||
if (!isFinite(strike)) return '';
|
||||
return rowObject.IsMoneynessOption === "是"
|
||||
? (strike * 100).toFixed(2) + '%'
|
||||
: EventTwoDecimalFormat(strike);
|
||||
}
|
||||
|
||||
function CommissionFormatter(cellValue, options, rowObject) {
|
||||
@@ -651,4 +651,4 @@ function showcolumnChooser() {
|
||||
|
||||
function getColModel() {
|
||||
return getColModelDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,6 +361,16 @@ function formatQuotaAbs(obj, fieldName) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function formatFixedTwoDecimals(cellvalue) {
|
||||
if (cellvalue === null || cellvalue === undefined || cellvalue === '' || cellvalue === 'NaN') {
|
||||
return '';
|
||||
}
|
||||
var numberValue = Number(cellvalue);
|
||||
return isFinite(numberValue)
|
||||
? numberValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
: '';
|
||||
}
|
||||
|
||||
function formatQuotaRef(obj, fieldName) {
|
||||
var html = "";
|
||||
var upperValue = obj["Quota_" + fieldName + "_Upper"];
|
||||
@@ -544,8 +554,21 @@ function quotaMonitorUploadForm(url) {
|
||||
}
|
||||
|
||||
function Output() {
|
||||
var fileName = new moment().format("YYYYMMDD") + $('#myTab .active a').text();
|
||||
main.toExcel("listGrid", fileName, "xls", null, [" ", "操作"]);
|
||||
var summaryType = $('#myTab .active a').text();
|
||||
var fileName = new moment().format("YYYYMMDD") + summaryType;
|
||||
var formatters = null;
|
||||
if (summaryType === '整体业务汇总' || summaryType === '标的汇总' || summaryType === '客户汇总') {
|
||||
formatters = [{
|
||||
colName: '名义本金',
|
||||
formatter: function (cellvalue, options, rowObject) {
|
||||
if (summaryType === '整体业务汇总' && rowObject.BusinessType === '场内业务') {
|
||||
return '';
|
||||
}
|
||||
return formatFixedTwoDecimals(cellvalue);
|
||||
}
|
||||
}];
|
||||
}
|
||||
main.toExcel("listGrid", fileName, "xls", null, [" ", "操作"], formatters);
|
||||
}
|
||||
|
||||
function confirmAllSelect() {
|
||||
@@ -1090,7 +1113,7 @@ var colModel_undelrying = [
|
||||
align: 'right',
|
||||
sortable: false,
|
||||
formatter: function (cellvalue, options, rowObject) {
|
||||
return !cellvalue || cellvalue == "NaN" ? "" : cellvalue.toLocaleString();
|
||||
return formatFixedTwoDecimals(cellvalue);
|
||||
},
|
||||
cellattr: function (cellvalue, options, rowObject) {
|
||||
var style = checkQuota(rowObject, 'StockEqvNotional');
|
||||
@@ -1507,7 +1530,7 @@ var colModel_client = [
|
||||
align: 'right',
|
||||
sortable: false,
|
||||
formatter: function (cellvalue, options, rowObject) {
|
||||
return !cellvalue || cellvalue == "NaN" ? "" : cellvalue.toLocaleString();
|
||||
return formatFixedTwoDecimals(cellvalue);
|
||||
},
|
||||
cellattr: function (cellvalue, options, rowObject) {
|
||||
var style = "style='" + checkQuota(rowObject, 'StockEqvNotional') + "'";
|
||||
@@ -1658,10 +1681,7 @@ var colModel_global = [
|
||||
if (rowObject.BusinessType == "场内业务") {
|
||||
return '<div class="lineCss"></div>';
|
||||
}
|
||||
if (page.IsGuoXin && cellvalue != null) {
|
||||
return cellvalue == "NaN" ? "" : cellvalue.toLocaleString();
|
||||
}
|
||||
return !cellvalue || cellvalue == "NaN" ? "" : cellvalue.toLocaleString();
|
||||
return formatFixedTwoDecimals(cellvalue);
|
||||
},
|
||||
cellattr: function (cellvalue, options, rowObject) {
|
||||
var style = checkQuota(rowObject, 'StockEqvNotional');
|
||||
@@ -2132,4 +2152,4 @@ var colModel_Log = [
|
||||
align: 'center',
|
||||
sortable: false
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
@@ -13,20 +13,12 @@ const colModelGrid = (function () {
|
||||
}
|
||||
|
||||
function StrikeFormatter(cellValue, options, rowObject) {
|
||||
if (rowObject.Strike) {
|
||||
if (rowObject.IsMoneynessOption === "是") {
|
||||
return otcformat.trading.premiumRateP(rowObject.Strike);
|
||||
} else {
|
||||
return otcformat.trading.umprice(rowObject.Strike);
|
||||
}
|
||||
} else {
|
||||
if (rowObject.Strike === 0) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '' || cellValue === 'NaN') return '';
|
||||
var strike = Number(cellValue);
|
||||
if (!isFinite(strike)) return '';
|
||||
return rowObject.IsMoneynessOption === "是"
|
||||
? (strike * 100).toFixed(2) + '%'
|
||||
: strike.toFixed(2);
|
||||
}
|
||||
|
||||
var col = [
|
||||
@@ -273,4 +265,4 @@ function setTestValue(list) {
|
||||
testStstus = false;
|
||||
main.alert("计算完成");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,9 @@ function colModelGridEodPosition() {
|
||||
index: 'eodPosition.PosiNetPrice',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: PriceFormat
|
||||
formatter: PriceFormat,
|
||||
exportFormatter: ExportPriceNineDecimalFormat,
|
||||
exportNumberFormat: '0.000000000'
|
||||
}, {
|
||||
name: 'eodPosition.PosiGrossPrice',
|
||||
label: '期初价格-不含费',
|
||||
@@ -176,6 +178,8 @@ function colModelGridEodPosition() {
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: PriceFormat,
|
||||
exportFormatter: ExportPriceNineDecimalFormat,
|
||||
exportNumberFormat: '0.000000000'
|
||||
}, {
|
||||
name: 'eodPosition.PosiQuantity',
|
||||
label: '名义数量',
|
||||
@@ -743,7 +747,7 @@ function exportVisibleColumns() {
|
||||
var tabName = page.tabIndex == 2 ? '框架合约' : '日终持仓';
|
||||
var fileName = '日终持仓风险_互换_' + tabName + (dateStr ? '_' + dateStr : '');
|
||||
if (page.tabIndex != 2) {
|
||||
main.exportVisibleColumnsToExcel(jgrid, fileName, null);
|
||||
exportEodPositionRows(jgrid, fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -770,6 +774,26 @@ function exportVisibleColumns() {
|
||||
});
|
||||
}
|
||||
|
||||
function exportEodPositionRows(jgrid, fileName) {
|
||||
var exportPostData = $.extend({}, GetPostData(), {
|
||||
page: jgrid.jqGrid('getGridParam', 'page'),
|
||||
rows: jgrid.jqGrid('getGridParam', 'rowNum'),
|
||||
sidx: jgrid.jqGrid('getGridParam', 'sortname'),
|
||||
sord: jgrid.jqGrid('getGridParam', 'sortorder')
|
||||
});
|
||||
$.ajax({
|
||||
url: queryurl,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
traditional: true,
|
||||
data: exportPostData
|
||||
}).done(function (result) {
|
||||
main.exportVisibleColumnsToExcel(jgrid, fileName, null, result && result.rows ? result.rows : []);
|
||||
}).fail(function () {
|
||||
main.message && main.message('导出失败,无法获取日终持仓数据');
|
||||
});
|
||||
}
|
||||
|
||||
function getVisibleEodSwapBusinessColumnNames(jgrid) {
|
||||
var colModel = jgrid.jqGrid('getGridParam', 'colModel') || [];
|
||||
return colModel.filter(function (col) {
|
||||
@@ -803,6 +827,14 @@ function PriceFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.umprice(cellValue);
|
||||
}
|
||||
|
||||
function ExportPriceNineDecimalFormat(cellValue) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
}
|
||||
var price = Number(cellValue);
|
||||
return isFinite(price) ? price.toFixed(9) : cellValue;
|
||||
}
|
||||
|
||||
function RealizedPnlFormat(cellValue, options, rowObject) {
|
||||
return otcformat.trading.tradePrice(cellValue);
|
||||
}
|
||||
|
||||
@@ -176,36 +176,31 @@ var getColModelGrid = function () {
|
||||
label: '成交全价',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'TradingAmountFeeAvg',
|
||||
label: '成交全价(含费)',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'TradingAmountNetAvg',
|
||||
label: '成交净价',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'InitYtm',
|
||||
label: '成交收益率',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: function (cellValue, options, rowObject) {
|
||||
if (cellValue == null) {
|
||||
return "";
|
||||
}
|
||||
return otcformat.trading.premiumRateP(cellValue);
|
||||
}
|
||||
formatter: EventNineDecimalFormat
|
||||
}, {
|
||||
name: 'TradingAmountNetFeeAvg',
|
||||
label: '成交净价(含费)',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'Quantity',
|
||||
label: '成交数量/张数',
|
||||
@@ -217,7 +212,7 @@ var getColModelGrid = function () {
|
||||
label: '成交金额(元)',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'ContractSize',
|
||||
label: '乘数',
|
||||
@@ -230,31 +225,31 @@ var getColModelGrid = function () {
|
||||
label: '交易费用佣金',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'TradingFeePending',
|
||||
label: '待结算交易费用佣金',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'DividendPending',
|
||||
label: '待结算分红收益',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'MarkClosePnl',
|
||||
label: '浮动端平仓盈亏·浮动',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}, {
|
||||
name: 'DividendIn',
|
||||
label: '浮动端平仓盈亏·分红',
|
||||
width: 160,
|
||||
align: 'center',
|
||||
formatter: otcformat.trading.umprice
|
||||
formatter: EventTwoDecimalFormat
|
||||
}
|
||||
];
|
||||
return col;
|
||||
@@ -262,6 +257,23 @@ var getColModelGrid = function () {
|
||||
|
||||
var colModelGrid = getColModelGrid();
|
||||
|
||||
function EventTwoDecimalFormat(cellValue) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
}
|
||||
var price = Number(cellValue);
|
||||
return isFinite(price) ? price.toFixed(2) : cellValue;
|
||||
}
|
||||
|
||||
function EventNineDecimalFormat(cellValue) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '';
|
||||
}
|
||||
var price = Number(cellValue);
|
||||
return isFinite(price) ? price.toFixed(9) : cellValue;
|
||||
}
|
||||
|
||||
|
||||
function gridComplete() {
|
||||
$('.ui-jqgrid-bdiv', '#gbox_listGrid').floatingScroll();
|
||||
}
|
||||
|
||||
@@ -12,11 +12,39 @@ const inputFormatTradeAmount = Object.freeze({ precision: otcformat.trading.noti
|
||||
const inputFormatSwapRate = Object.freeze({ precision: otcformat.trading.premiumRateP.precision, 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 inputFormatPosiFeePercent = Object.freeze({ precision: 4, negative: true, append: '%' });
|
||||
const inputFormatPosiFeeUnit = Object.freeze({ precision: 2, negative: true, append: '' });
|
||||
const inputFormatMarginRate = Object.freeze({ precision: otcformat.trading.marginRateP.precision, append: '%' });
|
||||
const inputFormatSwapDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: false });
|
||||
const inputFormatSwapBondDeliveryPrice = Object.freeze({ precision: 9, negative: true, append: '', percent: true });
|
||||
const inputFormatSwapBondNetPriceAndYtm = Object.freeze({ precision: 9, negative: true, append: '', percent: true });
|
||||
const swapBondStoragePricePrecision = inputFormatSwapBondDeliveryPrice.precision + 2;
|
||||
const consPosiFeeType = Object.freeze({ Percent: 0, Unit: 1 });
|
||||
const swapPosiFeeCalc = Object.freeze({
|
||||
normalizeFeeType(feeType) {
|
||||
return Number(feeType) === consPosiFeeType.Unit ? consPosiFeeType.Unit : consPosiFeeType.Percent;
|
||||
},
|
||||
calcPending(feeType, feeUnit, stockEqvNotional, quantity) {
|
||||
const normalizedFeeType = this.normalizeFeeType(feeType);
|
||||
const normalizedFeeUnit = Number(feeUnit) || 0;
|
||||
const normalizedNotional = Number(stockEqvNotional) || 0;
|
||||
const normalizedQuantity = Number(quantity) || 0;
|
||||
const tradingFeePending = normalizedFeeType === consPosiFeeType.Percent
|
||||
? normalizedFeeUnit / 100 * normalizedNotional
|
||||
: normalizedFeeUnit * normalizedQuantity;
|
||||
return otcformat.trading.tradeSinglePrice(tradingFeePending);
|
||||
},
|
||||
calcFeeUnit(feeType, tradingFeePending, stockEqvNotional, quantity) {
|
||||
const normalizedFeeType = this.normalizeFeeType(feeType);
|
||||
const normalizedTradingFeePending = Number(tradingFeePending) || 0;
|
||||
const normalizedNotional = Number(stockEqvNotional) || 0;
|
||||
const normalizedQuantity = Number(quantity) || 0;
|
||||
if (normalizedFeeType === consPosiFeeType.Percent) {
|
||||
return normalizedNotional === 0 ? 0 : _.round(normalizedTradingFeePending / normalizedNotional * 100, inputFormatPosiFeePercent.precision);
|
||||
}
|
||||
return normalizedQuantity === 0 ? 0 : _.round(normalizedTradingFeePending / normalizedQuantity, inputFormatPosiFeeUnit.precision);
|
||||
}
|
||||
});
|
||||
|
||||
const consUnderlyingFlagBase = (function () {
|
||||
let unSelFlag = tradeHelper.UnderlyingSelectFlag;
|
||||
@@ -153,6 +181,7 @@ const vue = new Vue({
|
||||
currencys: page.currencys,
|
||||
getNotionalSingleFee: 0,
|
||||
isSingleFee: page.Trade.trade_extend.ExtendObj.OpenFeeType == 0,
|
||||
posiFeeModePercent: true,
|
||||
observation: {//互换观察日
|
||||
ObservationInterval: "",
|
||||
IntervalList: [],
|
||||
@@ -236,6 +265,41 @@ const vue = new Vue({
|
||||
const isBond = tradeHelper.IsBond(item && item.UnderlyingInstrumentType);
|
||||
return `${index}-${field}-${isBond ? 'bond' : 'other'}`;
|
||||
},
|
||||
getCurrentPosiFeeType() {
|
||||
return this.posiFeeModePercent ? consPosiFeeType.Percent : consPosiFeeType.Unit;
|
||||
},
|
||||
normalizePosiFeeType(feeType) {
|
||||
return swapPosiFeeCalc.normalizeFeeType(feeType);
|
||||
},
|
||||
syncPosiFeeModeByItem(item) {
|
||||
this.posiFeeModePercent = this.normalizePosiFeeType(item && item.PosiFeeType) !== consPosiFeeType.Unit;
|
||||
},
|
||||
syncPayItemFeeType(item) {
|
||||
item.PosiFeeType = this.getCurrentPosiFeeType();
|
||||
},
|
||||
refreshTradingFeePendingByUnit(item) {
|
||||
this.syncPayItemFeeType(item);
|
||||
item.PosiTradingFeePending = swapPosiFeeCalc.calcPending(
|
||||
item.PosiFeeType,
|
||||
item.PosiTradingFeeUnit,
|
||||
this.trade.StockEqvNotional,
|
||||
item.PosiQuantity
|
||||
);
|
||||
},
|
||||
refreshTradingFeeUnitByPending(item) {
|
||||
this.syncPayItemFeeType(item);
|
||||
item.PosiTradingFeeUnit = swapPosiFeeCalc.calcFeeUnit(
|
||||
item.PosiFeeType,
|
||||
item.PosiTradingFeePending,
|
||||
this.trade.StockEqvNotional,
|
||||
item.PosiQuantity
|
||||
);
|
||||
},
|
||||
refreshPayTradingFeesByUnit() {
|
||||
this.paySwapList.forEach(item => {
|
||||
this.refreshTradingFeePendingByUnit(item);
|
||||
});
|
||||
},
|
||||
changeStructureType() {
|
||||
this.trade.StockEqvNotional = 0;
|
||||
let direction = this.trade.trade_extend.ExtendObj.Direction;
|
||||
@@ -382,17 +446,18 @@ const vue = new Vue({
|
||||
changeContractSize(item) {
|
||||
this.calcNotional();
|
||||
},
|
||||
//变更名义本金
|
||||
//变更名义本金(仅格式化,不反算数量)
|
||||
changeStockEqvNotional() {
|
||||
this.trade.StockEqvNotional = otcformat.trading.StockEqvNotional(this.trade.StockEqvNotional);
|
||||
this.refreshPayTradingFeesByUnit();
|
||||
//计算数量
|
||||
if (this.paySwapList.length > 0) {
|
||||
var item = this.paySwapList[0];
|
||||
var deliveryPrice = this.roundStorageDeliveryPrice(item, item.PosiGrossPrice);
|
||||
var notional = deliveryPrice * item.ContractSize;
|
||||
item.PosiQuantity = notional == 0 ? 0 : _.round(this.trade.StockEqvNotional / notional, page.otcFormatConfig.StockEqvNotional.precision);
|
||||
this.calcNotional();
|
||||
}
|
||||
// if (this.paySwapList.length > 0) {
|
||||
// var item = this.paySwapList[0];
|
||||
// var deliveryPrice = this.roundStorageDeliveryPrice(item, item.PosiGrossPrice);
|
||||
// var notional = deliveryPrice * item.ContractSize;
|
||||
// item.PosiQuantity = notional == 0 ? 0 : _.round(this.trade.StockEqvNotional / notional, page.otcFormatConfig.StockEqvNotional.precision);
|
||||
// this.calcNotional();
|
||||
// }
|
||||
|
||||
},
|
||||
//变更初始预付金 为¥
|
||||
@@ -474,6 +539,7 @@ const vue = new Vue({
|
||||
var stockEqvNotional = SwapCalc.calcStockEqvNotional(deliveryPrice, national);//名义本金=期初价格*数量*乘数
|
||||
this.trade.StockEqvNotional = otcformat.trading.stockEqvNotional(stockEqvNotional);
|
||||
payItem.PosiNotionalValue = this.trade.StockEqvNotional;
|
||||
this.refreshPayTradingFeesByUnit();
|
||||
}
|
||||
},
|
||||
//变更到期日
|
||||
@@ -509,26 +575,27 @@ const vue = new Vue({
|
||||
},
|
||||
//变更单位交易费用
|
||||
changeTradingFeeUnit(item) {
|
||||
//计算交易费用
|
||||
//if (this.trade.trade_extend.ExtendObj.OpenFeeType == 0) {//按手数收费
|
||||
// item.PosiTradingFee = item.ContractSize == 0 ? 0 : otcformat.trading.tradeSinglePrice(item.PosiQuantity * item.PosiTradingFeeUnit / item.ContractSize);
|
||||
//} else {
|
||||
// item.PosiTradingFee = otcformat.trading.tradeSinglePrice(item.PosiQuantity * item.PosiTradingFeeUnit);
|
||||
//}
|
||||
|
||||
this.refreshTradingFeePendingByUnit(item);
|
||||
},
|
||||
//变更交易费用
|
||||
changeTradingFee(item) {
|
||||
//计算单位交易费用
|
||||
//if (item.PosiQuantity == 0) {
|
||||
// item.PosiTradingFeeUnit = 0;
|
||||
// return
|
||||
//}
|
||||
//if (this.trade.trade_extend.ExtendObj.OpenFeeType == 0) {//按手数收费
|
||||
// item.PosiTradingFeeUnit = item.ContractSize == 0 ? 0 : otcformat.trading.tradeSinglePrice(item.PosiTradingFee * item.ContractSize / item.PosiQuantity);
|
||||
//} else {
|
||||
// item.PosiTradingFeeUnit = otcformat.trading.tradeSinglePrice(item.PosiTradingFee / item.PosiQuantity);
|
||||
//}
|
||||
this.refreshTradingFeeUnitByPending(item);
|
||||
},
|
||||
showPayAbsPrice() {
|
||||
this.posiFeeModePercent = false;
|
||||
this.paySwapList.forEach(item => {
|
||||
item.PosiFeeType = consPosiFeeType.Unit;
|
||||
item.PosiTradingFeeUnit = 0;
|
||||
item.PosiTradingFeePending = 0;
|
||||
});
|
||||
},
|
||||
showPayPercentPrice() {
|
||||
this.posiFeeModePercent = true;
|
||||
this.paySwapList.forEach(item => {
|
||||
item.PosiFeeType = consPosiFeeType.Percent;
|
||||
item.PosiTradingFeeUnit = 0;
|
||||
item.PosiTradingFeePending = 0;
|
||||
});
|
||||
},
|
||||
savetrade() {
|
||||
if (!this.checkSubmitData()) {
|
||||
@@ -618,6 +685,7 @@ const vue = new Vue({
|
||||
x.PosiGrossPrice = thisObj.roundStorageDeliveryPrice(x, x.PosiGrossPrice);
|
||||
x.PosiNetNoFeePrice = thisObj.roundStorageBondNetPriceAndYtm(x.PosiNetNoFeePrice);
|
||||
x.InitYtm = x.InitYtm == null ? null : thisObj.roundStorageBondNetPriceAndYtm(x.InitYtm);
|
||||
x.PosiFeeType = thisObj.normalizePosiFeeType(x.PosiFeeType);
|
||||
thisObj.trade.swap_positions.push(x);
|
||||
});
|
||||
} else {
|
||||
@@ -1414,6 +1482,7 @@ const vue = new Vue({
|
||||
thisObj.getSwapList = thisObj.trade.swap_positions.filter(x => { if ((x.UnderlyingCode == null || x.UnderlyingCode.length == 0) && x.IsInitial && (x.InterestMode == 1 || x.InterestMode == 2 || x.InterestMode == 7 || x.InterestMode == 8 || x.InterestMode == 9)) return x; });
|
||||
thisObj.getSwapList.forEach((val, num, arr) => {
|
||||
arr[num].index = num;
|
||||
arr[num].category_tag = arr[num].category_tag || '互换利率';
|
||||
// 解析 InterestSwapInterval 为 SwapIntervalList
|
||||
if (arr[num].InterestSwapInterval && !arr[num].SwapIntervalList) {
|
||||
try {
|
||||
@@ -1438,6 +1507,7 @@ const vue = new Vue({
|
||||
thisObj.paySwapList = thisObj.trade.swap_positions.filter(x => { if (x.UnderlyingCode != null && x.UnderlyingCode.length != 0 && x.IsInitial) return x; });
|
||||
thisObj.paySwapList.forEach((val, num, arr) => {
|
||||
arr[num].index = num;
|
||||
arr[num].PosiFeeType = thisObj.normalizePosiFeeType(arr[num].PosiFeeType);
|
||||
this.StockEqvNotional = val.ContractSize * val.PosiQuantity * val.PosiGrossPrice;
|
||||
// D2 修复:重开(审批重开/刷新)已保存的债券成交单时,三字段互算的手动标志随页面重置而丢失;
|
||||
// 若不锁,用户一旦编辑任一价格字段就会以它为源重新反算、覆盖当初保存的其他两格。
|
||||
@@ -1449,6 +1519,9 @@ const vue = new Vue({
|
||||
thisObj.$set(arr[num], 'bondManual', { CP: true, DP: true, YD: true });
|
||||
}
|
||||
});
|
||||
if (thisObj.paySwapList.length > 0) {
|
||||
thisObj.syncPosiFeeModeByItem(thisObj.paySwapList[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (thisObj.paySwapList.length == 0) {
|
||||
@@ -1502,7 +1575,8 @@ const vue = new Vue({
|
||||
HappenDate: null,//发生日期,
|
||||
Currency: 'CNY',//币种
|
||||
interest_rest_days: 7,//重置频率
|
||||
interest_rule: null//利率准则
|
||||
interest_rule: null,//利率准则
|
||||
category_tag: '互换利率'//类别
|
||||
}
|
||||
thisObj.getSwapList.push(getSwap);
|
||||
},
|
||||
@@ -1574,6 +1648,7 @@ const vue = new Vue({
|
||||
PosiTradingFee: 0,//交易费用
|
||||
PosiTradingFeePending: 0,//交易费用后付
|
||||
PosiTradingFeeUnit: 0,//单位交易费用
|
||||
PosiFeeType: thisObj.getCurrentPosiFeeType(),//单位交易费用模式
|
||||
InterestDirection: 0,//利息收支方式
|
||||
InterestRateDefault: 0,//计息利率
|
||||
InterestMode: 0,//计息基本类型
|
||||
|
||||
@@ -26,6 +26,10 @@ function editTrade(enid) {
|
||||
window.location.href = `/swapTrade2/tradeEdit/?enid=${enid}`;
|
||||
}
|
||||
|
||||
function renewTrade(enid) {
|
||||
window.location.href = `/swapTrade2/tradeEdit/?enid=0&renewEnid=${encodeURIComponent(enid)}`;
|
||||
}
|
||||
|
||||
function editTradeRemarkInfo(enid) {
|
||||
main.open("修改备注", "/trade/EditRemarkInfo?enid=" + enid, { area: ["700px", "500px"] });
|
||||
}
|
||||
@@ -586,4 +590,4 @@ function SubmissionFields(enid) {
|
||||
|
||||
function SubmissionFieldsHistory(enid) {
|
||||
main.open("报送相关字段填写", "/trade/submissionFieldsHistory?encryptId=" + enid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,9 +423,9 @@ const colModelGrid = (new function () {
|
||||
}, {
|
||||
name: 'trade.ClientName', label: '客户名称', index: 'trade.ClientName', width: 180, align: 'left'
|
||||
}, {
|
||||
name: 'swap_flow_event.Quantity', label: '份额', index: 'swap_flow_event.Quantity', width: 150, align: 'left', formatter: ShowNotionalFormater
|
||||
name: 'swap_flow_event.Quantity', label: '份额', index: 'swap_flow_event.Quantity', width: 150, align: 'left', formatter: otcformat.fixed2
|
||||
}, {
|
||||
name: 'swap_flow_event.TradingAmountAvg', label: '结算价', index: 'swap_flow_event.TradingAmountAvg', width: 150, align: 'left', formatter: otcformat.trading.umprice
|
||||
name: 'swap_flow_event.TradingAmountAvg', label: '结算价', index: 'swap_flow_event.TradingAmountAvg', width: 150, align: 'left', formatter: otcformat.fixed2
|
||||
}, {
|
||||
name: 'swap_event.unwindData.SwapCloseAmount', label: '了结总额', index: 'swap_event.unwindData.SwapCloseAmount', width: 150, align: 'left', formatter: otcformat.trading.StockEqvNotional, sortable: false,
|
||||
}, {
|
||||
|
||||
@@ -8,6 +8,22 @@ const inputFormatEqvNotional = Object.freeze({ precision: otcformat.trading.Stoc
|
||||
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 });
|
||||
const consPosiFeeType = Object.freeze({ Percent: 0, Unit: 1 });
|
||||
const swapPosiFeeCalc = {
|
||||
normalizeFeeType(feeType) {
|
||||
return Number(feeType) === consPosiFeeType.Unit ? consPosiFeeType.Unit : consPosiFeeType.Percent;
|
||||
},
|
||||
calcTradingFee(feeType, feeUnit, closeNotionalValue, closeQty) {
|
||||
const normalizedFeeType = this.normalizeFeeType(feeType);
|
||||
const normalizedFeeUnit = Number(feeUnit) || 0;
|
||||
const normalizedCloseNotionalValue = Number(closeNotionalValue) || 0;
|
||||
const normalizedCloseQty = Number(closeQty) || 0;
|
||||
const tradingFee = normalizedFeeType === consPosiFeeType.Unit
|
||||
? normalizedFeeUnit * normalizedCloseQty
|
||||
: normalizedFeeUnit / 100 * normalizedCloseNotionalValue;
|
||||
return otcformat.trading.StockEqvNotional(_.round(tradingFee, 2));
|
||||
}
|
||||
};
|
||||
let ValueDate = model.ValueDate;
|
||||
const vue = new Vue({
|
||||
el: '#vueDiv',
|
||||
@@ -140,6 +156,7 @@ const vue = new Vue({
|
||||
this.deal.CloseQty = this.calcCloseQtyByPercent(this.deal.ClosePercent);
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.refreshTradingFeeByUnit();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
},
|
||||
@@ -153,6 +170,13 @@ const vue = new Vue({
|
||||
calcTradingFeePending() {
|
||||
this.floatPosition.TradingFeePending = this.floatPosition.BeforeCloseFee * parseFloat(this.deal.ClosePercent);
|
||||
},
|
||||
refreshTradingFeeByUnit() {
|
||||
this.floatPosition.TradingFee = swapPosiFeeCalc.calcTradingFee(
|
||||
this.floatPosition.PosiFeeType,
|
||||
this.floatPosition.PosiTradingFeeUnit,
|
||||
this.deal.CloseNotionalValue,
|
||||
this.deal.CloseQty);
|
||||
},
|
||||
changeCloseQty() {//修改平仓数量
|
||||
if (parseFloat(this.deal.CloseQty) > parseFloat(this.deal.PositionQty)) {
|
||||
main.message("平仓数量不能超过持仓数量");
|
||||
@@ -169,6 +193,7 @@ const vue = new Vue({
|
||||
// 占期初口径:平仓名义本金 = 平仓比例 × 期初名义本金(NotionalValue)
|
||||
this.deal.CloseNotionalValue = otcformat.trading.StockEqvNotional(parseFloat(this.deal.ClosePercent) * parseFloat(this.deal.NotionalValue));
|
||||
this.calcTradingFeePending();
|
||||
this.refreshTradingFeeByUnit();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
},
|
||||
@@ -187,6 +212,7 @@ const vue = new Vue({
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.refreshTradingFeeByUnit();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
},
|
||||
@@ -205,6 +231,7 @@ const vue = new Vue({
|
||||
this.deal.CloseMethod = 2;
|
||||
}
|
||||
this.calcTradingFeePending();
|
||||
this.refreshTradingFeeByUnit();
|
||||
this.getInterestList();
|
||||
this.calcFloatClosePnl();
|
||||
},
|
||||
|
||||
@@ -263,18 +263,18 @@ const vue = new Vue({
|
||||
this.CnStockEqvNotional();
|
||||
this.changeStockEqvNotional();
|
||||
},
|
||||
//变更名义本金
|
||||
//变更名义本金(仅格式化,不反算数量)
|
||||
changeStockEqvNotional() {
|
||||
this.trade.StockEqvNotional = otcformat.trading.StockEqvNotional(this.trade.StockEqvNotional);
|
||||
|
||||
if (this.trade.trade_swap.IsPayFloatingProfit) {
|
||||
var spotPrice = isPaySyntheticUnderlying ? payMaxPrice : $("#trade_swap\\.PaySpotPrice").val();
|
||||
var notional = spotPrice != 0 ? this.trade.StockEqvNotional / spotPrice : 0;
|
||||
this.trade.trade_swap.PayNotional = otcformat.trading.notional(Math.abs(notional));
|
||||
var tradeAmount = notional / payCountRatio;
|
||||
this.trade.trade_swap.PayTradeAmount = otcformat.trading.notional(Math.abs(tradeAmount));
|
||||
var lots = notional / payContractSize;
|
||||
this.trade.trade_swap.PayLot = otcformat.trading.notional(Math.abs(lots));
|
||||
// var spotPrice = isPaySyntheticUnderlying ? payMaxPrice : $("#trade_swap\\.PaySpotPrice").val();
|
||||
// var notional = spotPrice != 0 ? this.trade.StockEqvNotional / spotPrice : 0;
|
||||
// this.trade.trade_swap.PayNotional = otcformat.trading.notional(Math.abs(notional));
|
||||
// var tradeAmount = notional / payCountRatio;
|
||||
// this.trade.trade_swap.PayTradeAmount = otcformat.trading.notional(Math.abs(tradeAmount));
|
||||
// var lots = notional / payContractSize;
|
||||
// this.trade.trade_swap.PayLot = otcformat.trading.notional(Math.abs(lots));
|
||||
if (this.isSingleFee) {
|
||||
this.changeGetSingleFee();
|
||||
}
|
||||
@@ -284,13 +284,6 @@ const vue = new Vue({
|
||||
}
|
||||
|
||||
if (this.trade.trade_swap.IsGetFloatingProfit) {
|
||||
var spotPrice = isGetSyntheticUnderlying ? getMaxPrice : $("#trade_swap\\.GetSpotPrice").val();
|
||||
var notional = spotPrice != 0 ? this.trade.StockEqvNotional / spotPrice : 0;
|
||||
this.trade.trade_swap.GetNotional = otcformat.trading.notional(Math.abs(notional));
|
||||
var tradeAmount = notional / getCountRatio;
|
||||
this.trade.trade_swap.GetTradeAmount = otcformat.trading.notional(Math.abs(tradeAmount));
|
||||
var lots = notional / getContractSize;
|
||||
this.trade.trade_swap.GetLot = otcformat.trading.notional(Math.abs(lots));
|
||||
if (this.isSingleFee) {
|
||||
this.changePaySingleFee();
|
||||
}
|
||||
|
||||
@@ -1236,7 +1236,9 @@ main.exportVisibleColumnsToExcel = function (jgrid, fileName, groupConfig, expor
|
||||
exportCols.forEach(function (col) {
|
||||
var colIndex = colModel.indexOf(col);
|
||||
var rawValue = $.jgrid.getAccessor(row, col.name);
|
||||
var formattedValue = gridElement && gridElement.formatter
|
||||
var formattedValue = typeof col.exportFormatter === 'function'
|
||||
? col.exportFormatter(rawValue, row)
|
||||
: gridElement && gridElement.formatter
|
||||
? gridElement.formatter(rowIndex + 1, rawValue, colIndex, row, 'add')
|
||||
: rawValue;
|
||||
formattedRow[col.name] = $('<div>').html(formattedValue == null ? '' : String(formattedValue)).text().replace(/\u00a0/g, '');
|
||||
@@ -1263,7 +1265,9 @@ main.exportVisibleColumnsToExcel = function (jgrid, fileName, groupConfig, expor
|
||||
for (var c = 0; c < exportCols.length; c++) {
|
||||
var val = rows[i][exportCols[c].name];
|
||||
if (val === undefined || val === null) val = '';
|
||||
html += '<td>' + escapeXml(String(val)) + '</td>';
|
||||
var numberFormat = exportCols[c].exportNumberFormat;
|
||||
var style = numberFormat ? ' style="mso-number-format:\'' + escapeXml(String(numberFormat)) + '\';"' : '';
|
||||
html += '<td' + style + '>' + escapeXml(String(val)) + '</td>';
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user