Merge remote-tracking branch 'origin/glms/feature/1.4.2' into glms/feature/1.4.2

This commit is contained in:
马冰冰
2026-07-31 11:27:13 +08:00
24 changed files with 425 additions and 89 deletions
+20 -5
View File
@@ -1,4 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
using YLErp.Models;
namespace YLErp
@@ -135,6 +135,9 @@ namespace YLErp
public const string Shibor = "Shibor";
public const string FixingRepoRate = "FixingRepoRate";
public const string OtherRate = "OtherRate";
public const string BondYield = "BondYield"; //债券收益率
public const string BondIndex = "BondIndex"; // 债券指数
//public const string OtherUnderlying = "OtherUnderlying";
@@ -169,6 +172,8 @@ namespace YLErp
//case OtherUnderlying: return "其他标的";
case CreditBonds: return "信用债";
case OtherBonds: return "其它债券";
case BondYield: return "债券收益率";
case BondIndex: return "债券指数";
default: return instType;
}
}
@@ -204,6 +209,8 @@ namespace YLErp
//case "其他标的": return OtherUnderlying;
case "信用债": return CreditBonds;
case "其它债券": return OtherBonds;
case "债券收益率": return BondYield;
case "债券指数": return BondIndex;
default: throw new ServiceException("资产类型不存在");
}
}
@@ -264,6 +271,8 @@ namespace YLErp
new SelectItem { Text = "汇率" ,Value = ExRate },
new SelectItem { Text = "Shibor" ,Value = Shibor},
new SelectItem { Text = "银行间回购定盘" ,Value = FixingRepoRate},
new SelectItem { Text = "债券收益率" ,Value = BondYield},
new SelectItem { Text = "债券指数" ,Value = BondIndex},
//new SelectItem { Text = "其他利率" ,Value = OtherRate},
//new SelectItem { Text = "其他标的" ,Value = OtherUnderlying}
};
@@ -271,7 +280,7 @@ namespace YLErp
public static IEnumerable<string> FutureTypes()
{
return new[] { CommodityFutures, StockIF, GoldFutures, TBFutures, OtherFutures, AbroadFutures };
return new[] { CommodityFutures, StockIF, GoldFutures, TBFutures, OtherFutures, AbroadFutures, BondYield, BondIndex };
}
/// <summary>
@@ -371,8 +380,12 @@ namespace YLErp
/// 类型转换为计算所用类型
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ConvertCalcType(string instType)
public static string ConvertCalcType(string instType,bool pringScene = false)
{
if (pringScene && PS.Config.Company == Configuration.CompanyEnum.)
{
return Bonds;
}
switch (instType)
{
case Stock:
@@ -390,6 +403,8 @@ namespace YLErp
case TBFutures:
case OtherFutures:
case AbroadFutures:
case BondYield:
case BondIndex:
return CommodityFutures;
case CommoditySpot:
case GoldSpot:
@@ -414,7 +429,7 @@ namespace YLErp
public static string[] GetFutureTypes()
{
return new[] { CommodityFutures, StockIF,
GoldFutures, TBFutures, AbroadFutures, OtherFutures };
GoldFutures, TBFutures, AbroadFutures, OtherFutures,BondYield,BondIndex };
}
/// <summary>
@@ -458,7 +473,7 @@ namespace YLErp
Bonds,
TBonds,
CreditBonds,
OtherBonds
OtherBonds,
};
}
}
+1 -1
View File
@@ -13,7 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
<PackageReference Include="NPOI" Version="2.5.6" />
<PackageReference Include="RestSharp" Version="108.0.3" />
<PackageReference Include="Qdp.Pricing.Base" Version="1.0.1" />
<PackageReference Include="Qdp.Pricing.Base" Version="1.1.5-glms" />
<PackageReference Include="TinyPinyin.Net" Version="1.0.2" />
<PackageReference Include="YieldChain.Core" Version="1.0.3" />
</ItemGroup>
@@ -1,4 +1,4 @@
using Qdp.ComputeService.Data.CommonModels.ValuationParams.Equity;
using Qdp.ComputeService.Data.CommonModels.ValuationParams.Equity;
using Qdp.ComputeService.Data.CommonModels.ValuationParams.FixedIncome;
using Qdp.ComputeServiceV2.Data.CommonModels.TradeInfos;
using Qdp.ComputeServiceV2.Data.CommonModels.TradeInfos.Options;
@@ -59,7 +59,7 @@ namespace YLErp.BLL.Calculation.V2
{
underlying = underlying.Clone();
underlying.UnderlyingInstrumentType = ConsGlobal.InstrumentType.ConvertCalcType(underlying.UnderlyingInstrumentType);
underlying.UnderlyingInstrumentType = ConsGlobal.InstrumentType.ConvertCalcType(underlying.UnderlyingInstrumentType,true);
switch (trade.TradeType)
{
+22 -1
View File
@@ -1,4 +1,4 @@
using BaseOUDAL;
using BaseOUDAL;
using YLErp.Commons;
using YLErp.Helpers;
using YLErp.Modules;
@@ -211,6 +211,27 @@ namespace YLErp.Model
public double DividendRate { get; set; }
/// <summary>
/// Delta_R
/// </summary>
public double? Delta_r { get; set; }
public double? Delta_r_1bp { get; set; }
public double? Dv01 { get; set; }
public double? Gamma_r { get; set; }
public double? Gamma_r_1bp { get; set; }
public double? Vega_r { get; set; }
public double? Vega_r_1bp { get; set; }
/// <summary>
/// 维持预付金
/// </summary>
@@ -1,4 +1,4 @@
using Qdp.Foundation.Implementations;
using Qdp.Foundation.Implementations;
using Qdp.Pricing.Base.Enums;
using Qdp.Pricing.Base.Implementations;
using YLErp.Enums;
@@ -105,7 +105,7 @@ namespace YLErp.Modules.CalculationModule
public string underlyingInstrumentType
{
get => _underlyingInstrumentType;
set => _underlyingInstrumentType = ConsGlobal.InstrumentType.ConvertCalcType(value);
set => _underlyingInstrumentType = ConsGlobal.InstrumentType.ConvertCalcType(value,true);
}
/// <summary>
@@ -1,4 +1,4 @@
using Qdp.Foundation.Implementations;
using Qdp.Foundation.Implementations;
using Qdp.Pricing.Base.Implementations;
using Qdp.Pricing.Base.Interfaces;
using Qdp.Pricing.Base.Utilities;
@@ -570,7 +570,7 @@ namespace YLErp.Modules.CalculationModule
{
underlying = underlying.Clone();
underlying.UnderlyingInstrumentType = ConsGlobal.InstrumentType.ConvertCalcType(underlying.UnderlyingInstrumentType);
underlying.UnderlyingInstrumentType = ConsGlobal.InstrumentType.ConvertCalcType(underlying.UnderlyingInstrumentType,true);
switch (trade.TradeType)
{
@@ -1,4 +1,4 @@
using YLErp.Abstract.DataProviders;
using YLErp.Abstract.DataProviders;
using YLErp.Helpers;
using YLErp.Models;
using YLErp.Modules.TradeModule.DealModule;
@@ -118,13 +118,13 @@ namespace YLErp.Modules.DataProviderModule
ValueDate = ValueDate,
UnderlyingId = um.id,
UnderlyingCode = um.UnderlyingCode,
ClosePrice = 0,
SettlePrice = 0,
ClosePrice = (double)(eodprice.net_price ?? 0) / 100,
SettlePrice = (double)(eodprice.dirty_price_close ?? 0) / 100,
HighPrice = 0,
LowPrice = 0,
UnderlyingStatus = "正常运行",
UnderlyingInstrumentType = "Bonds",
ReferencePrice = 0,
ReferencePrice = (double)(eodprice.yield ?? 0),
DeciSettlePrice = eodprice.dirty_price_close,
DeciClosePrice = eodprice.net_price,
DeciReferencePrice = eodprice.yield,
@@ -1,4 +1,4 @@
using BaseOUDAL;
using BaseOUDAL;
using System;
using System.Data;
using System.Diagnostics;
@@ -339,7 +339,9 @@ namespace YLErp.Modules.EodModule
var tcActions = new[] { ClientCashInCashOut._平仓费,
ClientCashInCashOut._行权费,ClientCashInCashOut._票息,ClientCashInCashOut._互换,ClientCashInCashOut._其他};
var underlyingCodes = searchResult.rows.Select(O => O.UnderlyingCode).Distinct().ToList();
var greeksHandleService = new GLMSGreeksHandleService();
greeksHandleService.InitData(req.ValueDate, underlyingCodes);
foreach (var r in searchResult.rows)
{
var um = DataCacheProvider.GetUnderlyingDataSource().GetData(r.UnderlyingCode);
@@ -470,6 +472,7 @@ namespace YLErp.Modules.EodModule
r.DeltaLots = (r.Delta ?? 0) / um.ContractSize;
}
r.Rho *= 100;
greeksHandleService.Handle(r);
if (r.trade == null)
{
r.TradeSinglePrice = r.etcTradePrice / r.TradeAmount;
@@ -1118,6 +1121,9 @@ namespace YLErp.Modules.EodModule
.AsEnumerable()
.GroupBy(O => O.ParentTradeId)
.ToDictionary(K => K.Key, V => V.Select(O => new xodTradeBase() { TradeJson = O.TradeJson }));
var underlyingCodes = searchResult.rows.Select(O => O.UnderlyingCode).Distinct().ToList();
var greeksHandleService = new GLMSGreeksHandleService();
greeksHandleService.InitData(req.ValueDate, underlyingCodes);
foreach (var r in searchResult.rows)
{
@@ -1200,6 +1206,7 @@ namespace YLErp.Modules.EodModule
}
r.Rho *= 100;
greeksHandleService.Handle(r);
r.CountRatio = um.CountRatio;
r.TradeOriginalAmount = r.OriginalNotional / um.CountRatio;
r.etcTradePrice = ConsTrade.TradeTypesForHedge.Contains(r.TradeType)
@@ -1706,10 +1713,10 @@ namespace YLErp.Modules.EodModule
var results = GetEodPositionRisksDataHandle(req);
var list2 = results.Select(n => new ExpandoDictionary<string, string>(n)).ToArray();
var templateFile = OtcAppContext.MapPath("~/App_Docs/导出模板/日终持仓风险.xlsx");
var templateFile = OtcAppContext.MapPath("~/App_Docs/导出模板/日终持仓风险_国联.xlsx");
if (PS.Config.ErpElement.NeedShowSpv)
{
templateFile = OtcAppContext.MapPath("~/App_Docs/导出模板/日终持仓风险-Spv.xlsx");
templateFile = OtcAppContext.MapPath("~/App_Docs/导出模板/日终持仓风险-Spv_国联.xlsx");
}
return ExcelGenerator.UseTemplateGenerator(templateFile).AddVariable(new { list = list2 }).GenerateBytes();
@@ -2016,6 +2023,17 @@ namespace YLErp.Modules.EodModule
dic.Add("Spv3", item.PnLDelta.OtcFormat(OtcFormatFlag.greek));
}
if (PS.Config.Company == Configuration.CompanyEnum.)
{
dic.Add("Delta_r", OtcFormatExtensions.OtcFormat(item.Delta_r, OtcFormatFlag.greek));
dic.Add("Delta_r_1bp", OtcFormatExtensions.OtcFormat(item.Delta_r_1bp, OtcFormatFlag.greek));
dic.Add("Dv01", OtcFormatExtensions.OtcFormat(item.Dv01, OtcFormatFlag.greek));
dic.Add("Gamma_r", OtcFormatExtensions.OtcFormat(item.Gamma_r, OtcFormatFlag.greek));
dic.Add("Gamma_r_1bp", OtcFormatExtensions.OtcFormat(item.Gamma_r_1bp, OtcFormatFlag.greek));
dic.Add("Vega_r", OtcFormatExtensions.OtcFormat(item.Vega_r, OtcFormatFlag.greek));
dic.Add("Vega_r_1bp", OtcFormatExtensions.OtcFormat(item.Vega_r_1bp, OtcFormatFlag.greek));
}
dic.Add("备注", item.Comments);
@@ -2115,6 +2133,17 @@ namespace YLErp.Modules.EodModule
dic.Add("Spv2", "");
dic.Add("Spv3", "");
}
if (PS.Config.Company == Configuration.CompanyEnum.)
{
dic.Add("Delta_r", "");
dic.Add("Delta_r_1bp", "");
dic.Add("Dv01", "");
dic.Add("Gamma_r", "");
dic.Add("Gamma_r_1bp", "");
dic.Add("Vega_r", "");
dic.Add("Vega_r_1bp", "");
}
results.Add(dic);
}
return results;
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YLErp.Model;
namespace YLErp.Modules.EodModule
{
/// <summary>
/// 国联民生 希腊字母计算
/// </summary>
public class GLMSGreeksHandleService
{
public void InitData(DateTime valueDate,List<string> underlyingCodes)
{
}
public void Handle(EodPositionRisksDTO dto)
{
//对Delta_r Delta_r_1bp Dv01 Gamma_r Gamma_r_1bp Vega_r Vega_r_1bp
}
}
}
@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Linq.Dynamic.Core;
using YLErp.Modules.DataProviderModule;
@@ -48,7 +48,7 @@ namespace YLErp.Modules.EodModule.SettlementModule
if (request.IsSettleOtcTrades)
{
var tradePredicate = _context.PredicateBuilder.GetOtcTradePredicate()
.And(t => t.TradeType != "自定义交易" && t.TradeType != "收益互换" && t.UnderlyingCode != null);
.And(t => t.TradeType != "自定义交易" && t.UnderlyingCode != null);
//新增客户筛选 tw
if (clienIds != null)
@@ -58,6 +58,11 @@ namespace YLErp.Modules.EodModule.SettlementModule
allQuery = DbContext.trade.Where(tradePredicate).Select(n => n.UnderlyingCode).Distinct();
if (PS.Config.ErpElement.ForwardTradePriceModel == Configuration.Enums.ForwardTradePriceModel.STANDARD)
{
allQuery = allQuery.Union(DbContext.trade.Where(tradePredicate).Where(x => x.BasisUnderlyingCode != null && x.BasisUnderlyingCode != "").Select(n => n.BasisUnderlyingCode).Distinct());
}
var clientProductPredicate = _context.PredicateBuilder.GetClientProductPredicate();
//新增客户筛选 tw
@@ -74,26 +79,28 @@ namespace YLErp.Modules.EodModule.SettlementModule
}
//检查场内标的
//if (request.IsSettleExchangeTrades)
//{
// var exTradePredicate = _context.PredicateBuilder.GetExchangeTradePredicate();
if (request.IsSettleExchangeTrades)
{
var exTradePredicate = _context.PredicateBuilder.GetExchangeTradePredicate();
// var exTradeQuery = DbContext.ExchangeTrade.Where(exTradePredicate).Select(n => n.UnderlyingCode);
var exTradeQuery = DbContext.ExchangeTrade.Where(exTradePredicate).Select(n => n.UnderlyingCode);
// allQuery = allQuery == null ? exTradeQuery.Distinct() : allQuery.Union(exTradeQuery.Distinct());
allQuery = allQuery == null ? exTradeQuery.Distinct() : allQuery.Union(exTradeQuery.Distinct());
// var preSettleDate = _context.PreSettleDate;
// //最后一个交易日持仓信息
// var futureTypes = ConsGlobal.InstrumentType.GetFutureTypes();
// var positionQuery = from t in DbContext.eod_trade_position
// join um in DbContext.underlying_manager on t.UnderlyingCode equals um.UnderlyingCode
// where t.ValueDate == preSettleDate && t.Amount != 0
// && ConsTrade.TradeTypesForHedge.Contains(t.TradeType)
// && (!futureTypes.Contains(um.UnderlyingInstrumentType) || um.MaturityDate >= settleDate)
// select t.UnderlyingCode;
var preSettleDate = _context.PreSettleDate;
//最后一个交易日持仓信息
var futureTypes = ConsGlobal.InstrumentType.GetFutureTypes();
var positionQuery = from t in DbContext.eod_trade_position
join um in DbContext.underlying_manager on t.UnderlyingCode equals um.UnderlyingCode
where t.ValueDate == preSettleDate && t.Amount != 0
&& ConsTrade.TradeTypesForHedge.Contains(t.TradeType)
&& (!futureTypes.Contains(um.UnderlyingInstrumentType) || um.MaturityDate >= settleDate)
select t.UnderlyingCode;
// allQuery = allQuery.Union(positionQuery.Distinct());
//}
allQuery = allQuery.Union(positionQuery.Distinct());
}
if (allQuery == null)
{
@@ -103,12 +110,10 @@ namespace YLErp.Modules.EodModule.SettlementModule
//检查是否所有标的都在结算日有结算价格
var umCodeArr = allQuery.ToArray();
var swapUmCodeArr = GetSwapUnderlyingCodes();
var codeArr = umCodeArr.Union(swapUmCodeArr);
var eodPriceProvider = _context.GetEodPriceProvider().Initialize(codeArr);
// var preEodPriceProvidaer = _context.GetPreEodPriceProvider().Initialize(swapUmCodeArr);
var umCodes = codeArr.Where(n => !string.IsNullOrEmpty(n) && !eodPriceProvider.HasValue(n)).ToHashSet(StringComparer.OrdinalIgnoreCase);
//var swapUmCodes= swapUmCodeArr.Where(n => !string.IsNullOrEmpty(n) && !preEodPriceProvidaer.HasValue(n)).ToHashSet(StringComparer.OrdinalIgnoreCase);
var eodPriceProvider = _context.GetEodPriceProvider().Initialize(umCodeArr);
var umCodes = umCodeArr.Where(n => !string.IsNullOrEmpty(n) && !eodPriceProvider.HasValue(n)).ToHashSet(StringComparer.OrdinalIgnoreCase);
//排除掉节假日不需要结算的交易
foreach (var t in _context.HolidayTrades)
{
@@ -1,4 +1,4 @@
using BaseOUDAL;
using BaseOUDAL;
using YLErp.BLL;
using YLErp.Configuration;
using YLErp.Configuration.Enums;
@@ -993,10 +993,10 @@ namespace YLErp.Modules.TradeModule.OrderModule
throw new ServiceException($"客户'{client.Name}'为非高风险客户,没有卖出权限");
}
if (_underlying != null && _underlying.CalcTypeIsStock() && ((client.TradingInstType & TradingInstTypeEnum.Equity) != TradingInstTypeEnum.Equity))
{
throw new ServiceException($"客户'{client.Name}'的'交易资产'属性不包括'权益',不能进行权益类的期权交易");
}
//if (_underlying != null && _underlying.CalcTypeIsStock() && ((client.TradingInstType & TradingInstTypeEnum.Equity) != TradingInstTypeEnum.Equity))
//{
// throw new ServiceException($"客户'{client.Name}'的'交易资产'属性不包括'权益',不能进行权益类的期权交易");
//}
if (PS.Config.ErpElement.SecuritiesEnvironment)
{
+4 -4
View File
@@ -40,10 +40,10 @@
<PackageReference Include="Snowflake.Core" Version="2.0.0" />
<PackageReference Include="SocialExplorer.FastDBF" Version="1.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.0" />
<PackageReference Include="Qdp.Pricing.Library.Options" Version="1.0.5" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="YLErp.Office" Version="1.1.2" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.0.5" />
<PackageReference Include="Qdp.Pricing.Library.Options" Version="5.18.4-glms" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageReference Include="YLErp.Office" Version="1.4.5" />
</ItemGroup>
<ItemGroup>
+4
View File
@@ -7055,6 +7055,10 @@ namespace YLErp.Web.Controllers
public JsonResult BatchDownloadEodPositionRisks(EodPositionRisksReq req)
{
req.BookIds = AssetUnitModel.IntersectAssetUnits(req.AssetIdGroupList, req.BookIds).ToList();
req.UserAssets = CurUser.GetAssetUnitIds();
req.UserClients = CurUser.GetClientIdsByCurUser(CurUser._查看所有交易);
if (CurUser._查看所有交易) { req.UserClients.Clear(); }
if (!new EodPositionRisksQueryService(CurUser).BatchDownloadEodPositionRisks(req))
{
return JsonError("线程正在被占用");
+1
View File
@@ -34,6 +34,7 @@
})
function CaptchaClick(){
var img = document.getElementById('captcha');
if (!img) return; // NeedCaptcha=false 时验证码 img 不渲染,避免 null.setAttribute 报错
var newSrc = '@captchaUrl' + '&t=' + new Date().getTime();
img.setAttribute('src', newSrc);
}
+20 -1
View File
@@ -44,6 +44,15 @@
.no-skin {
position: static !important;
}
.searchdiv .contract-code-search-group {
width: 272px;
}
.searchdiv .contract-code-search-group .search-label {
width: 110px;
white-space: nowrap;
}
</style>
}
@section JS{
@@ -57,7 +66,17 @@
<div class="searchdiv">
@Html.ShortInput("TradeNumber", "交易编号")
@Html.ShortInput("ContractCode", pageObj.isGuoLian ? "交易确认书编号" : "合约编号")
@if (pageObj.isGuoLian)
{
<div class="search-group contract-code-search-group">
<span class="search-label" for="ContractCode">交易确认书编号</span>
<input class="search-input" id="ContractCode" name="ContractCode" type="text" value="@(ViewData["ContractCode"] ?? "")" autocomplete="off" />
</div>
}
else
{
@Html.ShortInput("ContractCode", "合约编号")
}
@Html.MyAceDropdownInput("ClientId", "客户名称", ClientDataModel.GetAllClient())
@Html.MyAceDropdownInput("AssetId", "簿记账户", AssetunitController.GetClientassetunit())
@Html.MyAceDropdownInput("GroupId", "簿记账户组", AssetUnitModel.GetAllAssetUnitGroupItem())
+3 -2
View File
@@ -1,4 +1,4 @@
@{
@{
ViewBag.Title = "日终持仓风险";
Layout = "~/Views/Shared/_MainLayout.cshtml";
var settleConfig = PS.Config.GetSettlementConfig();
@@ -19,7 +19,8 @@
IsUseDisplayNotional = PS.Config.IsUseDisplayNotional,
showGuohai = PS.Config.Is国海,
isGuoyuan = PS.Config.Company==CompanyEnum.国元固收,
isGuoJun = PS.Config.IsGuoJun
isGuoJun = PS.Config.IsGuoJun,
isGLMS = PS.Config.Company == CompanyEnum.国联
};
}
<style>
+7 -9
View File
@@ -30,7 +30,6 @@
<PackageReference Include="RazorEngineCore" Version="2022.8.1" />
<PackageReference Include="RazorLight-rpm" Version="3.0.0" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" />
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="6.0.1" />
<PackageReference Include="DocumentFormat.OpenXml" Version="2.7.2" />
<PackageReference Include="System.IO.Packaging" Version="4.5.0" />
</ItemGroup>
@@ -105,6 +104,9 @@
<None Update="App_Docs\导出模板\客户盈亏状况模板.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="App_Docs\导出模板\日终持仓风险_国联.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="App_Docs\导出模板\日终持仓风险.xlsx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@@ -242,10 +244,8 @@
<_RebuildScript>$(MSBuildProjectDirectory)\rebuild-bundles.ps1</_RebuildScript>
<_RebuildScriptSh>$(MSBuildProjectDirectory)/rebuild-bundles.sh</_RebuildScriptSh>
</PropertyGroup>
<Exec Condition="'$(OS)' == 'Windows_NT'"
Command="powershell -ExecutionPolicy Bypass -File &quot;$(_RebuildScript)&quot;" />
<Exec Condition="'$(OS)' != 'Windows_NT'"
Command="bash &quot;$(_RebuildScriptSh)&quot;" />
<Exec Condition="'$(OS)' == 'Windows_NT'" Command="powershell -ExecutionPolicy Bypass -File &quot;$(_RebuildScript)&quot;" />
<Exec Condition="'$(OS)' != 'Windows_NT'" Command="bash &quot;$(_RebuildScriptSh)&quot;" />
</Target>
<!-- 手动校验 targetdotnet build -t:VerifyBundles
@@ -257,10 +257,8 @@
<_RebuildScript>$(MSBuildProjectDirectory)\rebuild-bundles.ps1</_RebuildScript>
<_RebuildScriptSh>$(MSBuildProjectDirectory)/rebuild-bundles.sh</_RebuildScriptSh>
</PropertyGroup>
<Exec Condition="'$(OS)' == 'Windows_NT'"
Command="powershell -ExecutionPolicy Bypass -File &quot;$(_RebuildScript)&quot; -Verify" />
<Exec Condition="'$(OS)' != 'Windows_NT'"
Command="bash &quot;$(_RebuildScriptSh)&quot; --verify" />
<Exec Condition="'$(OS)' == 'Windows_NT'" Command="powershell -ExecutionPolicy Bypass -File &quot;$(_RebuildScript)&quot; -Verify" />
<Exec Condition="'$(OS)' != 'Windows_NT'" Command="bash &quot;$(_RebuildScriptSh)&quot; --verify" />
</Target>
<Target Name="CopyFilesAfterPublish" AfterTargets="Publish">
@@ -0,0 +1,100 @@
/**
* swapPriceInput.component.test.js — vue-swap-price-input 事件派发回归守卫
* ============================================================================
* 目的:锁定债券三字段输入组件「粘贴 / 重输原值 不派发 input/enter」的 bug。
* 症状:粘贴或重输原值后,点空白(标REV/清其他标识)与回车(触发计算)均无反应。
* 根因:组件把 input/enter 全部锁在原生 change 事件里,而 change 仅在「值相对聚焦时
* 变动」才触发;onPaste 甚至完全不派发任何事件。
*
* 做法:直接实例化组件 options 对象(createVueInputComponent),用 mock this + $emit 间谍
* 驱动 onPaste/onKeydown/onChange,断言事件派发。不依赖 Vue 运行时挂载、不改动源文件。
*
* 运行:cd YLErpWeb/fe-tests && npx jest swapPriceInput.component
* 设 SWAP_PRICE_HELPER 指向其它版本文件可做对照(如改之前的旧代码应使用例变红)。
*/
const fs = require('fs');
const path = require('path');
function loadComponent() {
const rel = process.env.SWAP_PRICE_HELPER
|| '../wwwroot/Scripts/app/swaptrade/swapPricePrecisionHelper.js';
const file = path.resolve(__dirname, rel);
const code = fs.readFileSync(file, 'utf8');
// swapPricePrecisionHelper.js: var swapPricePrecision = (function(global){...})(window);
// 在受控作用域求值并取出 createVueInputComponentIIFE 不挂 window,故用 new Function 取返回值)。
const factory = new Function('window', 'global', 'console', code + '\n;return swapPricePrecision;');
const sandbox = {};
const swapPricePrecision = factory(sandbox, sandbox, console);
return swapPricePrecision.createVueInputComponent();
}
function makeCtx(component, opts) {
opts = opts || {};
const emitted = [];
const ctx = {
text: opts.text != null ? String(opts.text) : '',
enterPressed: false,
formatSnapshot: '{}',
format: opts.format || { integerDigits: 2, precision: 4, percent: true },
$emit: function (name, payload) { emitted.push({ name: name, payload: payload }); },
};
Object.keys(component.methods).forEach(function (m) {
ctx[m] = component.methods[m].bind(ctx);
});
return { ctx: ctx, emitted: emitted };
}
describe('vue-swap-price-input 事件派发(粘贴 / 重输原值场景)', () => {
const component = loadComponent();
test('粘贴原值应派发 input(点空白标REV / 清其他标识)', () => {
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { integerDigits: 2, precision: 4, percent: true } });
const evt = { preventDefault() {}, clipboardData: { getData: () => '3.5' }, target: { value: '' } };
ctx.onPaste(evt);
const inputs = emitted.filter(e => e.name === 'input');
expect(inputs.length).toBe(1);
expect(inputs[0].payload).toBe('0.035'); // percent:true → ÷100
});
test('回车(原值未变)应派发 enter(触发自动计算)', () => {
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { integerDigits: 2, precision: 4, percent: true } });
const evt = { which: 13, keyCode: 13, key: 'Enter', target: { blur() {}, value: '3.5' } };
ctx.onKeydown(evt);
const enters = emitted.filter(e => e.name === 'enter');
expect(enters.length).toBe(1);
expect(enters[0].payload).toBe('0.035');
});
test('回车(值已改)应派发 input+enter 各一次,且不重复派发 enter', () => {
const { ctx, emitted } = makeCtx(component, { text: '3.6', format: { percent: true } });
const evt = { which: 13, keyCode: 13, key: 'Enter', target: { blur() {}, value: '3.6' } };
ctx.onKeydown(evt);
expect(emitted.filter(e => e.name === 'input').length).toBe(1);
expect(emitted.filter(e => e.name === 'enter').length).toBe(1);
});
test('非 Enter 按键不应派发 enter / input', () => {
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { percent: true } });
const evt = { which: 65, keyCode: 65, key: 'a', target: { blur() {} } };
ctx.onKeydown(evt);
expect(emitted.filter(e => e.name === 'enter').length).toBe(0);
expect(emitted.filter(e => e.name === 'input').length).toBe(0);
});
test('失焦(onChange)即使值未变也应派发 input(点空白标REV),且不再派发 enter', () => {
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { percent: true } });
const evt = { target: { value: '3.5' } };
ctx.onChange(evt);
expect(emitted.filter(e => e.name === 'input').length).toBe(1);
expect(emitted.filter(e => e.name === 'enter').length).toBe(0);
});
test('粘贴但格式为 percent:false 也应派发 input(原值透传)', () => {
const { ctx, emitted } = makeCtx(component, { text: '3.5', format: { integerDigits: 2, precision: 4, percent: false } });
const evt = { preventDefault() {}, clipboardData: { getData: () => '3.5' }, target: { value: '' } };
ctx.onPaste(evt);
expect(emitted.filter(e => e.name === 'input').length).toBe(1);
expect(emitted.filter(e => e.name === 'input')[0].payload).toBe('3.5');
});
});
@@ -1,4 +1,4 @@
function getColModelGrid() {
function getColModelGrid() {
var i = 0;
var colModelGrid = [
{ name: 'id', hidden: true, optionHide: true },
@@ -494,6 +494,75 @@
colModelGrid = colModelGrid.concat(spvCols);
}
if (page.isGLMS) {
colModelGrid.push({
name: 'Delta_r',
label: 'Delta_r',
index: 'Delta_r',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
}, {
name: 'Delta_r_1bp',
label: 'Delta_r(1bp)',
index: 'Delta_r_1bp',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
}, {
name: 'Dv01',
label: 'Dv01',
index: 'Dv01',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
}, {
name: 'Gamma_r',
label: 'Gamma_r',
index: 'Gamma_r',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
}, {
name: 'Gamma_r_1bp',
label: 'Gamma_r(1bp)',
index: 'Gamma_r_1bp',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
}, {
name: 'Vega_r',
label: 'Vega_r',
index: 'Vega_r',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
}, {
name: 'Vega_r_1bp',
label: 'Vega_r(1bp)',
index: 'Vega_r_1bp',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
},);
}
return tradeHelper.getAmountToNotional(colModelGrid);
}
@@ -594,6 +663,13 @@ function TradeAmountFormat(cellValue, options, rowObject) {
return otcformat.trading.notional(cellValue);
}
function greeksFormat(cellValue, options, rowObject) {
if (cellValue) {
return otcformat.trading.greek(cellValue);
}
return "";
}
function SpotPriceFormat(cellValue, options, rowObject) {
if (rowObject.TradeType !== "场内期权" && rowObject.TradeType !== "商品期货" && rowObject.TradeType !== "股票" && rowObject.TradeType !== "商品现货") {
return otcformat.trading.umprice(cellValue)
@@ -208,22 +208,32 @@ var swapPricePrecision = (function (global) {
event.preventDefault();
this.updateValue(clipboard.getData('text'), true, false);
event.target.value = this.text;
// 粘贴即显式赋值意图:直接派发 input,使「单击空白处标REV/其余清空」「回车前已标记」等行为生效。
// 原实现只把粘贴内容写回DOM、等失焦时的原生change才派发,而原值粘贴时change不触发→静默无反应。
this.$emit('input', this.toModel(this.text));
},
onKeydown: function (event) {
this.enterPressed = false;
this.$emit('keydown', event);
const keyCode = event.which || event.keyCode;
if (event.key !== 'Enter' && keyCode !== 13 && keyCode !== 108) return;
this.enterPressed = true;
event.target.blur();
// 回车=用户显式提交意图:无论值是否相对聚焦时变化,都直接派发 input(标REV) + enter(触发计算)。
// 原实现把 enter 锁在原生 change 里,而 change 仅在值变化时触发,
// 导致「重输/重贴原值 + 回车」静默不计算(QA 高优 Bug)。
this.updateValue(this.text, true, false);
const value = this.toModel(this.text);
event.target.blur(); // 同步触发原生 change(值变化时再派发一次 input,幂等无害)
this.$emit('input', value);
this.$emit('enter', value);
this.enterPressed = false;
},
onChange: function (event) {
if (this.text.endsWith('.')) this.text = this.text.substring(0, this.text.length - 1);
this.updateValue(this.text, true, false);
const value = this.toModel(this.text);
// 失焦(非回车)→ 仅派发 input 标 REVenter(计算) 改由 onKeydown 直接派发,
// 避免 change 不触发时丢失,也避免与 onKeydown 重复派发 enter 造成重复计算。
this.$emit('input', value);
if (this.enterPressed) this.$emit('enter', value);
this.enterPressed = false;
event.target.value = this.text;
}
},
+24 -9
View File
@@ -14667,6 +14667,7 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
};
}(window.FastVue));
//日期选择组件FastVue.DatePicker
(function (global) {
global.vueDatePicker = function () {
@@ -15009,21 +15010,35 @@ $.fn.selectpicker.Constructor.DEFAULTS = Object.assign($.fn.selectpicker.Constru
//依赖类库:lodash.js + jquery + jquery.validate + bootstrap.emodal.js
// 统一调试日志工具:仅在 otcdebug 开启时输出,默认静默(生产零噪音)。
// 开启方式URL 加 ?otcdebug=1,或控制台 localStorage.setItem('otcdebug','1')。
// 业务脚本逐步改用 otcDebug.banner / otcDebug.log 替代裸 console.log
// 既能在排查时一键全开,又能避免版本标记散落硬编码(见 swapTradeEdit.js / fastVue.base.js 用法)。
var __otcDiag = (window.ylotc && window.ylotc.__diag) || { debug: false };
// 开启:URL 加 ?otcdebug=1(会自动写入 localStorage,同源 iframe/弹窗自动继承);
// 或控制台 localStorage.setItem('otcdebug','1')。关闭:localStorage.removeItem('otcdebug')。
// 业务脚本用 otcDebug.banner / otcDebug.log 替代裸 console.log(见 swapTradeEdit.js / fastVue.base.js)。
//
// 命名约定(避免 __私有对象 + otcDebug.API 大小写混搭):
// __diag —— 版本元数据对象(仅 _MainLayout 灌入 ylotc.__diag 时存在,仅供 banner 展示,不参与开关判定)
// __debug —— 调试开关布尔值(唯一真相来源:localStorage 优先 → URL 参数 → 兜底 __diag.debug
var __diag = (window.ylotc && window.ylotc.__diag) || {};
var __debug = (function () {
try {
// 1) URL 参数优先(排查最直观);命中即写入 localStorage,供同源 iframe/弹窗继承
if (/[?&]otcdebug=1/.test(location.search)) { localStorage.setItem('otcdebug', '1'); return true; }
// 2) 已持久化的开关(父页面开过一次,_InfoLayout 弹窗自动生效)
if (localStorage.getItem('otcdebug') === '1') return true;
} catch (e) { /* localStorage 不可用时忽略,继续走兜底 */ }
// 3) 兜底:layout 灌入的 ylotc.__diag.debug(仅 _MainLayout 有)
return !!__diag.debug;
})();
window.otcDebug = {
// 模块加载横幅:F12 一眼看到模块名+bundle版本+git sha,用于排查"是不是加载了旧代码/旧缓存"
// 模块加载横幅:F12 一眼看到模块名+版本,用于排查"是不是加载了旧代码/旧缓存"
banner: function (name, ver) {
if (!__otcDiag.debug || !window.console) return;
var git = (__otcDiag.git || '').slice(0, 7);
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __otcDiag.jsVersion + ', git=' + git + ', built=' + __otcDiag.built + ')',
if (!__debug || !window.console) return;
var git = (__diag.git || '').slice(0, 7);
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __diag.jsVersion + ', git=' + git + ', built=' + __diag.built + ')',
'color:#06c;font-weight:bold');
},
// 普通调试日志:透传参数,仅 debug 开启时输出
log: function () {
if (!__otcDiag.debug || !window.console) return;
if (!__debug || !window.console) return;
console.log.apply(console, arguments);
}
};
+23 -9
View File
@@ -802,21 +802,35 @@ An}();typeof define=="function"&&typeof define.amd=="object"&&define.amd?($n._=r
//依赖类库:lodash.js + jquery + jquery.validate + bootstrap.emodal.js
// 统一调试日志工具:仅在 otcdebug 开启时输出,默认静默(生产零噪音)。
// 开启方式URL 加 ?otcdebug=1,或控制台 localStorage.setItem('otcdebug','1')。
// 业务脚本逐步改用 otcDebug.banner / otcDebug.log 替代裸 console.log
// 既能在排查时一键全开,又能避免版本标记散落硬编码(见 swapTradeEdit.js / fastVue.base.js 用法)。
var __otcDiag = (window.ylotc && window.ylotc.__diag) || { debug: false };
// 开启:URL 加 ?otcdebug=1(会自动写入 localStorage,同源 iframe/弹窗自动继承);
// 或控制台 localStorage.setItem('otcdebug','1')。关闭:localStorage.removeItem('otcdebug')。
// 业务脚本用 otcDebug.banner / otcDebug.log 替代裸 console.log(见 swapTradeEdit.js / fastVue.base.js)。
//
// 命名约定(避免 __私有对象 + otcDebug.API 大小写混搭):
// __diag —— 版本元数据对象(仅 _MainLayout 灌入 ylotc.__diag 时存在,仅供 banner 展示,不参与开关判定)
// __debug —— 调试开关布尔值(唯一真相来源:localStorage 优先 → URL 参数 → 兜底 __diag.debug
var __diag = (window.ylotc && window.ylotc.__diag) || {};
var __debug = (function () {
try {
// 1) URL 参数优先(排查最直观);命中即写入 localStorage,供同源 iframe/弹窗继承
if (/[?&]otcdebug=1/.test(location.search)) { localStorage.setItem('otcdebug', '1'); return true; }
// 2) 已持久化的开关(父页面开过一次,_InfoLayout 弹窗自动生效)
if (localStorage.getItem('otcdebug') === '1') return true;
} catch (e) { /* localStorage 不可用时忽略,继续走兜底 */ }
// 3) 兜底:layout 灌入的 ylotc.__diag.debug(仅 _MainLayout 有)
return !!__diag.debug;
})();
window.otcDebug = {
// 模块加载横幅:F12 一眼看到模块名+bundle版本+git sha,用于排查"是不是加载了旧代码/旧缓存"
// 模块加载横幅:F12 一眼看到模块名+版本,用于排查"是不是加载了旧代码/旧缓存"
banner: function (name, ver) {
if (!__otcDiag.debug || !window.console) return;
var git = (__otcDiag.git || '').slice(0, 7);
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __otcDiag.jsVersion + ', git=' + git + ', built=' + __otcDiag.built + ')',
if (!__debug || !window.console) return;
var git = (__diag.git || '').slice(0, 7);
console.log('%c[' + name + '] v' + ver + ' (bundle=' + __diag.jsVersion + ', git=' + git + ', built=' + __diag.built + ')',
'color:#06c;font-weight:bold');
},
// 普通调试日志:透传参数,仅 debug 开启时输出
log: function () {
if (!__otcDiag.debug || !window.console) return;
if (!__debug || !window.console) return;
console.log.apply(console, arguments);
}
};