Merge branch 'glms/feature/dotnumber' into 'glms/feature/1.4.2'
fix(swap): 修复利息互换平仓结算中的尾差处理和本金计算问题 See merge request otc-dev/zszq-trs!3
This commit is contained in:
@@ -109,33 +109,33 @@ namespace YLErp
|
||||
/// </summary>
|
||||
public static class InstrumentType
|
||||
{
|
||||
public const string Stock = "Stock";
|
||||
public const string StockIndex = "StockIndex";
|
||||
public const string StockIF = "StockIF";
|
||||
public const string CommoditySpot = "CommoditySpot";
|
||||
public const string CommodityFutures = "CommodityFutures";
|
||||
public const string NewOtcStock = "NewOtcStock";
|
||||
public const string HKStock = "HKStock";
|
||||
public const string HKStockIndex = "HKStockIndex";
|
||||
public const string Fund = "Fund";
|
||||
public const string TBonds = "TBonds";
|
||||
public const string CreditBonds = "CreditBonds"; //信用债
|
||||
public const string OtherBonds = "OtherBonds"; //其它债券
|
||||
public const string Bonds = "Bond"; //债券
|
||||
public const string GoldFutures = "GoldFutures";
|
||||
public const string TBFutures = "TBFutures";
|
||||
public const string OtherFutures = "OtherFutures";
|
||||
public const string GoldSpot = "GoldSpot";
|
||||
public const string OtherSpot = "OtherSpot";
|
||||
public const string AbroadFutures = "AbroadFutures";
|
||||
public const string AbroadSpot = "AbroadSpot";
|
||||
public const string AbroadStock = "AbroadStock";
|
||||
public const string AbroadStockIndex = "AbroadStockIndex";
|
||||
public const string ExRate = "ExRate";
|
||||
public const string Shibor = "Shibor";
|
||||
public const string FixingRepoRate = "FixingRepoRate";
|
||||
public const string OtherRate = "OtherRate";
|
||||
public const string RateYield = "RateYield"; //利率收益率
|
||||
public const string Stock = "Stock"; // 股票
|
||||
public const string StockIndex = "StockIndex"; // 股指
|
||||
public const string StockIF = "StockIF"; // 股指期货
|
||||
public const string CommoditySpot = "CommoditySpot"; // 商品现货
|
||||
public const string CommodityFutures = "CommodityFutures"; // 商品期货
|
||||
public const string NewOtcStock = "NewOtcStock"; // 新三板挂牌股票
|
||||
public const string HKStock = "HKStock"; // 香港股票
|
||||
public const string HKStockIndex = "HKStockIndex"; // 香港股指
|
||||
public const string Fund = "Fund"; // 基金及基金专户
|
||||
public const string TBonds = "TBonds"; // 利率债
|
||||
public const string CreditBonds = "CreditBonds"; // 信用债
|
||||
public const string OtherBonds = "OtherBonds"; // 其它债券
|
||||
public const string Bonds = "Bond"; // 债券
|
||||
public const string GoldFutures = "GoldFutures"; // 黄金期货
|
||||
public const string TBFutures = "TBFutures"; // 国债期货
|
||||
public const string OtherFutures = "OtherFutures"; // 其他期货
|
||||
public const string GoldSpot = "GoldSpot"; // 黄金现货
|
||||
public const string OtherSpot = "OtherSpot"; // 其他现货
|
||||
public const string AbroadFutures = "AbroadFutures"; // 境外期货
|
||||
public const string AbroadSpot = "AbroadSpot"; // 境外现货
|
||||
public const string AbroadStock = "AbroadStock"; // 境外股票
|
||||
public const string AbroadStockIndex = "AbroadStockIndex"; // 境外股指
|
||||
public const string ExRate = "ExRate"; // 汇率
|
||||
public const string Shibor = "Shibor"; // Shibor
|
||||
public const string FixingRepoRate = "FixingRepoRate"; // 银行间回购定盘
|
||||
public const string OtherRate = "OtherRate"; // 其他利率
|
||||
public const string RateYield = "RateYield"; // 利率收益率
|
||||
public const string BondIndex = "BondIndex"; // 债券指数
|
||||
|
||||
//public const string OtherUnderlying = "OtherUnderlying";
|
||||
|
||||
@@ -85,6 +85,21 @@ namespace YLErp.Modules.EodModule
|
||||
|
||||
#endregion
|
||||
|
||||
#region 手工上传日终价格落表分流
|
||||
|
||||
[TestMethod]
|
||||
[Description("股指期货和基金进入股票价格表,Shibor、利率收益率和债券指数进入商品期货价格表")]
|
||||
public void 手工上传_按标的类型选择股票价格表()
|
||||
{
|
||||
Assert.IsTrue(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.StockIF));
|
||||
Assert.IsTrue(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.Fund));
|
||||
Assert.IsFalse(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.Shibor));
|
||||
Assert.IsFalse(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.RateYield));
|
||||
Assert.IsFalse(SettlementPriceImportService.UseStockPriceTable(ConsGlobal.InstrumentType.BondIndex));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 问题3:债券数据来源按是否手工改过区分"人工"/"系统"
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -92,10 +92,12 @@ namespace YLErp.Modules.SwapModule
|
||||
// public 包装:验证自动互换时的“高精度应结 -> 两位实际结算 -> 待实现尾差”链路。
|
||||
public eod_swap_position ExecuteSaveAutoEodInterestPosition(
|
||||
eod_swap_position eodPayPosition, swap_position position, trade td,
|
||||
DateTime valueDate, IntervalModel interval)
|
||||
DateTime valueDate, IntervalModel interval, eod_swap lastEodSwap = null,
|
||||
decimal posiLongNotional = DealInterestsScenarioTest.Principal,
|
||||
decimal orginPv = DealInterestsScenarioTest.Principal)
|
||||
{
|
||||
SaveAutoEodInterestPosition(eodPayPosition, null, position, td, valueDate, interval,
|
||||
null, DealInterestsScenarioTest.Principal, 0m, 1m, DealInterestsScenarioTest.Principal);
|
||||
lastEodSwap, posiLongNotional, 0m, 1m, orginPv);
|
||||
return PersistedPositions.LastOrDefault();
|
||||
}
|
||||
|
||||
@@ -697,10 +699,10 @@ namespace YLErp.Modules.SwapModule
|
||||
var firstCloseFlow = CreateSwapFlowEvent(firstCloseDate, 0.01m);
|
||||
firstCloseFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
firstCloseFlow.InterestPrincipal = 50m;
|
||||
// 模拟 CalcUnwindInterest: 上日尾差 + 本次平仓后的高精度待实现。
|
||||
// 模拟 CalcUnwindInterest:上日尾差加当日新增,尚未扣除本次 0.01 平仓结算。
|
||||
service.AutoInterests = new List<swap_flow_event>
|
||||
{
|
||||
CreateAutoSwapFlowEvent(firstCloseDate, 0.006383561644m)
|
||||
CreateAutoSwapFlowEvent(firstCloseDate, 0.016383561644m)
|
||||
};
|
||||
service.AutoInterests[0].InterestPrincipal = 50m;
|
||||
var firstCloseResult = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
@@ -733,6 +735,239 @@ namespace YLErp.Modules.SwapModule
|
||||
"全平后累计已实现应包含自动互换和两次平仓");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_AUTO_SETTLEMENT_004_PartialCloseAccruesOnlyAfterPreviousEod()
|
||||
{
|
||||
const decimal originalNotional = 10012.35m;
|
||||
const decimal remainingNotional = 5006.17m;
|
||||
const decimal closeNotional = 5006.172835m;
|
||||
const decimal rate = 0.0299m;
|
||||
const decimal pendingInterest = 0.820379534246m;
|
||||
const decimal settledInterest = 0.82m;
|
||||
const decimal expectedPendingInterest = 0.820569301369m;
|
||||
var service = new StubEodPositionService();
|
||||
var td = CreateTrade();
|
||||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "01",
|
||||
SettlementRules = 0
|
||||
});
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestRateDefault = rate;
|
||||
position.InterestSwapInterval = null;
|
||||
var previousEodDate = StartDate.AddDays(2);
|
||||
var closeDate = previousEodDate.AddDays(1);
|
||||
var previousEod = CreatePreEod(previousEodDate, pendingInterest, settledInterest);
|
||||
previousEod.TdInterestPrincipal = originalNotional;
|
||||
var closeFlow = CreateSwapFlowEvent(closeDate, settledInterest);
|
||||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
closeFlow.InterestPrincipal = 5006.18m;
|
||||
closeFlow.InterestRate = rate;
|
||||
|
||||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
previousEod, position, td, closeDate, null,
|
||||
remainingNotional, 0m, new List<swap_flow_event> { closeFlow },
|
||||
closeNotional, false);
|
||||
|
||||
AssertDecimal(expectedPendingInterest, result.InterestIncomeSum,
|
||||
"Partial close must accrue only the day after the previous EOD snapshot");
|
||||
AssertDecimal(remainingNotional, result.TdInterestPrincipal,
|
||||
"The close-day snapshot must carry the remaining principal into the next EOD");
|
||||
AssertDecimal(1.64m, result.RealizedInterest,
|
||||
"Realized interest must include the previous and current settlements");
|
||||
Assert.AreEqual(previousEodDate, service.LastInterestCalculationEodPosition.ValueDate,
|
||||
"The previous EOD ValueDate must be preserved for accrual boundaries");
|
||||
Assert.AreEqual(previousEod.id, service.LastInterestCalculationEodPosition.id,
|
||||
"The previous EOD identity must not be reset to a new position");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_AUTO_SETTLEMENT_005_AutoSettlementKeepsRemainingPrincipal()
|
||||
{
|
||||
const decimal originalNotional = 10012.35m;
|
||||
const decimal remainingNotional = 5006.17m;
|
||||
const decimal rate = 0.0299m;
|
||||
var settlementDate = new DateTime(2026, 7, 14);
|
||||
var service = new StubEodPositionService();
|
||||
var td = CreateTrade();
|
||||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "01",
|
||||
SettlementRules = 0
|
||||
});
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestRateDefault = rate;
|
||||
position.InterestSwapInterval = null;
|
||||
var previousEod = CreatePreEod(settlementDate.AddDays(-1), 2.460947197259m, 1.64m);
|
||||
previousEod.TdInterestPrincipal = remainingNotional;
|
||||
var staleAggregate = new eod_swap { NotionalValue = originalNotional };
|
||||
|
||||
var result = service.ExecuteSaveAutoEodInterestPosition(
|
||||
previousEod, position, td, settlementDate,
|
||||
new IntervalModel { Date = settlementDate, Rate = rate, Settlement = 1 },
|
||||
staleAggregate, remainingNotional, originalNotional);
|
||||
|
||||
AssertDecimal(2.87m, result.TdCloseInterest,
|
||||
"Automatic settlement must round the half-position interest to 2.87");
|
||||
AssertDecimal(remainingNotional, result.TdInterestPrincipal,
|
||||
"Automatic settlement must not restore the original principal from eod_swap");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_AUTO_SETTLEMENT_006_CloseAndAutoSettlementOnlySettlesRemainder()
|
||||
{
|
||||
var settleDate = new DateTime(2026, 7, 16);
|
||||
var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m);
|
||||
var service = new StubEodPositionService
|
||||
{
|
||||
AutoInterests = new List<swap_flow_event> { autoFlow }
|
||||
};
|
||||
var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m);
|
||||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
|
||||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
CreatePreEod(settleDate.AddDays(-1), 0m), CreateInterestPosition(), CreateTrade(),
|
||||
settleDate, new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 },
|
||||
50m, 0m, new List<swap_flow_event> { closeFlow }, 50m, true);
|
||||
|
||||
AssertDecimal(0.73m, autoFlow.InterestAmount,
|
||||
"Automatic settlement must deduct the 0.50 already settled by the close");
|
||||
AssertDecimal(0.73m, autoFlow.InterestClosePnL,
|
||||
"The automatic flow PnL must use the actual 2-decimal remainder");
|
||||
AssertDecimal(1.23m, result.TdCloseInterest,
|
||||
"EOD realized interest must include both manual and automatic settlements");
|
||||
AssertDecimal(0.0045m, result.InterestIncomeSum,
|
||||
"The high-precision total less actual settlements must remain unrealized");
|
||||
AssertDecimal(1.23m, result.RealizedInterest,
|
||||
"Cumulative realized interest must add the combined actual settlement once");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_AUTO_SETTLEMENT_007_PayLegKeepsUnsignedSettlementAndAppliesDirectionOnce()
|
||||
{
|
||||
var settleDate = new DateTime(2026, 7, 16);
|
||||
var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m);
|
||||
autoFlow.InterestDirection = (int)SwapDirectionEnum.支付;
|
||||
autoFlow.InterestClosePnL = -1.2345m;
|
||||
var service = new StubEodPositionService
|
||||
{
|
||||
AutoInterests = new List<swap_flow_event> { autoFlow }
|
||||
};
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestDirection = (int)SwapDirectionEnum.支付;
|
||||
var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m);
|
||||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
closeFlow.InterestDirection = (int)SwapDirectionEnum.支付;
|
||||
closeFlow.InterestClosePnL = -0.50m;
|
||||
|
||||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
CreatePreEod(settleDate.AddDays(-1), 0m), position, CreateTrade(), settleDate,
|
||||
new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 },
|
||||
50m, 0m, new List<swap_flow_event> { closeFlow }, 50m, true);
|
||||
|
||||
AssertDecimal(0.73m, autoFlow.InterestAmount);
|
||||
AssertDecimal(-0.73m, autoFlow.InterestClosePnL);
|
||||
AssertDecimal(1.23m, result.TdCloseInterest,
|
||||
"TdCloseInterest follows the unsigned settlement convention used by other interest branches");
|
||||
AssertDecimal(0.0045m, result.InterestIncomeSum);
|
||||
AssertDecimal(-1.23m, result.RealizedInterest,
|
||||
"The pay direction must be applied exactly once when cumulative realized interest is stored");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_AUTO_SETTLEMENT_008_MarginLegAppliesReversedDirectionOnce()
|
||||
{
|
||||
var settleDate = new DateTime(2026, 7, 16);
|
||||
var autoFlow = CreateAutoSwapFlowEvent(settleDate, 1.2345m);
|
||||
autoFlow.InterestMode = (int)InterestModeEnum.初始预付金;
|
||||
var service = new StubEodPositionService
|
||||
{
|
||||
AutoInterests = new List<swap_flow_event> { autoFlow }
|
||||
};
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestMode = (int)InterestModeEnum.初始预付金;
|
||||
position.InterestPrincipalFix = 50m;
|
||||
var closeFlow = CreateSwapFlowEvent(settleDate, 0.50m);
|
||||
closeFlow.EventType = (int)SwapFlowEventTypeEnum.平仓;
|
||||
closeFlow.InterestMode = (int)InterestModeEnum.初始预付金;
|
||||
|
||||
var result = service.ExecuteSaveAutoEodWithCloseInterestPosition(
|
||||
CreatePreEod(settleDate.AddDays(-1), 0m), position, CreateTrade(), settleDate,
|
||||
new IntervalModel { Date = settleDate, Rate = FixedRate, Settlement = 1 },
|
||||
50m, 0m, new List<swap_flow_event> { closeFlow }, 50m, true);
|
||||
|
||||
AssertDecimal(0.73m, autoFlow.InterestAmount);
|
||||
AssertDecimal(0.73m, autoFlow.InterestClosePnL);
|
||||
AssertDecimal(1.23m, result.TdCloseInterest);
|
||||
AssertDecimal(0.0045m, result.InterestIncomeSum);
|
||||
AssertDecimal(-1.23m, result.RealizedInterest,
|
||||
"A received margin principal produces payable interest, so the margin ratio reverses once");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DI_MANUAL_CLOSE_006_FinalCloseIncludesCloseDateInterest()
|
||||
{
|
||||
const decimal originalNotional = 10012.35m;
|
||||
const decimal remainingNotional = 5006.17m;
|
||||
const decimal rate = 0.0299m;
|
||||
const decimal pendingInterest = 0.411136145205m;
|
||||
const decimal expectedInterest = 0.821230619178m;
|
||||
var closeDate = new DateTime(2026, 7, 16);
|
||||
var service = new StubEodPositionService();
|
||||
var td = CreateTrade();
|
||||
td.trade_extend.ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson
|
||||
{
|
||||
AnnualDays = AnnualDays,
|
||||
InterestCalcMode = "01",
|
||||
SettlementRules = 0
|
||||
});
|
||||
var position = CreateInterestPosition();
|
||||
position.InterestRateDefault = rate;
|
||||
position.InterestSwapInterval = null;
|
||||
var previousEod = CreatePreEod(closeDate.AddDays(-1), pendingInterest, 4.51m);
|
||||
previousEod.TdInterestPrincipal = remainingNotional;
|
||||
var previousFloatingPosition = new eod_swap_position
|
||||
{
|
||||
PosiDirection = (int)SwapDirectionEnum.支付,
|
||||
PosiNotionalValue = remainingNotional
|
||||
};
|
||||
var previousAggregate = new eod_swap
|
||||
{
|
||||
NotionalValue = originalNotional,
|
||||
NotionalValueLong = remainingNotional
|
||||
};
|
||||
var orginPv = SwapDealService.ResolveUnwindPreviousNotional(
|
||||
previousAggregate, new List<eod_swap_position> { previousEod, previousFloatingPosition },
|
||||
remainingNotional);
|
||||
|
||||
AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional(
|
||||
previousAggregate, Array.Empty<eod_swap_position>(), originalNotional),
|
||||
"Missing details must fall back to the aggregate directional notionals");
|
||||
AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional(
|
||||
null, null, remainingNotional),
|
||||
"Missing EOD data must fall back to the current notional");
|
||||
AssertDecimal(remainingNotional, SwapDealService.ResolveUnwindPreviousNotional(
|
||||
new eod_swap { NotionalValue = originalNotional }, Array.Empty<eod_swap_position>(),
|
||||
remainingNotional),
|
||||
"Zero directional notionals must not override a non-zero current remaining notional");
|
||||
|
||||
var result = new SwapDealService(service).GetInterests(
|
||||
td, td.trade_extend, closeDate, closeDate,
|
||||
new List<eod_swap_position> { previousEod }, new List<swap_position> { position },
|
||||
remainingNotional, remainingNotional, 0m, remainingNotional, 1m,
|
||||
(int)SwapEventTypeEnum.平仓, false, false, 1m, orginPv,
|
||||
false, settment: false, newCalcLast: false, closeList: null).Single();
|
||||
|
||||
AssertDecimal(remainingNotional, result.InterestPrincipal,
|
||||
"Final close must accrue on the remaining principal");
|
||||
AssertDecimal(expectedInterest, result.InterestAmount,
|
||||
"InterestCalcMode 01 must include the final close date");
|
||||
AssertDecimal(0.82m, Math.Round(result.InterestAmount, ConsGlobal.MoneyRound,
|
||||
MidpointRounding.AwayFromZero), "Final close cash interest must be 0.82");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// [DI_MATURITY_SETTLEMENT_001] 到期日存在手动互换但未带齐待实现时不能清零;
|
||||
/// 当前事件按两位覆盖全部可结金额后,才可视为最终结算并清零。
|
||||
|
||||
@@ -14,6 +14,12 @@ namespace YLErp.Modules.EodModule
|
||||
|
||||
}
|
||||
|
||||
public static bool UseStockPriceTable(string instrumentType)
|
||||
{
|
||||
return ConsGlobal.InstrumentType.EquityTypes().Contains(instrumentType)
|
||||
|| instrumentType == ConsGlobal.InstrumentType.Fund;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入xlsx数据
|
||||
/// </summary>
|
||||
@@ -134,7 +140,7 @@ namespace YLErp.Modules.EodModule
|
||||
EodPriceService.StampBondOperator(eodPrice, UserId, eodPrice.id == 0);
|
||||
result.SuccessCount++;
|
||||
}
|
||||
else if (!underlying.CalcTypeIsStock())
|
||||
else if (!UseStockPriceTable(underlying.UnderlyingInstrumentType))
|
||||
{
|
||||
var eodPrice = DbContext.eod_commodity_future_price
|
||||
.FirstOrDefault(p => p.ValueDate == item.date && p.UnderlyingCode == underlying.UnderlyingCode);
|
||||
|
||||
@@ -565,7 +565,7 @@ namespace YLErp.Modules.SwapModule
|
||||
var posiShortNotionalValue = shortPositions.Sum(s => s.PosiNotionalValue);// 剩余名义本金
|
||||
var stockEqvNotional = realPostitions.Where(x => x.PosiDirection > 0).Sum(s => s.PosiNotionalValue);
|
||||
var posiNotionalValue = stockEqvNotional * closePercent;//剩余名义本金
|
||||
var orginPv = lastEod != null ? lastEod.NotionalValue : stockEqvNotional;
|
||||
var orginPv = ResolveUnwindPreviousNotional(lastEod, lastEodPositions, stockEqvNotional);
|
||||
var grossPrice = realPostitions.Where(x => x.PosiDirection > 0).FirstOrDefault()?.PosiGrossPrice;
|
||||
var closeList = DbContext.swap_flow_event.Where(x => x.SwapTradeId == tradeId && x.UnwindDate == unwindDate && eventTypes.Contains(x.EventType) && x.DataState == (int)SwapFlowDateStateEnum.完成).ToList();
|
||||
bool tdClose = closeList.Count > 0;
|
||||
@@ -607,6 +607,29 @@ namespace YLErp.Modules.SwapModule
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public static decimal ResolveUnwindPreviousNotional(
|
||||
eod_swap lastEod,
|
||||
IEnumerable<eod_swap_position> lastEodPositions,
|
||||
decimal currentNotional)
|
||||
{
|
||||
var floatingPositions = lastEodPositions?.Where(x => x.PosiDirection > 0).ToList();
|
||||
decimal previousNotional;
|
||||
if (floatingPositions?.Count > 0)
|
||||
{
|
||||
previousNotional = floatingPositions.Sum(x => x.PosiNotionalValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
previousNotional = lastEod == null
|
||||
? currentNotional
|
||||
: Math.Abs(lastEod.NotionalValueLong) + Math.Abs(lastEod.NotionalValueShort);
|
||||
}
|
||||
|
||||
return previousNotional == 0m && currentNotional != 0m
|
||||
? currentNotional
|
||||
: previousNotional;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取利息腿"已通过历史互换结出的累计利息"(用于复利重算时扣除,类比分红的 CalcConsumedDividend)。
|
||||
/// 数据源为事件级 swap_flow_event.InterestAmount(互换/自动互换 完成态事件,互换当时即落库,不依赖日终归档)。
|
||||
|
||||
@@ -1161,13 +1161,7 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
|
||||
var tradeExtend = td.trade_extend.ExtendObj;
|
||||
decimal oriPosiNotionalValue = posiLongNotional + posiShortNational;
|
||||
decimal posiNotionalValue = oriPosiNotionalValue;
|
||||
bool longShort = td.StructureType == ClientMarginTypeEnum.多空组合.ToString();
|
||||
if (lastEodSwap != null)
|
||||
{
|
||||
posiNotionalValue = lastEodSwap.NotionalValue;
|
||||
}
|
||||
decimal posiNotionalValue = posiLongNotional + posiShortNational;
|
||||
decimal closePercent = 1;
|
||||
decimal ratio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m;//收取为正,支付为负
|
||||
if (marginTypes.Contains(position.InterestMode))
|
||||
@@ -1293,10 +1287,11 @@ namespace YLErp.Modules.SwapModule
|
||||
var lastInterestFeeSum = eodPayPosition?.InterestFeeSum ?? 0m;
|
||||
var lastRealizedInterest = eodPayPosition?.RealizedInterest ?? 0m;
|
||||
var lastRealizedInterestFee = eodPayPosition?.RealizedInterestFee ?? 0m;
|
||||
eodPayPosition = new eod_swap_position();
|
||||
// 保留上一日日终标识和计息上下文,部分平仓只从 ValueDate 之后续算,不能重置到交易起始日。
|
||||
eodPayPosition = eodPayPosition?.Clone() ?? new eod_swap_position();
|
||||
eodPayPosition.ClientId = td.ClientId;
|
||||
eodPayPosition.SwapTradeId = td.id;
|
||||
// CalcSwapInterests 按 PositionId 查找上一日日终;id 仍保持 0,沿用盘中平仓的原有计息日期语义。
|
||||
// CalcSwapInterests 按 PositionId 匹配上一日日终。
|
||||
eodPayPosition.PositionId = position.id;
|
||||
eodPayPosition.PosiStartDate = td.StartDate.Value;
|
||||
eodPayPosition.PosiMatuirityDate = td.ExerciseDate.Value;
|
||||
@@ -1338,7 +1333,17 @@ namespace YLErp.Modules.SwapModule
|
||||
preEodPositions.Add(eodPayPosition);
|
||||
var interests = CalcSwapInterests(td, td.trade_extend, valueDate, valueDate, preEodPositions, positions, posiNotionalValue, posiLongNotional, posiShortNational, closeNational, 1, eventType, false, true, grossPrice, orginPv, true, settment: false, newCalcLast: true);
|
||||
decimal TdInterestAmount = interests.Sum(x => x.TdInterestAmount);
|
||||
decimal InterestAmount = interests.Sum(x => x.InterestAmount);
|
||||
decimal interestAmountBeforeSettlement = interests.Sum(x => x.InterestAmount);
|
||||
decimal manualSettledInterestAmount = flowEvents.Sum(x => x.InterestAmount);
|
||||
decimal autoSettledInterestAmount = 0m;
|
||||
if (autoSwap && interests.Count > 0)
|
||||
{
|
||||
autoSettledInterestAmount = RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount);
|
||||
var autoInterest = interests[0];
|
||||
autoInterest.InterestAmount = autoSettledInterestAmount;
|
||||
autoInterest.InterestClosePnL = autoSettledInterestAmount
|
||||
* (autoInterest.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m);
|
||||
}
|
||||
newEodPayPosition.ValueDate = valueDate;
|
||||
newEodPayPosition.PositionId = position.id;
|
||||
UpdateDbOption(newEodPayPosition);
|
||||
@@ -1359,7 +1364,9 @@ namespace YLErp.Modules.SwapModule
|
||||
newEodPayPosition.interest_rest_days = position.interest_rest_days;
|
||||
newEodPayPosition.interest_rule = position.interest_rule;
|
||||
//利息端估值用信息
|
||||
newEodPayPosition.TdInterestPrincipal = interests.Count > 0 ? interests.First().InterestPrincipal : 0;
|
||||
newEodPayPosition.TdInterestPrincipal = position.InterestMode == (int)InterestModeEnum.标的期初全价
|
||||
? posiNotionalValue
|
||||
: interests.Count > 0 ? interests.First().InterestPrincipal : 0;
|
||||
if (interval != null)
|
||||
{
|
||||
newEodPayPosition.TdInterestRate = interval.Rate;
|
||||
@@ -1371,7 +1378,7 @@ namespace YLErp.Modules.SwapModule
|
||||
//当日已实现,平仓时已处理
|
||||
newEodPayPosition.TdInterestFee = flowEvents.Sum(s => s.InterestFee);
|
||||
newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee;
|
||||
newEodPayPosition.TdCloseInterest = flowEvents.Sum(s => s.InterestClosePnL);
|
||||
newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount;
|
||||
var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate);
|
||||
if (position.IsAnnualized)
|
||||
{
|
||||
@@ -1390,7 +1397,8 @@ namespace YLErp.Modules.SwapModule
|
||||
}
|
||||
else
|
||||
{
|
||||
newEodPayPosition.InterestIncomeSum = InterestAmount;
|
||||
newEodPayPosition.InterestIncomeSum = RoundEodInterest(
|
||||
interestAmountBeforeSettlement - newEodPayPosition.TdCloseInterest);
|
||||
newEodPayPosition.InterestFeeSum = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee;
|
||||
}
|
||||
//持仓内容-利息腿-损益统计(本方视角)
|
||||
|
||||
@@ -41,6 +41,8 @@ window.main.swapPricePrecision = {
|
||||
ExRate: { integerDigits: 2, precision: 8 },
|
||||
Shibor: { integerDigits: 2, precision: 4 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4 },
|
||||
RateYield: {integerDigits: 6, precision: 8},
|
||||
BondIndex: {integerDigits: 6, precision: 4},
|
||||
|
||||
// TODO: 利率收益率(6+8)、债券指数(6+4)、黄金期货(6+4)待对应的 UnderlyingInstrumentType 枚举确认后启用。
|
||||
};
|
||||
|
||||
@@ -47,6 +47,68 @@ global.main = {
|
||||
|
||||
const SwapCalc = require('../wwwroot/Scripts/app/swaptrade/swapCalc.js');
|
||||
|
||||
describe('TradeEdit bond price input bindings', () => {
|
||||
const view = fs.readFileSync(path.join(__dirname, '..', 'Views', 'SwapTrade2', 'TradeEdit.cshtml'), 'utf8');
|
||||
const methods = fs.readFileSync(path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'swapTradeEdit.js'), 'utf8');
|
||||
const precisionHelper = fs.readFileSync(path.join(__dirname, '..', 'wwwroot', 'Scripts', 'app', 'swaptrade', 'swapPricePrecisionHelper.js'), 'utf8');
|
||||
|
||||
test('binds the three bond price fields to current Vue methods', () => {
|
||||
expect(view).toMatch(/v-model="item\.PosiGrossPrice"[^>]*v-on:input="onDpPriceInput\(item\)"/);
|
||||
expect(view).toMatch(/v-model="item\.PosiNetNoFeePrice"[^>]*v-on:input="onBondPriceEdit\(item,'CP'\)"/);
|
||||
expect(view).toMatch(/v-model="item\.InitYtm"[^>]*v-on:input="onBondPriceEdit\(item,'YD'\)"/);
|
||||
expect(view).not.toContain('v-on:input="onBondPriceInput(');
|
||||
expect(methods).toMatch(/^\s*onBondPriceEdit\s*\(/m);
|
||||
});
|
||||
|
||||
test('precision input preserves the keydown, input, enter event contract', () => {
|
||||
const helper = new Function('window', precisionHelper + '\nreturn swapPricePrecision;')({});
|
||||
const component = helper.createVueInputComponent();
|
||||
expect(component.template).toContain('@blur="onChange"');
|
||||
expect(component.template).not.toContain('@change="onChange"');
|
||||
const emitted = [];
|
||||
const vm = {
|
||||
text: '99.5',
|
||||
enterPressed: false,
|
||||
format: { precision: 4, percent: true },
|
||||
$emit: (event, value) => emitted.push({ event, value })
|
||||
};
|
||||
Object.keys(component.methods).forEach(name => {
|
||||
vm[name] = component.methods[name].bind(vm);
|
||||
});
|
||||
const target = {
|
||||
value: vm.text,
|
||||
blur: () => vm.onChange({ target })
|
||||
};
|
||||
|
||||
vm.onKeydown({ keyCode: 13, target });
|
||||
|
||||
expect(emitted.map(x => x.event)).toEqual(['keydown', 'input', 'enter']);
|
||||
expect(emitted[1].value).toBe('0.995');
|
||||
expect(emitted[2].value).toBe('0.995');
|
||||
});
|
||||
|
||||
test('equal format refresh does not overwrite uncommitted input', () => {
|
||||
const helper = new Function('window', precisionHelper + '\nreturn swapPricePrecision;')({});
|
||||
const component = helper.createVueInputComponent();
|
||||
const format = { precision: 4, percent: true };
|
||||
const vm = {
|
||||
value: '0.995',
|
||||
text: '99.5',
|
||||
format,
|
||||
formatSnapshot: JSON.stringify(format),
|
||||
$emit: () => {}
|
||||
};
|
||||
Object.keys(component.methods).forEach(name => {
|
||||
vm[name] = component.methods[name].bind(vm);
|
||||
});
|
||||
|
||||
vm.onInput({ target: { value: '99.51' } });
|
||||
component.watch.format.handler.call(vm);
|
||||
|
||||
expect(vm.text).toBe('99.51');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 根因 1:vue-number-input keydown 事件 emit 链路
|
||||
// ============================================================================
|
||||
|
||||
@@ -39,7 +39,9 @@ var swapPricePrecision = (function (global) {
|
||||
AbroadStockIndex: { integerDigits: 8, precision: 4 },
|
||||
ExRate: { integerDigits: 2, precision: 8 },
|
||||
Shibor: { integerDigits: 2, precision: 4 },
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4 }
|
||||
FixingRepoRate: { integerDigits: 2, precision: 4 },
|
||||
RateYield: {integerDigits: 6, precision: 8},
|
||||
BondIndex: {integerDigits: 6, precision: 4},
|
||||
// TODO: Add InterestYield, BondIndex and GoldFutures after their enum values are confirmed.
|
||||
});
|
||||
|
||||
@@ -252,7 +254,7 @@ var swapPricePrecision = (function (global) {
|
||||
}
|
||||
}
|
||||
},
|
||||
template: '<input type="text" :disabled="disabled" :value="text" @input="onInput" @paste="onPaste" @keydown="onKeydown" @change="onChange">'
|
||||
template: '<input type="text" :disabled="disabled" :value="text" @input="onInput" @paste="onPaste" @keydown="onKeydown" @blur="onChange">'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user