Merge branch 'refs/heads/glms/feature/1.4.2' into glms/feature/0812_zmr_divPower

This commit is contained in:
张名锐
2026-08-21 17:09:15 +08:00
67 changed files with 1748 additions and 265 deletions
@@ -596,6 +596,11 @@ namespace YLErp.Configuration
/// </summary>
int ExternalAPIForCustomCalcTimeOut { get; set; }
/// <summary>
/// 收益互换 OA 移动审批状态查询间隔(秒)。
/// </summary>
int TradeApprovalOaSyncIntervalSeconds { get; set; }
bool IsForwardTradeToBuy { get; set; }
/// <summary>
/// 是否需要计算实时预警
@@ -702,6 +702,11 @@ namespace YLErp
/// </summary>
public int ExternalAPIForCustomCalcTimeOut { get; set; }
/// <summary>
/// 收益互换 OA 移动审批状态查询间隔(秒)。
/// </summary>
public int TradeApprovalOaSyncIntervalSeconds { get; set; } = 30;
public bool IsForwardTradeToBuy { get; set; }
/// <summary>
/// 是否需要实时预警
@@ -70,5 +70,9 @@ namespace YLErp.DBModels
/// </summary>
[DisplayName("节点触发条件")]
public string triggerCondition { get; set; }
/// <summary>是否在进入该节点时关联国联民生 OA 移动审批。</summary>
public bool isOaApproval { get; set; }
}
}
@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace YLErp.DBModels
{
/// <summary>
/// 衍生品交易审批节点对应的国联民生 OA 流程记录。
/// </summary>
[Table("trade_approval_oa_result")]
public class trade_approval_oa_result : DBModelBaseV6
{
public int trade_id { get; set; }
public int approval_process_id { get; set; }
public string process_type { get; set; }
public int? applicant_id { get; set; }
public string applicant_login_name { get; set; }
public string oa_fileid { get; set; }
public string status { get; set; }
public string oa_msg { get; set; }
public string request_payload { get; set; }
public string last_response { get; set; }
public DateTime? last_query_time { get; set; }
public bool is_valid { get; set; }
}
}
@@ -137,6 +137,20 @@ namespace YLErp.DBModels
[DisplayName("标的名称")]
public string UnderlyingName { set; get; }
/// <summary>
/// 基金及基金专户的基金管理人名称
/// </summary>
[Column("investadvisorname")]
[DisplayName("基金管理人")]
public string InvestAdvisorName { get; set; }
/// <summary>
/// 基金及基金专户所属的 ETF 子类,保存“ETF 子类”字典项名称
/// </summary>
[Column("etf_sub_type")]
[DisplayName("ETF 子类")]
public string EtfSubType { get; set; }
/// <summary>
/// 标的英文名
@@ -42,4 +42,17 @@
<dictionary name="可取系数" catalog="客户"></dictionary>
<dictionary name="最小转账金额" catalog="客户"></dictionary>
<dictionary name="累计转远期到期日类型" catalog="客户"></dictionary>
<dictionary name="ETF 子类" catalog="标的">
<item name="国债 ETF" short_name="国债 ETF"></item>
<item name="政金债 ETF" short_name="政金债 ETF"></item>
<item name="地方债 ETF" short_name="地方债 ETF"></item>
<item name="国债政金债 ETF" short_name="国债政金债 ETF"></item>
<item name="短融 ETF" short_name="短融 ETF"></item>
<item name="公司债 ETF" short_name="公司债 ETF"></item>
<item name="城投债 ETF" short_name="城投债 ETF"></item>
<item name="做市信用债 ETF" short_name="做市信用债 ETF"></item>
<item name="科创债 ETF" short_name="科创债 ETF"></item>
<item name="可转债 ETF" short_name="可转债 ETF"></item>
</dictionary>
</dictionaries>
@@ -508,7 +508,8 @@ namespace YLErp.Plugins.GuoLian.DocumentGenerator
dic["参考标的名义份额"] = swapPosition != null
? ((double)swapPosition.PosiQuantity).ToString("0.##")
: "0";
dic["参考标的基金管理人"] = "";
dic["参考标的基金管理人"] = underlying?.InvestAdvisorName ?? "";
var contractTypeId = (Context.GetContractTypes().FirstOrDefault(O => O.ContactType == "交易确认书接收")?.id) ?? 0;
// 乙方联系人信息
var clientDuties = Context.GetClientDuties().Where(O => O.ContactTypeIdsInt.Contains(contractTypeId)).ToList();
@@ -0,0 +1,51 @@
using YLErp.Modules.CalculationModule;
using YLErp.Modules.EodModule;
namespace YLErp.BasicTests
{
[TestClass]
public class GLMSGreeksHandleServiceTest
{
[TestMethod]
public void Handle_NonRateAsset_CalculatesVega1bpBeforeRateFilter()
{
var service = new GLMSGreeksHandleService();
var dto = new EodPositionRisksDTO
{
Vega = 2500d
};
var underlying = new underlying_manager
{
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock
};
service.Handle(dto, underlying);
Assert.IsNotNull(dto.Vega_1bp);
Assert.AreEqual(0.25d, dto.Vega_1bp.Value, 1e-12);
Assert.IsNull(dto.Vega_r);
Assert.IsNull(dto.Vega_r_1bp);
}
[TestMethod]
public void Handle_TradeValueResult_NonRateAsset_CalculatesVega1bpBeforeRateFilter()
{
var service = new GLMSGreeksHandleService();
var result = new TradeValueResult
{
Vega = 2500d
};
var underlying = new underlying_manager
{
UnderlyingInstrumentType = ConsGlobal.InstrumentType.Stock
};
service.Handle(new trade(), result, underlying);
Assert.IsNotNull(result.Vega_1bp);
Assert.AreEqual(0.25d, result.Vega_1bp.Value, 1e-12);
Assert.IsNull(result.Vega_r);
Assert.IsNull(result.Vega_r_1bp);
}
}
}
@@ -0,0 +1,61 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using YLErp.Helpers;
using YLErp.Model;
namespace UnitTestProject.Modules
{
/// <summary>
/// EQD-6953 疑似到期债券值域闸门:IsMaturedDegenerate / IsResultAbsurd 分支覆盖。
/// 场景来源:UAT 060203.IB2006年国债,2026估值日已无剩余现金流)——
/// jquantlib 对空现金流求解得 ytm=0、净/全价均为面值100errCode=0"成功但退化"。
/// 与前端 swapCalc.js::getBondCalcErrorMessage 的同款闸门保持一致口径。
/// </summary>
[TestClass]
public class BondCalcHeplerTest
{
[TestMethod]
public void 退_三条件同时成立_命中()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100m, ytm = 0m };
Assert.IsTrue(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void _不命中_按UAT实测180205IB()
{
var r = new CalBondResult { cleanPrice = 97.43300000000002m, dirtyPrice = 100.00001369863016m, ytm = 6.738278242318886m };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void ytm为0但净价非面值_不命中_真实零息平价券场景()
{
var r = new CalBondResult { cleanPrice = 99.5m, dirtyPrice = 100m, ytm = 0m };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void ytm非0_不命中_正常息票平价券场景()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100.5m, ytm = 3.2m };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void ytm为null_不命中()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100m, ytm = null };
Assert.IsFalse(BondCalcHepler.IsMaturedDegenerate(r));
}
[TestMethod]
public void IsResultAbsurd_到期退化值_命中并带原因()
{
var r = new CalBondResult { cleanPrice = 100m, dirtyPrice = 100m, ytm = 0m };
var ok = typeof(BondCalcHepler)
.GetMethod("IsResultAbsurd", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
.Invoke(null, new object[] { r, null });
Assert.IsTrue((bool)ok);
}
}
}
@@ -0,0 +1,20 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
namespace YLErp.UnitTestProject.Modules.UnderlyingModule
{
[TestClass]
public class UnderlyingFundManagerMappingTest
{
[TestMethod]
public void InvestAdvisorName_MapsExistingFundManagerColumn()
{
var property = typeof(underlying_manager).GetProperty("InvestAdvisorName");
Assert.IsNotNull(property, "underlying_manager 应公开基金管理人属性 InvestAdvisorName");
Assert.AreEqual("investadvisorname", property.GetCustomAttribute<ColumnAttribute>()?.Name);
Assert.AreEqual("基金管理人", property.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName);
}
}
}
+2 -1
View File
@@ -406,6 +406,7 @@ namespace YLErp.BLL
public DbSet<client_margin_config> clientMarginConfig { get; set; }
public DbSet<client_margin_detail> clientMarginDetail { get; set; }
public DbSet<trade_contract_oa_result> tradeContractOaResult { get; set; }
public DbSet<trade_approval_oa_result> tradeApprovalOaResult { get; set; }
public DbSet<BondPayment> bondPayment { get; set; }
public DbSet<glms_risk_rule> glms_risk_rule { get; set; }
@@ -414,4 +415,4 @@ namespace YLErp.BLL
public DbSet<glms_risk_variable> glms_risk_variable { get; set; }
}
}
}
+21 -1
View File
@@ -162,7 +162,8 @@ namespace YLErp.Helpers
/// 命中则上层返回 null、不回写,避免把不可信结果覆盖用户手工输入。规则:
/// - 净价/全价:占面值百分比,正常约 20~300,必须为正且不破千(≤0 或 &gt;1000 视为离谱);
/// - 收益率(ytm,百分数口径)|ytm| &gt; 100 视为爆炸(允许负利率债,但量级须合理);
/// - 哨兵值:任一字段命中 -999999 或 -999999×100(如 yieldToCP 不可用哨兵透传)
/// - 哨兵值:任一字段命中 -999999 或 -999999×100(如 yieldToCP 不可用哨兵透传)
/// - 疑似到期/无剩余现金流:见 <see cref="IsMaturedDegenerate"/>。
/// 与前端 swapCalc.js::getBondCalcErrorMessage 的值域闸门保持一致口径。
/// </summary>
private static bool IsResultAbsurd(CalBondResult data, out string reason)
@@ -186,8 +187,27 @@ namespace YLErp.Helpers
reason = "债券计算收益率量级异常(" + data.ytm.Value + "),请检查估值日/价格输入或联系管理员核对债券计算服务";
return true;
}
if (IsMaturedDegenerate(data))
{
reason = "该债券疑似已到期或无剩余现金流(收益率=0、净价/全价均为面值100),无法反算,请核对债券到期日或手工填写";
return true;
}
return false;
}
/// <summary>
/// 疑似到期/无剩余现金流债券:估值日晚于到期日时底层 jquantlib 对空现金流求解得 ytm=0、
/// 净价/全价均为面值100——三者同时成立在现实在市券中几乎不可能(UAT 060203.IB 实证)。
/// 判定条件与前端 swapCalc.js::getBondCalcErrorMessage 内同款闸门保持一致,勿单边改动。
/// public 供单测直接覆盖(BondCalcHeplerTest)。
/// </summary>
public static bool IsMaturedDegenerate(CalBondResult data)
{
return data != null
&& data.ytm == 0m
&& data.cleanPrice == 100m
&& data.dirtyPrice == 100m;
}
}
}
+2
View File
@@ -230,6 +230,8 @@ namespace YLErp.Model
public double? Vega_r_1bp { get; set; }
public double? Vega_1bp { get; set; }
/// <summary>
+1
View File
@@ -64,6 +64,7 @@ namespace YLErp.Model
public int? ProcessRoleId { get; set; }
public string ProcessRoleName { get; set; }
public string OaRemark { get; set; }
/// <summary>
/// 验证当前审批角色是否有开仓(交易)复核权限
+43
View File
@@ -423,6 +423,8 @@ namespace YLErp.Modules.AppModule
var exists = adminDb.Dictionaries.Select(n => n.Name).ToArray()
.Select(n => n.Trim()).ToHashSet();
// 记录本次新建的字典,确保初始项只写入一次,不覆盖后续人工维护结果。
var addedDictionaryNames = new HashSet<string>();
var root = XElement.Parse(YLErp.Resources.Properties.Resources.db_dictionaries);
var itemsAll = root.Elements();
@@ -450,10 +452,51 @@ namespace YLErp.Modules.AppModule
Name = name,
Catalog = catalog
});
addedDictionaryNames.Add(name);
}
adminDb.SaveChanges();
// 重点功能:解析 XML 中的初始项,仅为本次首次创建的字典生成 DictionaryItem。
foreach (var dictionaryElement in itemsAll.Where(item =>
item.Elements().Any() && addedDictionaryNames.Contains(item.Attribute("name")?.Value.TrimToEmpty())))
{
var dictionaryName = dictionaryElement.Attribute("name")?.Value.TrimToNull();
var dictionary = adminDb.Dictionaries.FirstOrDefault(item => item.Name == dictionaryName);
if (dictionary == null)
{
continue;
}
var existingItemNames = adminDb.DictionaryItems
.Where(item => item.DictId == dictionary.Id)
.Select(item => item.Name)
.ToHashSet();
var nextDictionaryItemIndex = adminDb.DictionaryItems
.Where(item => item.DictId == dictionary.Id)
.Max(item => (int?)item.IndexNum) ?? -1;
// XML 中的排列顺序写入 IndexNum,页面下拉按该顺序展示。
foreach (var itemElement in dictionaryElement.Elements())
{
var itemName = itemElement.Attribute("name")?.Value.TrimToNull();
if (itemName == null || existingItemNames.Contains(itemName))
{
continue;
}
adminDb.DictionaryItems.Add(new BaseOUDAL.DictionaryItem
{
DictId = dictionary.Id,
Name = itemName,
ShortName = itemElement.Attribute("short_name")?.Value.TrimToNull() ?? itemName,
IndexNum = ++nextDictionaryItemIndex
});
existingItemNames.Add(itemName);
}
}
adminDb.SaveChanges();
var marginTemplateDictionary = adminDb.Dictionaries.FirstOrDefault(item => item.Name == YLErp.Modules.SwapModule.SwapMarginTemplateConfigService.DictionaryName);
if (marginTemplateDictionary == null)
{
@@ -256,6 +256,7 @@ namespace YLErp.Modules.CalcPriceShowConfigModule
new CalcQuotaDto{Name="Gamma_r(1bp)",Tip=""},
new CalcQuotaDto{Name="Vega_r",Tip=""},
new CalcQuotaDto{Name="Vega_r(1bp)",Tip=""},
new CalcQuotaDto{Name="Vega(1bp)",Tip=""},
};
}
@@ -515,6 +515,8 @@ namespace YLErp.Modules.CalculationModule
public double? Vega_r { get; set; }
public double? Vega_r_1bp { get; set; }
public double? Vega_1bp { get; set; }
}
public class TradeValueResultExtend
@@ -2032,6 +2032,7 @@ namespace YLErp.Modules.EodModule
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("Vega_1bp", OtcFormatExtensions.OtcFormat(item.Vega_1bp, OtcFormatFlag.greek));
}
@@ -2143,6 +2144,7 @@ namespace YLErp.Modules.EodModule
dic.Add("Gamma_r_1bp", "");
dic.Add("Vega_r", "");
dic.Add("Vega_r_1bp", "");
dic.Add("Vega_1bp", "");
}
results.Add(dic);
}
@@ -164,6 +164,7 @@ namespace YLErp.Modules.EodModule
dto.Dv01 = calcDto.Dv01;
dto.Gamma_r_1bp = calcDto.Gamma_r_1bp;
dto.Vega_r_1bp = calcDto.Vega_r_1bp;
dto.Vega_1bp = calcDto.Vega_1bp;
}
@@ -183,6 +184,7 @@ namespace YLErp.Modules.EodModule
calRes.Dv01 = calcDto.Dv01;
calRes.Gamma_r_1bp = calcDto.Gamma_r_1bp;
calRes.Vega_r_1bp = calcDto.Vega_r_1bp;
calRes.Vega_1bp = calcDto.Vega_1bp;
}
@@ -267,6 +269,9 @@ namespace YLErp.Modules.EodModule
private void handle(GreeksCalcDto dto, underlying_manager um)
{
// 标准 Vega 的 1bp 变体:全资产计算,不受利率类过滤限制
dto.Vega_1bp = dto.Vega * 0.0001;
if (!calcInstrumentTypes.Contains(um.UnderlyingInstrumentType))
{
return;
@@ -353,6 +358,8 @@ namespace YLErp.Modules.EodModule
public double? Vega_r_1bp { get; set; }
public double? Vega_1bp { get; set; }
}
public class ChinaBondIndexQuoteQueryDto
@@ -1250,6 +1250,10 @@ namespace YLErp.Modules.SwapModule
{
return;
}
if (!new TradeApprovalOAService(this).ArchiveActiveForTrade(tradeObj, out var errorMessage))
{
throw new ServiceException(errorMessage);
}
var eventTypes = new List<int>() { (int)SwapEventTypeEnum., (int)SwapEventTypeEnum., (int)SwapEventTypeEnum., (int)SwapEventTypeEnum. };
var lastEvent = DbContext.swap_event.Where(x => x.SwapTradeId == intid && eventTypes.Contains(x.EventType) && !x.Invalid).OrderByDescending(o => o.OptTime).FirstOrDefault();
if (tradeObj.TradeStatus == ConsTrade. || tradeObj.TradeStatus == ConsTrade.)
@@ -59,7 +59,8 @@ namespace YLErp.Modules.SystemModule
parentNode = item.parentNode,
approvalCondition = item.approvalCondition,
conditionConfig = item.conditionConfig,
triggerCondition = item.triggerCondition
triggerCondition = item.triggerCondition,
isOaApproval = item.isOaApproval
}).ToList();
DbContext.approvalprocess.AddRange(list);
@@ -790,6 +791,8 @@ namespace YLErp.Modules.SystemModule
/// <summary>节点触发条件(JSON),需求①:满足才进入该审批节点,不满足则跳过。</summary>
public string triggerCondition { get; set; }
public bool isOaApproval { get; set; }
}
/// <summary>
/// 审批流程修改节点
@@ -263,6 +263,10 @@ namespace YLErp.Modules.TradeModule.DealModule
td.OptName = UserName;
td.OptDate = OptDate;
int passStatus = new ApprovalProcessService(OptUser).CheckTradeApprovalStep(td, OptUser.UserId);
if (passStatus != 0 && passStatus != -3)
{
new TradeApprovalOAService(this).EnsureForCurrentNode(td);
}
if (passStatus == 0)
{
td.TradeStatus = ConsTrade.;
@@ -56,6 +56,23 @@ namespace YLErp.Modules.TradeModule.DealModule
throw new ServiceException("该交易状态已变更,不能被审批,请先刷新页面");
}
if (!req.isFromOa && (req.status == "pass" || req.status == "reject"))
{
var currentNode = TradeProcessByCategory(td)
.Where(x => x.order == td.ProcessOrderId && (x.node == 0 || x.node == td.ProcessOrderBranch))
.OrderByDescending(x => x.node == td.ProcessOrderBranch)
.FirstOrDefault();
if (currentNode?.isOaApproval == true)
{
var oa = DbContext.tradeApprovalOaResult.FirstOrDefault(x => x.trade_id == td.id
&& x.approval_process_id == currentNode.id
&& x.is_valid
&& (x.status == "提交成功" || x.status == "同步中" || x.status == "归档中"));
if (oa != null && !new TradeApprovalOAService(this).ArchiveForLocalAction(oa, out var errorMessage))
throw new ServiceException(errorMessage);
}
}
if (req.status == "pass")
{
return TradePass(req, td);
@@ -179,6 +196,13 @@ namespace YLErp.Modules.TradeModule.DealModule
return result;
}
// 关联 OA 的节点必须先成功创建 OA 流程,才允许本地审批推进到该节点。
if (nextOrder.isOaApproval && td.TradeType == "收益互换")
{
// OA 创建是节点通过后的外部动作;明确失败只记录在 OA 结果表,不能回滚本地节点通过。
new TradeApprovalOAService(this).CreateForNode(td, nextOrder);
}
//-----------------------------------------------
// 继续审批流转
//-----------------------------------------------
@@ -788,6 +812,7 @@ namespace YLErp.Modules.TradeModule.DealModule
public bool notNeedOperationHistory;
public bool isPartialExercise;
public bool isExpire = false;
public bool isFromOa;
}
/// <summary>
@@ -50,6 +50,11 @@
return;
}
foreach (var td in revokeTrades)
{
ArchiveActiveOaOrThrow(td);
}
var tradeids = revokeTrades.Select(n => n.id);
var revokeTrade_cashs = DbContext.trade_cash.Where(c => c.ValidState == "InValid" && !c.IsDeleted && tradeids.Contains(c.TradeId)).ToList();
revokeTrade_cashs.ForEach(x =>
@@ -94,6 +99,7 @@
var Withdraws = DbContext.trade.Where(t => tradeIds.Contains(t.id)).ToList();
foreach (var td in Withdraws)
{
ArchiveActiveOaOrThrow(td);
var changesTradeStatus = DbContext.TradeAuditLog.Where(x => (x.OptType == "确认交易" || x.OptType == "交易特批-确认交易") && x.TradeId == td.id).OrderByDescending(x => x.id)?.FirstOrDefault()?.Changes;
td.TradeStatus = changesTradeStatus == ConsTrade. ? ConsTrade. : ConsTrade.;
td.CheckStatus = null;
@@ -103,6 +109,12 @@
}
DbContext.SaveChanges();
}
private void ArchiveActiveOaOrThrow(trade td)
{
if (!new TradeApprovalOAService(this).ArchiveActiveForTrade(td, out var errorMessage))
throw new ServiceException(errorMessage);
}
/// <summary>
/// 获取包含黑箱结构的子交易集合
/// </summary>
@@ -0,0 +1,481 @@
using BaseOUDAL;
using Newtonsoft.Json.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Consts;
using YLErp.DBModels.Enums;
using YLErp.Modules.ApiModule;
using YLErp.Modules.TradeModule.DealModule;
namespace YLErp.Modules.TradeModule
{
/// <summary>
/// 收益互换审批节点的国联民生 OA 对接。
/// 本期只负责 HTTP 创建、查询和归档;审批结果由轮询任务回写本地流程。
/// </summary>
public class TradeApprovalOAService : YLBaseService
{
private const string Pending = "提交成功";
private const string Syncing = "同步中";
private const string Ending = "归档中";
private readonly IYcLogger _logger = LogFactory.GetLogger<TradeApprovalOAService>();
public TradeApprovalOAService(OptUserInfo userInfo) : base(userInfo) { }
public TradeApprovalOAService(YLBaseService baseService) : base(baseService) { }
public bool IsSupported(trade trade)
{
return trade != null && trade.TradeType == "收益互换";
}
public trade_approval_oa_result CreateForNode(trade trade, approvalprocess node)
{
if (!IsSupported(trade) || node == null || !node.isOaApproval)
return null;
var scenario = node.processType == "TradeProcess"
? "开仓"
: (trade.TradeStatus == ConsTrade. ? "平仓" : "收益结算");
_logger.Info($"开始创建 OA 移动审批,交易:{trade.id},节点:{node.id},场景:{scenario}");
var record = new trade_approval_oa_result
{
trade_id = trade.id,
approval_process_id = node.id,
process_type = node.processType,
applicant_id = UserId,
applicant_login_name = Config("Applicant"),
status = "提交中",
is_valid = true
};
record.SetOpt(UserInfo);
DbContext.tradeApprovalOaResult.Add(record);
DbContext.SaveChanges();
try
{
var creator = Config("Creator");
var applicant = Config("Applicant");
if (string.IsNullOrWhiteSpace(creator) || string.IsNullOrWhiteSpace(applicant))
throw new ServiceException("OA 配置缺少 Creator 或 Applicant");
var spr = GetSprLoginName(node);
var payload = BuildPayload(trade, creator, applicant, spr, scenario);
record.request_payload = JsonHelper.Serialize(payload);
var responseText = Post("SubUrl", payload);
record.last_response = responseText;
var response = JObject.Parse(responseText);
var responseData = response["data"] as JObject;
var responseCode = (int?)response["code"];
var responseStatus = (string)response["status"];
_logger.Info($"OA 创建流程原始响应,交易:{trade.id},节点:{node.id},响应:{responseText}");
if ((int?)response["code"] != 0 || !string.Equals((string)response["status"], "SUCCESS", StringComparison.OrdinalIgnoreCase))
{
record.status = "提交失败";
record.oa_msg = (string)responseData?["msg"] ?? (string)response["msg"] ?? "OA 创建流程失败";
_logger.Error($"创建 OA 移动审批返回失败,交易:{trade.id},节点:{node.id}code:{responseCode}status:{responseStatus}");
}
else
{
record.status = Pending;
record.oa_fileid = (string)responseData?["requestid"] ?? (string)responseData?["requestId"];
record.oa_msg = (string)responseData?["msg"] ?? (string)response["msg"];
if (string.IsNullOrWhiteSpace(record.oa_fileid))
{
record.status = "提交失败";
_logger.Error($"创建 OA 移动审批未返回 requestId,交易:{trade.id},节点:{node.id}");
}
else
{
_logger.Info($"创建 OA 移动审批成功,交易:{trade.id},节点:{node.id}requestId:{record.oa_fileid}");
}
}
}
catch (Exception ex)
{
record.status = "提交失败";
record.oa_msg = ex.Message;
_logger.Error($"创建 OA 移动审批失败,交易:{trade.id},节点:{node.id}", ex);
}
DbContext.SaveChanges();
return record;
}
/// <summary>
/// 交易首次进入审批时,确保当前 OA 节点已创建对应流程。
/// </summary>
public trade_approval_oa_result EnsureForCurrentNode(trade trade)
{
if (!IsSupported(trade) || trade.ProcessOrderId <= 0)
return null;
var node = GetCurrentNode(trade);
if (node?.isOaApproval != true)
return null;
var hasActiveRecord = DbContext.tradeApprovalOaResult.Any(x => x.trade_id == trade.id
&& x.approval_process_id == node.id
&& x.is_valid
&& (x.status == "提交中" || x.status == Pending || x.status == Syncing || x.status == Ending));
return hasActiveRecord ? null : CreateForNode(trade, node);
}
/// <summary>
/// 本地撤回前归档交易仍在审批中的 OA 流程。
/// </summary>
public bool ArchiveActiveForTrade(trade trade, out string errorMessage)
{
errorMessage = null;
if (!IsSupported(trade))
return true;
var records = DbContext.tradeApprovalOaResult
.Where(x => x.trade_id == trade.id
&& x.is_valid
&& !string.IsNullOrWhiteSpace(x.oa_fileid)
&& (x.status == Pending || x.status == Syncing || x.status == Ending))
.OrderBy(x => x.id)
.ToList();
if (records.Count == 0)
{
_logger.Info($"本地撤回前未找到待归档 OA 流程,交易:{trade.id}");
return true;
}
foreach (var record in records)
{
if (!ArchiveForLocalAction(record, out errorMessage))
return false;
}
return true;
}
/// <summary>
/// 本地审批或撤回前归档 OA。通过条件更新抢占记录,避免与轮询回写同时推进同一节点。
/// </summary>
public bool ArchiveForLocalAction(trade_approval_oa_result record, out string errorMessage)
{
errorMessage = null;
if (record == null)
return true;
_logger.Info($"本地操作前开始归档 OA 流程,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
if (!TryChangeStatus(record.id, Pending, Ending, null))
{
_logger.Info($"本地操作前归档 OA 流程未抢占记录,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
errorMessage = "OA 审批结果正在同步,请稍后再试";
return false;
}
if (!ForceEnd(record.oa_fileid))
{
TryChangeStatus(record.id, Ending, Pending, "OA 流程归档失败,等待重试");
_logger.Error($"本地操作前归档 OA 流程失败,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
errorMessage = "OA 流程归档失败,暂不能执行本地审批操作";
return false;
}
TryChangeStatus(record.id, Ending, "已归档", "本地操作前已归档 OA 流程");
_logger.Info($"本地操作前归档 OA 流程成功,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
return true;
}
public JObject Query(string requestId)
{
var query = "requestId=" + Uri.EscapeDataString(requestId ?? string.Empty)
+ "&systemToken=" + Uri.EscapeDataString(Config("SystemToken"))
+ "&systemName=" + Uri.EscapeDataString(Config("SystemName"));
var queryUrl = Config("TradeApprovalQueryUrl");
if (string.IsNullOrWhiteSpace(queryUrl))
throw new ServiceException("OA 配置缺少 TradeApprovalQueryUrl");
var url = queryUrl + (queryUrl.Contains("?") ? "&" : "?") + query;
var client = new HttpClientWrap(Config("BaseUrl"), 60);
return JObject.Parse(client.Get(url, null));
}
public bool ForceEnd(string requestId)
{
try
{
// 客户 OA 的 requestId 由 @RequestParam 接收,必须放在 URL QueryString。
var response = JObject.Parse(Post("ForceEndUrl", new { }, requestId));
var code = (int?)response["code"];
var status = (string)response["status"];
var responseData = response["data"] as JObject;
var resultCode = (string)responseData?["resultcode"];
var success = code == 0
&& string.Equals((string)response["status"], "SUCCESS", StringComparison.OrdinalIgnoreCase)
&& resultCode == "0";
_logger.Info($"OA 强制归档返回,requestId:{requestId}code:{code}status:{status}resultcode:{resultCode}success:{success}");
return success;
}
catch (Exception ex)
{
_logger.Error($"归档 OA 流程失败,requestId:{requestId}", ex);
return false;
}
}
public List<string> SyncPendingStatuses(string requestId = null)
{
var messages = new List<string>();
var staleAt = DateTime.Now.AddMinutes(-5);
var pending = DbContext.tradeApprovalOaResult
.Where(x => x.is_valid
&& !string.IsNullOrWhiteSpace(x.oa_fileid)
&& (string.IsNullOrWhiteSpace(requestId) || x.oa_fileid == requestId)
&& (x.status == Pending || (x.status == Syncing && (x.last_query_time == null || x.last_query_time < staleAt))))
.ToList();
_logger.Info($"开始轮询 OA 移动审批状态,requestId:{requestId ?? ""},待查询数量:{pending.Count}");
foreach (var record in pending)
{
var claimed = false;
try
{
_logger.Info($"查询 OA 移动审批状态,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
var response = Query(record.oa_fileid);
record.last_response = response.ToString();
var flow = response["data"] is JArray data
? data.FirstOrDefault(x => (string)x["requestId"] == record.oa_fileid) as JObject
: null;
var flowStatusType = (string)flow?["flowStatusType"];
var flowNode = (string)flow?["flowNode"];
var isReturned = flowStatusType == "0" && flowNode == "退回";
// 1=批准;3=OA 批准后的自然归档。TRS 主动归档的记录不会进入本次查询。
var isApproved = flowStatusType == "1" || flowStatusType == "3";
_logger.Info($"OA 移动审批查询结果,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}flowStatusType:{flowStatusType}flowNode:{flowNode},判定:{(isReturned ? "退" : isApproved ? "" : "")}");
if (!isApproved && !isReturned)
{
record.last_query_time = DateTime.Now;
DbContext.SaveChanges();
continue;
}
DbContext.SaveChanges();
if (!TryClaimForSync(record.id))
{
_logger.Info($"OA 审批结果同步未抢占记录,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid}");
continue;
}
claimed = true;
var trade = DbContext.trade.Find(record.trade_id);
if (trade == null)
{
SetClaimedStatus(record.id, "同步失败", "交易不存在");
continue;
}
if (GetCurrentNode(trade)?.id != record.approval_process_id)
{
SetClaimedStatus(record.id, "同步忽略", "当前审批节点已变化,忽略 OA 结果");
continue;
}
var result = new TradeOpenService(this).UpdateTradeProcessLog(new TradeOpenReqModel
{
tradeId = trade.id,
status = isReturned ? "reject" : "pass",
comments = isReturned ? "OA 移动审批退回" : "OA 移动审批通过",
isFromOa = true,
notNeedOperationHistory = false
});
if (!string.IsNullOrWhiteSpace(result.ErrorMsg))
{
SetClaimedStatus(record.id, Pending, result.ErrorMsg);
_logger.Error($"OA 审批结果回写本地失败,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},原因:{result.ErrorMsg}");
}
else
{
SetClaimedStatus(record.id, isReturned ? "退回" : "通过", null);
_logger.Info($"OA 审批结果回写本地成功,交易:{record.trade_id},节点:{record.approval_process_id}requestId:{record.oa_fileid},结果:{(isReturned ? "退" : "")}");
}
messages.Add(record.trade_id + ":" + (string.IsNullOrWhiteSpace(result.ErrorMsg) ? (isReturned ? "退回" : "通过") : Pending));
}
catch (Exception ex)
{
if (claimed)
{
SetClaimedStatus(record.id, Pending, ex.Message);
}
else
{
record.last_query_time = DateTime.Now;
record.oa_msg = ex.Message;
DbContext.SaveChanges();
}
_logger.Error($"轮询 OA 审批状态失败,requestId:{record.oa_fileid}", ex);
}
}
_logger.Info($"OA 移动审批状态轮询结束,已处理数量:{messages.Count}");
return messages;
}
private string Post(string urlKey, object payload, string requestId = null)
{
var path = Config(urlKey);
if (string.IsNullOrWhiteSpace(path))
path = urlKey == "ForceEndUrl" ? "/gateway/oaflow/forceEndOaFlow" : "/gateway/oaflow/createOaFlow";
var query = "systemToken=" + Uri.EscapeDataString(Config("SystemToken"))
+ "&systemName=" + Uri.EscapeDataString(Config("SystemName"));
if (urlKey == "ForceEndUrl")
query += "&requestId=" + Uri.EscapeDataString(requestId ?? string.Empty);
else
query += "&flowId=" + Uri.EscapeDataString(Config("MobileFlowId", "245103"))
+ "&isnextflow=" + Uri.EscapeDataString(Config("TradeApprovalIsNextFlow", "0"));
var url = path + (path.Contains("?") ? "&" : "?") + query;
return new HttpClientWrap(Config("BaseUrl"), 60).PostJson(url, payload, null);
}
private string Config(string key, string fallback = "")
{
if (key == "BaseUrl" || key == "SubUrl" || key == "TradeApprovalQueryUrl" || key == "ForceEndUrl")
{
var mockEnabled = bool.TryParse(AppManager.GetConfiguration()["TradeApprovalOaMock:Enabled"], out var enabled) && enabled;
var mockValue = AppManager.GetConfiguration()[$"TradeApprovalOaMock:{key}"];
if (mockEnabled && !string.IsNullOrWhiteSpace(mockValue))
return mockValue;
}
var value = AppManager.GetConfiguration()[$"oa_confg:{key}"];
return string.IsNullOrWhiteSpace(value) ? fallback : value;
}
private approvalprocess GetCurrentNode(trade trade)
{
if (trade == null) return null;
var processType = IsCloseScenario(trade) ? "CloseProcess" : "TradeProcess";
return DbContext.approvalprocess
.Where(x => x.processType == processType
&& x.order == trade.ProcessOrderId
&& (x.node == trade.ProcessOrderBranch || x.node == 0))
.OrderByDescending(x => x.node == trade.ProcessOrderBranch)
.FirstOrDefault();
}
private static bool IsCloseScenario(trade trade)
{
return trade.TradeStatus == ConsTrade.
|| trade.TradeStatus == ConsTrade.
|| trade.TradeStatus == ConsTrade.;
}
private bool TryClaimForSync(int recordId)
{
var now = DateTime.Now;
return DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, last_query_time = {1} " +
"WHERE id = {2} AND is_valid = 1 AND (status = {3} OR (status = {0} AND (last_query_time IS NULL OR last_query_time < {4})))",
Syncing, now, recordId, Pending, now.AddMinutes(-5)) == 1;
}
private bool TryChangeStatus(int recordId, string expectedStatus, string targetStatus, string message)
{
return DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, oa_msg = {1} WHERE id = {2} AND is_valid = 1 AND status = {3}",
targetStatus, message, recordId, expectedStatus) == 1;
}
private void SetClaimedStatus(int recordId, string status, string message)
{
DbContext.Database.ExecuteSqlRaw(
"UPDATE trade_approval_oa_result SET status = {0}, oa_msg = {1} WHERE id = {2} AND is_valid = 1 AND status = {3}",
status, message, recordId, Syncing);
}
private string GetSprLoginName(approvalprocess node)
{
using var baseDb = DbContextFactory.GetErpBaseContext();
var users = (from roleUser in baseDb.RoleUsers
join user in baseDb.SystemUsers on roleUser.UserId equals user.Id
where roleUser.RoleId == node.roleId && user.State == (int)UserState.Enabled
select user.LoginName).ToList();
if (users.Count == 0)
throw new ServiceException($"关联 OA 的审批角色未配置启用用户,角色ID:{node.roleId}");
if (users.Count > 1)
throw new ServiceException($"关联 OA 的审批角色配置了多个启用用户,角色ID:{node.roleId}");
if (string.IsNullOrWhiteSpace(users[0]))
throw new ServiceException($"关联 OA 的审批用户未配置登录名,角色ID:{node.roleId}");
return users[0];
}
private Dictionary<string, object> BuildPayload(trade trade, string creator, string applicant, string spr, string scenario)
{
var positions = DbContext.swap_position
.Where(x => x.SwapTradeId == trade.id && x.IsInitial && !x.Invalid)
.ToList();
var floatPosition = positions.FirstOrDefault(x => x.IsInitial && !string.IsNullOrWhiteSpace(x.UnderlyingCode));
var underlyingName = floatPosition == null
? string.Empty
: DbContext.underlying_manager.Where(x => x.UnderlyingCode == floatPosition.UnderlyingCode)
.Select(x => x.UnderlyingName)
.FirstOrDefault() ?? string.Empty;
var interestPositions = positions.Where(x => string.IsNullOrWhiteSpace(x.UnderlyingCode)
&& x.InterestMode != (int)InterestModeEnum.
&& x.InterestMode != (int)InterestModeEnum.
&& x.InterestMode != (int)InterestModeEnum.Unknown).ToList();
var marginPositions = positions.Where(x => x.InterestMode == (int)InterestModeEnum.
|| x.InterestMode == (int)InterestModeEnum.).ToList();
var tradeDate = trade.TradeDate?.ToString("yyyy-MM-dd") ?? string.Empty;
var startDate = floatPosition?.PosiStartDate.ToString("yyyy-MM-dd") ?? string.Empty;
var maturityDate = floatPosition?.PosiMatuirityDate?.ToString("yyyy-MM-dd") ?? string.Empty;
var contractDays = floatPosition?.PosiMatuirityDate == null
? string.Empty
: ((floatPosition.PosiMatuirityDate.Value.Date - floatPosition.PosiStartDate.Date).Days).ToString();
var elementLines = new List<string>
{
"交易日期=" + tradeDate,
"审批角色=衍生品负责人",
"交易编号=" + (trade.TradeNumber ?? string.Empty),
"对手方=" + (trade.ClientName ?? string.Empty),
"交易类型=" + scenario,
"浮动端多空头=" + (floatPosition == null ? string.Empty : floatPosition.PositionType == 1 ? "多头" : "空头"),
"利息端方向=" + JoinValues(interestPositions.Select(x => x.InterestDirection == 1 ? "收取" : "支付")),
"簿记账户=" + (trade.AssetBookName ?? string.Empty),
"结构类型=" + (trade.StructureType ?? string.Empty),
"标的代码=" + (floatPosition?.UnderlyingCode ?? string.Empty),
"标的名称=" + underlyingName,
"期初收益率=" + (floatPosition?.InitYtm?.ToString("0.####%") ?? string.Empty),
"期初价格=" + (floatPosition?.PosiNetPrice.ToString("0.####") ?? string.Empty),
"行权方式=" + (trade.GetExerciseModeCn() ?? string.Empty),
"合约期限=" + contractDays,
"数量=" + (floatPosition?.PosiQuantity.ToString("0.####") ?? string.Empty),
"起始日=" + startDate,
"到期日=" + maturityDate,
"交易员=" + (trade.TraderName ?? string.Empty),
"支付日期=" + JoinValues(marginPositions.Select(x => x.HappenDate?.ToString("yyyy-MM-dd") ?? string.Empty)),
"期初预付金=" + JoinValues(marginPositions.Select(x => x.InterestPrincipalFix.ToString("0.00"))),
"期初预付金方向=" + JoinValues(marginPositions.Select(x => x.InterestDirection == 1 ? "收取" : "支付"))
};
if (scenario == "开仓")
{
elementLines.Add("平仓金额=");
elementLines.Add("平仓日期=");
}
else
{
var eventType = scenario == "平仓" ? (int)SwapEventTypeEnum. : (int)SwapEventTypeEnum.;
var swapEvent = DbContext.swap_event.Where(x => x.SwapTradeId == trade.id && !x.Invalid && x.EventType == eventType)
.OrderByDescending(x => x.id).FirstOrDefault();
var unwind = string.IsNullOrWhiteSpace(swapEvent?.EventData) ? null : JsonHelper.Deserialize<UnwindData>(swapEvent.EventData);
elementLines.Add("平仓金额=" + (unwind?.SwapCloseAmount.ToString("0.##") ?? ""));
elementLines.Add("平仓日期=" + (unwind?.UnwindDate?.ToString("yyyy-MM-dd") ?? swapEvent?.ValueDate.ToString("yyyy-MM-dd") ?? ""));
}
var elements = string.Join("<br/>", elementLines);
var payload = new Dictionary<string, object>
{
["title"] = "衍生品系统TRS移动审批-" + trade.TradeNumber,
["creater"] = creator,
["applicant"] = applicant,
["spr"] = spr,
["applyDate"] = DateTime.Now.ToString("yyyy-MM-dd"),
["elements"] = elements
};
return payload;
}
private static string JoinValues(IEnumerable<string> values)
{
return string.Join("", values.Where(x => !string.IsNullOrWhiteSpace(x)));
}
}
}
+30
View File
@@ -287,6 +287,13 @@ namespace YLErp.BLL
.Select(a => new { a.order, a.roleId })
.ToList()
.ToDictionary(a => a.order, a => a.roleId);
var approvalNodes = db.approvalprocess
.Select(a => new { a.id, a.processType, a.order, a.node })
.ToList();
var oaRecords = db.tradeApprovalOaResult
.Where(x => swapTradeIds.Contains(x.trade_id) && x.is_valid)
.Select(x => new { x.id, x.trade_id, x.approval_process_id, x.status, x.oa_msg })
.ToList();
foreach (var tradeLinq in retListResult.rows)
{
@@ -305,6 +312,29 @@ namespace YLErp.BLL
tradeLinq.ProcessRoleName = name;
}
if (tradeLinq.TradeType == "收益互换")
{
var processType = tradeLinq.TradeStatus == "平仓待复核" || tradeLinq.TradeStatus == "行权待复核" || tradeLinq.TradeStatus == "互换待复核"
? "CloseProcess"
: "TradeProcess";
var currentNode = approvalNodes
.Where(x => x.processType == processType
&& x.order == tradeLinq.ProcessOrderId
&& (x.node == tradeLinq.ProcessOrderBranch || x.node == 0))
.OrderByDescending(x => x.node == tradeLinq.ProcessOrderBranch)
.FirstOrDefault();
var oa = currentNode == null ? null : oaRecords
.Where(x => x.trade_id == tradeLinq.id && x.approval_process_id == currentNode.id)
.OrderByDescending(x => x.id)
.FirstOrDefault();
if (oa != null)
{
tradeLinq.OaRemark = string.IsNullOrWhiteSpace(oa.oa_msg)
? oa.status
: oa.status + "" + oa.oa_msg;
}
}
if (tradeLinq.TradeType == "远期" || tradeLinq.TradeType == "掉期")
{
var option = db.trade_forward.FirstOrDefault(x => x.TradeId == tradeLinq.id);
@@ -120,6 +120,7 @@ namespace YLErp.Modules.TradeModule
// 需求①:进入审批流程时即应用触发条件——从起始节点开始,跳过所有不满足触发条件的节点。
// 若所有节点均不满足 → 直接审批通过(无需任何人审核)。
ApplyTriggerFromStart(tradeProcess, td, userId);
new TradeApprovalOAService(this).EnsureForCurrentNode(td);
}
/// <summary>
+45
View File
@@ -6,6 +6,7 @@ using YLErp.Modules.EodModule.SettlementModule;
using YLErp.Modules.RiskEngine;
using YLErp.Modules.RiskModule;
using YLErp.Modules.SuperviseReportModule.SAC.Service;
using YLErp.Modules.TradeModule;
using YLErp.Modules.TradeRiskCalcModule;
namespace YLErp.Web.App
@@ -17,6 +18,9 @@ namespace YLErp.Web.App
public class HostedTaskService : IHostedService
{
Timer _timer, _timerMonitor;
private int _tradeApprovalOaSyncRunning;
private DateTime _lastTradeApprovalOaSyncTime = DateTime.MinValue;
private readonly IYcLogger _logger = LogFactory.GetLogger<HostedTaskService>();
private readonly IServer server;
private readonly IHostApplicationLifetime hostApplicationLifetime;
@@ -44,6 +48,8 @@ namespace YLErp.Web.App
});
}
TrySyncTradeApprovalOaStatus();
if (AppHelper.RunTradeRiskCalcV2)
{
Task.Run(() =>
@@ -86,6 +92,45 @@ namespace YLErp.Web.App
});
}
private void TrySyncTradeApprovalOaStatus()
{
var intervalSeconds = PS.Config?.ErpElement.TradeApprovalOaSyncIntervalSeconds ?? 30;
if (intervalSeconds <= 0)
{
intervalSeconds = 30;
}
if (_lastTradeApprovalOaSyncTime != DateTime.MinValue
&& DateTime.Now - _lastTradeApprovalOaSyncTime < TimeSpan.FromSeconds(intervalSeconds))
{
return;
}
if (Interlocked.CompareExchange(ref _tradeApprovalOaSyncRunning, 1, 0) != 0)
{
return;
}
Task.Run(() =>
{
try
{
var results = new TradeApprovalOAService(new OptUserInfo(0, "系统移动审批同步", OptUserFrom.System))
.SyncPendingStatuses();
_logger.Info($"OA 移动审批状态定时同步完成,处理数量:{results.Count}");
}
catch (Exception ex)
{
_logger.Error("OA 移动审批状态定时同步失败", ex);
}
finally
{
_lastTradeApprovalOaSyncTime = DateTime.Now;
Interlocked.Exchange(ref _tradeApprovalOaSyncRunning, 0);
}
});
}
public Task StartAsync(CancellationToken cancellationToken)
{
object _state = System.Diagnostics.Debugger.IsAttached ? null : this;
+1 -1
View File
@@ -20,7 +20,7 @@ namespace YLErp.Web
GlobalConfig.EnableGenerateOnTradeClosed = true;
GlobalConfig.OverrideOldDoc = true;
GlobalConfig.ProjectTitle = "场外衍生品交易管理系统";
GlobalConfig.ProjectTitle = "固收场外衍生品系统";
GlobalConfig.GetNameByUserId = UserBLL.GetNameById;
GlobalConfig.SealHandler = new SealTaskHandleService(OptUserInfo.SystemUser);
@@ -13,6 +13,10 @@
<option value="结算确认书">结算确认书</option>
<option value="交易确认书(用印)">交易确认书(用印)</option>
<option value="结算确认书(用印)">结算确认书(用印)</option>
@* EQD-5320 资金通知书:value 与 bond-oms Java FundEmailHandler 查询的 EmailType 一字不差,勿改名;
此场景 {{文档编号}}/{{交易列表}} 渲染为空,可用 {{客户名称}}/{{交易日期}} *@
<option value="追保资金通知书">追保资金通知书</option>
<option value="轧差资金通知书">轧差资金通知书</option>
</select>
</div>
<div class="mb-4">
+11 -2
View File
@@ -32,6 +32,7 @@ namespace YLErp.Web.Controllers
// 如果上次登录失败,则显示验证码输入,否则不需要
//---------------------------------------
[ResponseCache(NoStore = true)]
public ActionResult Login(bool skipSso = false)
{
var logger = LogFactory.GetLogger<AccountController>();
@@ -69,7 +70,7 @@ namespace YLErp.Web.Controllers
// 注意:DisablePasswordLogin 的逻辑在 Login.cshtml 视图中处理
// 这里不再直接跳转,而是显示登录页,由页面决定显示什么内容
return LoginError(null);
return View();
}
public ActionResult Login2()
@@ -77,6 +78,14 @@ namespace YLErp.Web.Controllers
return Login(true);
}
/// <summary>
/// 内置超管账密登录兜底页
/// </summary>
public ActionResult AdminLogin()
{
return LoginError(null);
}
/// <summary>
/// 登录错误
/// </summary>
@@ -113,7 +122,7 @@ namespace YLErp.Web.Controllers
model.SID = DataProtectHelper.Encrypt(sid);
}
return View(nameof(Login), model);
return View(nameof(AdminLogin), model);
}
[HttpPost, ResponseCache(NoStore = true)]
+11 -3
View File
@@ -1237,7 +1237,8 @@ namespace YLErp.Web.Controllers
/// EQD-5320 批量发送结算确认书邮件——经服务端代理 bond-omsBondOmsInterface_BaseUrl 出口),
/// 替代前端直连 /trs_hub_api 反向代理(未配 nginx 的环境 404)。
/// </summary>
/// <param name="swapFlowEventIds">平仓流水id,逗号分隔</param>
/// <param name="swapFlowEventIds">平仓流水id,逗号分隔;单个令牌兼容【数字】与【EncryptId 加密串】两种形态
/// (批量按钮传网格数字id,单行按钮传 swap_flow_event.EncryptId——全站 enid 惯例)</param>
[HttpPost]
public JsonResult BatchSendSettleEmail(string swapFlowEventIds)
{
@@ -1248,14 +1249,21 @@ namespace YLErp.Web.Controllers
var ids = new List<long>();
foreach (var s in swapFlowEventIds.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
if (long.TryParse(s.Trim(), out var id))
var token = s.Trim();
long id;
if (!long.TryParse(token, out id) || id <= 0)
{
// 非数字令牌按 EncryptId 解密(解密失败/非法串返回0,不抛出)
try { id = DecryptLong(token); } catch { id = 0; }
}
if (id > 0)
{
ids.Add(id);
}
}
if (ids.Count == 0)
{
return JsonError("平仓流水id解析为空");
return JsonError("平仓流水id解析为空(既非数字也非有效加密ID):" + swapFlowEventIds);
}
var result = new SwapEndConfirmService(CurUser).BatchSendSettleEmail(ids);
return string.IsNullOrEmpty(result) ? JsonSuccess("发送成功") : JsonError(result);
@@ -473,6 +473,26 @@ namespace YLErp.Web.Controllers
{
return JsonError("资产类型 必须填写");
}
model.EtfSubType = model.EtfSubType?.Trim();
if (model.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund && string.IsNullOrEmpty(model.EtfSubType))
{
return JsonError("ETF 子类 必须填写");
}
if (model.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund)
{
var isValidEtfSubType = DictionaryBLL.GetDictionaryItems("ETF 子类", model.EtfSubType).Any();
if (!isValidEtfSubType)
{
return JsonError("ETF 子类 无效,请从字典选项中选择");
}
}
if (model.UnderlyingInstrumentType != ConsGlobal.InstrumentType.Fund)
{
model.EtfSubType = null;
}
if (ConsGlobal.InstrumentType.IsBond(model.UnderlyingInstrumentType))
{
model.ExJson = JsonHelper.Serialize(model.Bond);
+166
View File
@@ -0,0 +1,166 @@
@model LogOnModel
@{
Layout = null;
ViewBag.Title = "登录";
var captchaUrl = Url.Action("Captcha") + "?sid=" + Model.SID;
Model.LoadTime = DateTime.Now;
var disablePasswordLogin = PS.Config?.ErpElement?.DisablePasswordLogin ?? false;
var ssoEnable = YLErp.Web.Models.GeneralSSOConfig.Enable;
}
<!DOCTYPE html>
<html style="height:100%">
<head>
<meta charset="utf-8" />
<title>@GlobalConfig.ProjectTitle</title>
<link rel="stylesheet" href="~/statics/bundles/bundle.min.css?v=@(HtmlUtil.JsVersion)">
<style>
.login-container { position: relative; width: 100%; height: 100%; background-image: url(../../Statics/images/loginBackgroundImag.jpg); background-repeat: no-repeat; background-size: cover; }
.ms-title { width: 100%; line-height: 50px; text-align: center; font-size: 20px; color: #fff; border-bottom: 1px solid #ddd }
.ms-login { position: absolute; left: 50%; top: 50%; width: 350px; margin: -190px 0 0 -175px; border-radius: 5px; background: rgba(0,1,11,.32); overflow: hidden }
.ms-content { padding: 30px }
.login-table { width: 100%; }
.login-table td { padding-bottom: 10px; }
.login-input { width: 100% !important; height: 33.5px !important; text-align: left !important; }
.login-btn { text-align: center; margin-bottom: 18px; height: 36px; width: 100%; }
.btn-secondary { background-color: #6c757d; border-color: #6c757d; }
.btn-secondary:hover { background-color: #5a6268; border-color: #545b62; }
</style>
<script src="~/Statics/libs/jquery/jquery-3.5.1.min.js"></script>
<script>
$(function(){
CaptchaClick();
})
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);
}
</script>
</head>
<body style="height:100%">
<div class="login-container">
<div class="login-top">
<div class="login-logo">
@switch (YLErp.PS.Config.Company)
{
case YLErp.Configuration.CompanyEnum.申万:
<img src="/Images/sywg/maincenter.png" style="width: 300px;" />
break;
default:
<img src="/Images/loginlogo.png" style="height:150px" />
break;
}
</div>
</div>
<div class="ms-login">
<div class="ms-title">@GlobalConfig.ProjectTitle</div>
@if (disablePasswordLogin)
{
<!-- 禁用密码登录,只显示SSO按钮 -->
<div class="ms-content" style="text-align: center; padding: 40px 30px;">
<p style="color: #fff; margin-bottom: 20px;">请使用飞连登录</p>
@if (ssoEnable)
{
<a href="@YLErp.Web.Models.GeneralSSOConfig.AuthUrl" class="btn btn-primary login-btn" style="background-color: #1890ff; border-color: #1890ff;">飞连登录</a>
}
else
{
<p style="color: #ff6b6b;">SSO登录未配置,请联系管理员</p>
}
</div>
}
else
{
<!-- 正常登录表单 -->
<form id="form1" action="/Account/LoginHandle?g=@(Guid.NewGuid())" method="post" class="ms-content">
<input type="hidden" name="SID" value="@(Model.SID)" />
<input type="hidden" name="UserName" id="UserName" />
<input type="hidden" name="Password" id="Password" />
<input type="hidden" name="LoadTime" value="@(Model.LoadTime.ToString("yyyy-MM-dd HH:mm:ss"))" />
<table class="login-table">
<colgroup>
<col span="1" width="30" />
</colgroup>
<tr>
<td><img src="~/Images/username.png" /></td>
<td><input type="text" class="form-control login-input" id="UserName2" placeholder="用户名" autocomplete="off" /></td>
</tr>
<tr>
<td><img src="~/Images/password.png" /></td>
<td>
<!--阻止chrome浏览器自动填充-->
<input type="password" style="display:none" />
<input type="password" class="form-control login-input" id="Password2" placeholder="密码" autocomplete="new-password" />
</td>
</tr>
@if (Model.NeedCaptcha)
{
<tr>
<td></td>
<td>
<input type="text" name="CaptchaCode" id="CaptchaCode" class="form-control login-input mb-2" placeholder="输入验证码" autocomplete="off" />
<img onclick="CaptchaClick()" style="cursor:pointer;" id="captcha" />
</td>
</tr>
}
<tr>
<td></td>
<td>
<p id="errMsg" style="color:red;">@Html.Raw(Model?.ErrorMessage)</p>
<button id="sbumitbtn" type="submit" class="btn btn-primary login-btn">登&nbsp;录</button>
@if (ssoEnable)
{
<a href="@YLErp.Web.Models.GeneralSSOConfig.AuthUrl" class="btn btn-secondary login-btn">飞连登录</a>
}
</td>
</tr>
</table>
</form>
}
<div style="text-align:center;margin-bottom:1rem">
Copyright © 2020 YieldChain All rights reserved.
</div>
</div>
</div>
<div style="display:none">@(Model.Exception?.ToString())</div>
<script>
function encode(content) {
var result = [];
for (var i = 0; i < content.length; i++) {
result.push(content.charCodeAt(i));
}
return btoa(result);
}
function onSubmit() {
var userName = $('#UserName2').val();
var password = $('#Password2').val();
if (!userName || !password) {
$('#errMsg').text('用户名和密码必须填写');
return false;
}
if ($('#CaptchaCode').length && !$('#CaptchaCode').val()) {
$('#errMsg').text('请填写验证码');
return false;
}
$('#UserName').val(encode(userName));
$('#Password').val(encode(password));
return true;
}
$(function () {
$('#form1').on('submit', onSubmit);
});
window.history.replaceState(null, null, "/Account/AdminLogin?g=@(Guid.NewGuid())");
</script>
</body>
</html>
+80 -141
View File
@@ -1,12 +1,8 @@
@model LogOnModel
@{
Layout = null;
ViewBag.Title = "登录";
var captchaUrl = Url.Action("Captcha") + "?sid=" + Model.SID;
Model.LoadTime = DateTime.Now;
var disablePasswordLogin = PS.Config?.ErpElement?.DisablePasswordLogin ?? false;
var ssoEnable = YLErp.Web.Models.GeneralSSOConfig.Enable;
var authUrl = YLErp.Web.Models.GeneralSSOConfig.AuthUrl;
}
<!DOCTYPE html>
@@ -14,153 +10,96 @@
<head>
<meta charset="utf-8" />
<title>@GlobalConfig.ProjectTitle</title>
<link rel="stylesheet" href="~/statics/bundles/bundle.min.css?v=@(HtmlUtil.JsVersion)">
<style>
.login-container { position: relative; width: 100%; height: 100%; background-image: url(../../Statics/images/loginBackgroundImag.jpg); background-repeat: no-repeat; background-size: cover; }
.ms-title { width: 100%; line-height: 50px; text-align: center; font-size: 20px; color: #fff; border-bottom: 1px solid #ddd }
.ms-login { position: absolute; left: 50%; top: 50%; width: 350px; margin: -190px 0 0 -175px; border-radius: 5px; background: rgba(0,1,11,.32); overflow: hidden }
.ms-content { padding: 30px }
.login-table { width: 100%; }
.login-table td { padding-bottom: 10px; }
.login-input { width: 100% !important; height: 33.5px !important; text-align: left !important; }
.login-btn { text-align: center; margin-bottom: 18px; height: 36px; width: 100%; }
.btn-secondary { background-color: #6c757d; border-color: #6c757d; }
.btn-secondary:hover { background-color: #5a6268; border-color: #545b62; }
</style>
<script src="~/Statics/libs/jquery/jquery-3.5.1.min.js"></script>
<script>
$(function(){
CaptchaClick();
})
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);
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
color: #1F1F1F;
background: #F2F2F2;
background-image:
radial-gradient(circle at 12% 18%, #EDE6E6 0%, rgba(237,230,230,0) 42%),
radial-gradient(circle at 88% 82%, #E9E0E0 0%, rgba(233,224,224,0) 45%);
}
</script>
.login-page { position: relative; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 80px 24px 40px; }
.login-main { margin: auto; width: 100%; max-width: 420px; display: flex; flex-direction: column; align-items: center; }
.login-header { position: absolute; top: 30px; left: 34px; display: flex; align-items: center; gap: 10px; }
.login-header .brand { font-size: 23px; letter-spacing: -0.055em; color: #1D1A1A; font-weight: 400; line-height: 1; }
.login-header .brand b { font-weight: 800; color: #C0221E; }
.login-header .sub { display: block; margin-top: 3px; font-size: 11px; letter-spacing: 0.055em; color: #938A8A; }
.login-title { text-align: center; margin-bottom: 24px; }
.login-title h1 { font-size: 24px; line-height: 36px; letter-spacing: -0.025em; color: #1F1F1F; font-weight: 700; }
.login-title h1 .accent { color: #C0221E; font-weight: 800; letter-spacing: -0.04em; }
.login-title p { margin-top: 8px; font-size: 13px; line-height: 19.5px; color: #ADADAD; }
.login-card { width: 380px; max-width: 100%; padding: 36px 36px 32px; background: #FFFFFF; border-radius: 18px; box-shadow: 0 1px 4px rgba(0,0,0,.06), 0 10px 48px rgba(0,0,0,.1); text-align: center; }
.sso-icon { display: flex; align-items: center; justify-content: center; width: 80px; height: 80px; margin: 0 auto; border-radius: 40px; background: linear-gradient(135deg, #F5E8E8 0%, #F9E0E0 100%); box-shadow: 0 6px 24px rgba(192,34,30,.14); }
.sso-icon img { width: 36px; height: 36px; }
.sso-title { margin-top: 16px; font-size: 14px; font-weight: 600; line-height: 21px; color: #1F1F1F; }
.sso-desc { margin-top: 4px; font-size: 12px; line-height: 19.2px; color: #ADADAD; }
.sso-btn { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; height: 51px; margin-top: 32px; border: none; border-radius: 10px; background: #C0221E; box-shadow: 0 4px 18px rgba(192,34,30,.3); color: #fff; font-size: 15px; font-weight: 700; letter-spacing: 0.04em; text-decoration: none; cursor: pointer; transition: opacity .2s; }
.sso-btn:hover { opacity: .9; color: #fff; text-decoration: none; }
.sso-btn-icon { display: inline-flex; align-items: center; justify-content: center; width: 17px; height: 17px; line-height: 0; }
.sso-btn img { width: 17px; height: 17px; display: block; }
.sso-btn-text { line-height: 1; }
.sso-btn.disabled { background: #ccc; box-shadow: none; cursor: not-allowed; pointer-events: none; }
.sso-btn.loading { background: #E04B48; box-shadow: 0 4px 18px rgba(192,34,30,.25); pointer-events: none; opacity: 1; color: #fff; }
@@keyframes spin { to { transform: rotate(360deg); } }
.sso-spinner { width: 17px; height: 17px; border: 2px solid rgba(255,255,255,.35); border-top-color: #fff; border-radius: 50%; animation: spin .8s linear infinite; }
.sso-error { margin-top: 10px; font-size: 12px; color: #ff6b6b; }
.admin-link { position: absolute; bottom: 16px; left: 24px; font-size: 12px; color: #ADADAD; text-decoration: none; }
.admin-link:hover { color: #C0221E; }
</style>
</head>
<body style="height:100%">
<div class="login-container">
<div class="login-top">
<div class="login-logo">
@switch (YLErp.PS.Config.Company)
{
case YLErp.Configuration.CompanyEnum.申万:
<img src="/Images/sywg/maincenter.png" style="width: 300px;" />
break;
default:
<img src="/Images/loginlogo.png" style="height:150px" />
break;
}
<body>
<div class="login-page">
<div class="login-header">
<img src="~/Images/login/LogoMark.svg" style="width:50px;height:50px;" alt="logo" />
<div>
<span class="brand">One<b>Deriv</b></span>
<span class="sub">@GlobalConfig.ProjectTitle</span>
</div>
</div>
<div class="ms-login">
<div class="ms-title">@GlobalConfig.ProjectTitle</div>
@if (disablePasswordLogin)
{
<!-- 禁用密码登录,只显示SSO按钮 -->
<div class="ms-content" style="text-align: center; padding: 40px 30px;">
<p style="color: #fff; margin-bottom: 20px;">请使用飞连登录</p>
@if (ssoEnable)
{
<a href="@YLErp.Web.Models.GeneralSSOConfig.AuthUrl" class="btn btn-primary login-btn" style="background-color: #1890ff; border-color: #1890ff;">飞连登录</a>
}
else
{
<p style="color: #ff6b6b;">SSO登录未配置,请联系管理员</p>
}
</div>
}
else
{
<!-- 正常登录表单 -->
<form id="form1" action="/Account/LoginHandle?g=@(Guid.NewGuid())" method="post" class="ms-content">
<input type="hidden" name="SID" value="@(Model.SID)" />
<input type="hidden" name="UserName" id="UserName" />
<input type="hidden" name="Password" id="Password" />
<input type="hidden" name="LoadTime" value="@(Model.LoadTime.ToString("yyyy-MM-dd HH:mm:ss"))" />
<table class="login-table">
<colgroup>
<col span="1" width="30" />
</colgroup>
<tr>
<td><img src="~/Images/username.png" /></td>
<td><input type="text" class="form-control login-input" id="UserName2" placeholder="用户名" autocomplete="off" /></td>
</tr>
<tr>
<td><img src="~/Images/password.png" /></td>
<td>
<!--阻止chrome浏览器自动填充-->
<input type="password" style="display:none" />
<input type="password" class="form-control login-input" id="Password2" placeholder="密码" autocomplete="new-password" />
</td>
</tr>
@if (Model.NeedCaptcha)
{
<tr>
<td></td>
<td>
<input type="text" name="CaptchaCode" id="CaptchaCode" class="form-control login-input mb-2" placeholder="输入验证码" autocomplete="off" />
<img onclick="CaptchaClick()" style="cursor:pointer;" id="captcha" />
</td>
</tr>
}
<tr>
<td></td>
<td>
<p id="errMsg" style="color:red;">@Html.Raw(Model?.ErrorMessage)</p>
<button id="sbumitbtn" type="submit" class="btn btn-primary login-btn">登&nbsp;录</button>
@if (ssoEnable)
{
<a href="@YLErp.Web.Models.GeneralSSOConfig.AuthUrl" class="btn btn-secondary login-btn" target="_blank">飞连登录</a>
}
</td>
</tr>
</table>
</form>
}
<div style="text-align:center;margin-bottom:1rem">
Copyright © 2020 YieldChain All rights reserved.
<div class="login-main">
<div class="login-title">
<h1>欢迎登录 <span class="accent">OneDeriv</span></h1>
<p>@GlobalConfig.ProjectTitle</p>
</div>
<div class="login-card">
<div class="sso-icon"><img src="~/Images/login/Icon.svg" alt="feishu" /></div>
<div class="sso-title">一键跳转飞连认证</div>
<div class="sso-desc">系统将跳转至飞连身份认证中心<br />完成验证后自动返回</div>
@if (ssoEnable)
{
<a id="ssoBtn" class="sso-btn" href="@authUrl">
<span class="sso-btn-icon"><img src="~/Images/login/IconBolt.svg" alt="bolt" /></span>
<span class="sso-btn-text">飞连 SSO 登录</span>
</a>
}
else
{
<a class="sso-btn disabled">飞连 SSO 登录</a>
<p class="sso-error">SSO 登录未配置,请联系管理员</p>
}
</div>
</div>
</div>
<div style="display:none">@(Model.Exception?.ToString())</div>
<a class="admin-link" href="/Account/AdminLogin">旧版登录</a>
<script>
function encode(content) {
var result = [];
for (var i = 0; i < content.length; i++) {
result.push(content.charCodeAt(i));
}
return btoa(result);
}
function onSubmit() {
var userName = $('#UserName2').val();
var password = $('#Password2').val();
if (!userName || !password) {
$('#errMsg').text('用户名和密码必须填写');
return false;
}
if ($('#CaptchaCode').length && !$('#CaptchaCode').val()) {
$('#errMsg').text('请填写验证码');
return false;
}
$('#UserName').val(encode(userName));
$('#Password').val(encode(password));
return true;
}
$(function () {
$('#form1').on('submit', onSubmit);
});
window.history.replaceState(null, null, "/Account/Login?g=@(Guid.NewGuid())");
(function () {
var btn = document.getElementById('ssoBtn');
if (!btn) return;
btn.addEventListener('click', function (e) {
if (btn.classList.contains('loading')) { e.preventDefault(); return; }
btn.classList.add('loading');
var iconWrap = btn.querySelector('.sso-btn-icon');
var textEl = btn.querySelector('.sso-btn-text');
if (iconWrap) iconWrap.innerHTML = '<span class="sso-spinner"></span>';
if (textEl) textEl.textContent = '跳转认证中...';
});
})();
</script>
</body>
</html>
@@ -334,6 +334,9 @@
<span>审批规则</span>
<input :id="'ruleType'+item.Index+item.node" v-model="item.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
</div>
<div v-if="item.Type === 'TradeProcess'">
<label style="margin-left: 14px;"><input type="checkbox" v-model="item.isOaApproval" /> 关联 OA 移动审批</label>
</div>
<div class="trigger-condition-box">
<div class="trigger-condition-title">
<span>审批条件</span>
@@ -424,7 +427,10 @@
</div>
<div>
<span>审批规则</span>
<input :id="'ruleType'+childSecond.Index+childSecond.node" v-model="childSecond.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
<input :id="'ruleType'+childSecond.Index+childSecond.node" v-model="childSecond.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
</div>
<div v-if="childSecond.Type === 'TradeProcess'">
<label style="margin-left: 14px;"><input type="checkbox" v-model="childSecond.isOaApproval" /> 关联 OA 移动审批</label>
</div>
<div class="trigger-condition-box">
<div class="trigger-condition-title">
@@ -527,6 +533,9 @@
<span>审批规则</span>
<input :id="'closeRuleType'+item.Index+item.node" v-model="item.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
</div>
<div v-if="item.Type === 'CloseProcess'">
<label style="margin-left: 14px;"><input type="checkbox" v-model="item.isOaApproval" /> 关联 OA 移动审批</label>
</div>
<div class="trigger-condition-box">
<div class="trigger-condition-title">
<span>审批条件</span>
@@ -617,7 +626,10 @@
</div>
<div>
<span>审批规则</span>
<input :id="'closeRuleType'+childSecond.Index+childSecond.node" v-model="childSecond.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
<input :id="'closeRuleType'+childSecond.Index+childSecond.node" v-model="childSecond.ApprovalRules" type="text" name="selectRule" data-placeholder="" style="width: 200px; height: 20px" multiple="" />
</div>
<div v-if="childSecond.Type === 'CloseProcess'">
<label style="margin-left: 14px;"><input type="checkbox" v-model="childSecond.isOaApproval" /> 关联 OA 移动审批</label>
</div>
<div class="trigger-condition-box">
<div class="trigger-condition-title">
+3 -10
View File
@@ -1,4 +1,4 @@
@{
@{
ViewBag.Title = @GlobalConfig.ProjectTitle;
Layout = "~/Views/Shared/_MainLayout.cshtml";
}
@@ -11,15 +11,8 @@
<div class="container">
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4" align="center" style="margin-top: 100px">
<img src="/statics/images/logo_md_new.png" style="width:180px;height:160px;" />
</div>
<div class="col-md-4"></div>
</div>
<div class="row">
<div class="col-md-4"></div>
<div class="col-md-4" align="center" style="font-size: 30px; font-family: 微软雅黑; color: #8B2323;">
<div>欢迎使用</div>
<div class="col-md-4" align="center" style="margin-top: 100px; font-size: 30px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif; color: #8B2323;">
<div>欢迎使用 <span style="font-weight: 800; letter-spacing: -0.04em; color: #C0221E;">OneDeriv</span></div>
<div>@GlobalConfig.ProjectTitle</div>
</div>
<div class="col-md-4"></div>
+15 -8
View File
@@ -1195,59 +1195,66 @@
else if("Gamma*".Equals( quta.Name))
{
<tr>
<th title="Delta*">Gamma*</th>
<th title="Gamma*">Gamma*</th>
<td>{{calcResult.GammaContainsKnockOut}}</td>
</tr>
}
else if ("Delta_r".Equals(quta.Name))
{
<tr>
<th title="Delta*">Delta_r</th>
<th title="Delta_r">Delta_r</th>
<td>{{calcResult.Delta_r}}</td>
</tr>
}
else if ("Delta_r(1bp)".Equals(quta.Name))
{
<tr>
<th title="Delta*">Delta_r(1bp)</th>
<th title="Delta_r(1bp)">Delta_r(1bp)</th>
<td>{{calcResult.Delta_r_1bp}}</td>
</tr>
}
else if ("Dv01".Equals(quta.Name))
{
<tr>
<th title="Delta*">Dv01</th>
<th title="Dv01">Dv01</th>
<td>{{calcResult.Dv01}}</td>
</tr>
}
else if ("Gamma_r".Equals(quta.Name))
{
<tr>
<th title="Delta*">Gamma_r</th>
<th title="Gamma_r">Gamma_r</th>
<td>{{calcResult.Gamma_r}}</td>
</tr>
}
else if ("Gamma_r(1bp)".Equals(quta.Name))
{
<tr>
<th title="Delta*">Gamma_r(1bp)</th>
<th title="Gamma_r(1bp)">Gamma_r(1bp)</th>
<td>{{calcResult.Gamma_r_1bp}}</td>
</tr>
}
else if ("Vega_r".Equals(quta.Name))
{
<tr>
<th title="Delta*">Vega_r</th>
<th title="Vega_r">Vega_r</th>
<td>{{calcResult.Vega_r}}</td>
</tr>
}
else if ("Vega_r(1bp)".Equals(quta.Name))
{
<tr>
<th title="Delta*">Vega_r(1bp)</th>
<th title="Vega_r(1bp)">Vega_r(1bp)</th>
<td>{{calcResult.Vega_r_1bp}}</td>
</tr>
}
else if ("Vega(1bp)".Equals(quta.Name))
{
<tr>
<th title="Vega(1bp)">Vega(1bp)</th>
<td>{{calcResult.Vega_1bp}}</td>
</tr>
}
}
}
+1 -6
View File
@@ -1,4 +1,4 @@
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
@{
Layout = null;
}
@@ -233,8 +233,6 @@
<img class="gtja-img" src="/Images/bh/main_title.png" />
break;
default:
<!--基础页面,请考虑性能不要使用文件搜索方式-->
<img src="/App_Docs/Images/main_title.png" />
break;
}
<a href="/" class="gtja-title">
@@ -304,9 +302,6 @@
<div class="page-content" id="main-wrapper">
@RenderBody()
</div>
<div class="page-footer">
<p>Copyright &copy; 2020 YieldChain</p>
</div>
</div>
</div>
</div>
+2 -1
View File
@@ -187,7 +187,8 @@
<td>{{formatQuantity(floatPosition.Quantity)}}</td>
<td>
<vue-number-input v-model="floatPosition.TradingFee" v-bind:format="inputFormatInterestAmount" v-on:input="changeTradingFee"></vue-number-input>
<div class="bubble-box" style="margin-left:6px;">我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用</div>
@* <div class="bubble-box" style="margin-left:6px;">我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用</div> *@
<div class="bubble-box" style="margin-left:6px;">我方收取交易费用</div>
</td>
<td> <vue-number-input v-model="floatPosition.DividendIn" v-bind:format="inputFormatDividend" v-on:input="changeTradingFee"></vue-number-input></td>
<td style="font-size:18px;">{{formatAmount(floatPosition.FloatPnlSum)}}</td>
+6 -5
View File
@@ -6,11 +6,11 @@
bool isShowReCheckClose = ViewBag.IsShowReCheckClose;
bool hideFloatingIncomeDirection = PS.Config.ErpElement.SwapFloatingIncomeReceiveOnlyMode;
}
@section CSS{
@section CSS {
<link rel="stylesheet" href="~/Style/Css/unwindSwapTrade.css?v=@(HtmlUtil.JsVersion)" />
}
@section JS
{
{
<script>
var model = @Json.Serialize(Model);
model.StartDate = model.StartDate ? model.StartDate.substr(0, 10) : "";
@@ -90,7 +90,7 @@
</div>
<div class="form-group col-md-3">
<label class="formlabel">支付日期</label>
<vue-datepicker :mindate="minStartDate" :holiday="1" v-model="deal.PayDate" />
<vue-datepicker :mindate="minStartDate" :holiday="1" v-model="deal.PayDate" />
</div>
<div class="form-group col-md-3">
<label class="formlabel">是否罚息</label>
@@ -241,10 +241,11 @@
<td>{{formatQuantity(deal.CloseQty)}}</td>
<td>
<vue-number-input v-model="floatPosition.TradingFee" v-bind:format="inputFormatCloseAmount" v-on:input="changeTradingFee"></vue-number-input>
<div class="bubble-box">我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用</div>
@* <div class="bubble-box">我方{{floatPosition.PayDirection==1?"支付":"收取"}}交易费用</div> *@
<div class="bubble-box">我方收取交易费用</div>
</td>
<td>
<vue-number-input v-model="floatPosition.TradingFeePending" v-bind:format="inputFormatEqvNotional" disabled></vue-number-input>
<vue-number-input v-model="floatPosition.TradingFeePending" v-bind:format="inputFormatEqvNotional" disabled></vue-number-input>
</td>
<td>{{formatAmount(floatPosition.DividendIn)}}</td>
<td style="font-size:18px;">{{formatAmount(floatPosition.FloatPnlSum)}}</td>
+2 -1
View File
@@ -514,7 +514,8 @@
</td>
<td>
<vue-number-input v-model="item.PosiTradingFeePending" v-bind:format="inputFormatTradeSinglePriceFixed2"></vue-number-input>
<div class="bubble-box">我方{{item.PosiDirection==1?"支付":"收取"}}交易费用</div>
@* <div class="bubble-box">我方{{item.PosiDirection==1?"支付":"收取"}}交易费用</div> *@
<div class="bubble-box">我方收取交易费用</div>
</td>
</tr>
</tbody>
@@ -109,20 +109,6 @@
<input type="hidden" name="" value="" />
</form>
<div id="importEod" style="display:none;">
<form id="eodUploadForm" method="post" class="yc-panel" onsubmit="return false;" style="padding-bottom:30px;">
<h4 class="mb-4">互换日终估值导入</h4>
<div class="mb-4">
<input type="file" id="openFile" name="file" accept=".xlsx" />
</div>
<div class="mb-4">
<button type="button" class="btn btn-primary" onclick="importEodFile()">导入日终估值</button>
<a href="/App_Docs/导入模板/互换日终估值导入模板.xlsx" class="btn btn-primary">导入模板下载</a>
<button type="button" class="btn btn-primary" onclick="importClose()">关闭</button>
</div>
</form>
</div>
<script type="text/html" id="exportTpl">
<div style="padding:20px">
<p><input class="btn btn-primary " type="button" onclick="previewTradeAbstract();return false;" value="交易简讯"></p>
@@ -31,6 +31,7 @@
</script>
<script src="~/Scripts/app/tradeHelper.js?v=@HtmlUtil.JsVersion"></script>
<script src="~/Scripts/app/swaptrade/tradeEndConfirmList.js?v=@HtmlUtil.JsVersion"></script>
<script src="~/Scripts/app/swaptrade/tradeEndConfirmSendEmail.js?v=@HtmlUtil.JsVersion"></script>
}
<div class="searchdiv">
@@ -8,6 +8,8 @@
showDeltaS_S = PS.Config.Is长江
};
var bond = Model.Bond ?? new UnderlyingBond();
// ETF 子类下拉直接读取前端可维护字典;空选项用于新增页面的必填提示。
var etfSubtypeItems = YLErp.BLL.DictionaryBLL.GetList("ETF 子类", true, underlying.EtfSubType);
var blocks = new[] { underlying.Block1, underlying.Block2, underlying.Block3, underlying.Block4, underlying.Block5 };
for (var i = 1; i <= 5; i++)
{
@@ -28,13 +30,14 @@
}
}
.None, .Stock, .CommodityFutures, .Bonds {
.None, .Stock, .CommodityFutures, .Bonds, .Fund {
display: none;
}
.form-Stock .Stock,
.form-CommodityFutures .CommodityFutures,
.form-Bonds .Bonds {
.form-Bonds .Bonds,
.form-Fund .Fund {
display: block;
}
</style>
@@ -323,6 +326,31 @@
<label class='formlabel'>增值税率</label>
<input id='ValueAddedTax' class='text-box' type='number' value='@(underlying.ValueAddedTax)' name='ValueAddedTax' />
</div>
@* 仅基金及基金专户维护基金管理人 *@
<div class='form-group col-6 Fund'>
<label class='formlabel'>基金管理人</label>
<input id='InvestAdvisorName' class='text-box' type='text' value='@(underlying.InvestAdvisorName)' name='InvestAdvisorName' maxlength='100' />
</div>
@* 重点功能:ETF 子类由系统字典维护,使用 Fund 类控制显隐,并固定放在表单最后 *@
<div class='form-group col-6 Fund'>
<label class='formlabel'>ETF 子类</label>
<select id='EtfSubType' name='EtfSubType'>
@foreach (var item in etfSubtypeItems)
{
@* 编辑页面按实体中的字典名称回显当前选项 *@
if (item.Value == underlying.EtfSubType)
{
<option value='@item.Value' selected>@item.Text</option>
}
else
{
<option value='@item.Value'>@item.Text</option>
}
}
</select><span style='color:red'>*</span>
</div>
</div>
<div class="Stock" style="padding-left:130px;">
@@ -334,4 +362,4 @@
<button class="btn btn-primary" type="button" onclick="saveData()">保存</button>
<button class="btn btn-primary" type="button" onclick="layer.closeMe()">关闭</button>
</div>
</form>
</form>
@@ -49,6 +49,14 @@
@Html.MyDisplayFor(m => m.UnderlyingEnName)
@Html.MyDisplayFor(m => m.BBGTicker)
</tr>
@if (Model.UnderlyingInstrumentType == ConsGlobal.InstrumentType.Fund)
{
<tr>
@Html.MyDisplayFor(m => m.InvestAdvisorName)
@Html.MyDisplayFor(m => m.EtfSubType)
</tr>
}
<tr>
@Html.MyDisplayFor(m => m.UnderlyingType)
@if (pageObj.showDeltaS_S)
@@ -226,4 +234,4 @@
</table>
</div>
}
}
}
@@ -0,0 +1,25 @@
using System.Text.Json;
using YLErp.Helpers;
using YLErp.Model;
using YLErp.Modules.TradeModule;
namespace YLErp.Web.WebAPI.Controllers
{
/// <summary>国联民生衍生品移动审批轮询入口,由 PowerJob 等外部调度器调用。</summary>
[ApiController]
public class TradeApprovalOAJobController : ControllerBase
{
[HttpPost]
[Route("api/job/tradeApproval/syncOAStatus")]
public JsonResult SyncOAStatus([FromQuery] string requestId = null)
{
var configKey = AppManager.GetConfiguration()["JobConfig:ApiKey"];
if (!string.IsNullOrEmpty(configKey) && Request.Headers["X-Job-Api-Key"].FirstOrDefault() != configKey)
return new JsonResult(new { code = 401, msg = "Invalid API Key" });
var user = new OptUserInfo(0, "系统移动审批同步", OptUserFrom.System);
var messages = new TradeApprovalOAService(user).SyncPendingStatuses(requestId);
return new JsonResult(new { code = 0, data = new { total = messages.Count, details = messages } }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
}
}
}
@@ -0,0 +1,110 @@
using System.Collections.Concurrent;
using System.Text.Json;
using YLErp.Helpers;
namespace YLErp.Web.WebAPI.Controllers
{
/// <summary>
/// 仅供本地开发验证收益互换移动审批调用链的 OA 模拟接口。
/// </summary>
[ApiController]
[Route("api/mock/tradeApprovalOa")]
public class TradeApprovalOaMockController : ControllerBase
{
private static readonly ConcurrentDictionary<string, MockOaFlow> Flows = new();
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
[HttpPost("createOaFlow")]
public IActionResult CreateOaFlow([FromBody] JsonElement payload)
{
if (!IsEnabled()) return NotFound();
var requestId = "mock-" + Guid.NewGuid().ToString("N");
Flows[requestId] = new MockOaFlow { RequestId = requestId };
return Json(new { code = 0, data = new { msg = string.Empty, requestid = requestId }, status = "SUCCESS" });
}
[HttpGet("getRequestData")]
public IActionResult GetRequestData([FromQuery] string requestId)
{
if (!IsEnabled()) return NotFound();
if (!Flows.TryGetValue(requestId ?? string.Empty, out var flow))
return Json(new { code = 0, data = Array.Empty<object>(), status = "SUCCESS" });
return Json(new
{
code = 0,
data = new[] { new { requestId = flow.RequestId, flowStatusType = flow.FlowStatusType, status = flow.Status } },
status = "SUCCESS"
});
}
[HttpPost("forceEndOaFlow")]
public IActionResult ForceEndOaFlow([FromQuery] string requestId)
{
if (!IsEnabled()) return NotFound();
if (requestId != null && Flows.TryGetValue(requestId, out var flow))
{
flow.FlowStatusType = "3";
flow.Status = "归档";
flow.IsEnded = true;
}
return Json(new { code = 0, data = new { resultcode = "0" }, status = "SUCCESS" });
}
[HttpPost("setStatus")]
public IActionResult SetStatus([FromQuery] string requestId, [FromQuery] string flowStatusType, [FromQuery] string status = null)
{
if (!IsEnabled()) return NotFound();
if (flowStatusType != "0" && flowStatusType != "1" && flowStatusType != "2" && flowStatusType != "3")
return BadRequest(new { code = 1, msg = "flowStatusType 只能为 0、1 或 2" });
if (!Flows.TryGetValue(requestId ?? string.Empty, out var flow))
return NotFound(new { code = 1, msg = "未找到 requestId" });
if (flow.IsEnded)
return BadRequest(new { code = 1, msg = "OA 流程已归档" });
flow.FlowStatusType = flowStatusType;
if (!string.IsNullOrWhiteSpace(status))
flow.Status = status;
return Json(new { code = 0, data = new { requestId, flowStatusType, status = flow.Status }, status = "SUCCESS" });
}
[HttpGet("flows")]
public IActionResult GetFlows()
{
if (!IsEnabled()) return NotFound();
return Json(new
{
code = 0,
data = Flows.Values.Select(x => new { requestId = x.RequestId, flowStatusType = x.FlowStatusType, status = x.Status, isEnded = x.IsEnded }),
status = "SUCCESS"
});
}
private static bool IsEnabled()
{
return bool.TryParse(AppManager.GetConfiguration()["TradeApprovalOaMock:Enabled"], out var enabled) && enabled;
}
private JsonResult Json(object value)
{
return new JsonResult(value, JsonOptions);
}
private sealed class MockOaFlow
{
public string RequestId { get; set; }
public string FlowStatusType { get; set; } = "0";
public string Status { get; set; } = "创建";
public bool IsEnded { get; set; }
}
}
}
+14 -1
View File
@@ -78,13 +78,26 @@
"BaseUrl": "http://10.250.202.40:13590",
"SubUrl": "/gateway/oaflow/createOaFlow",
"QueryUrl": "/gateway/oaflow/getRequestData",
"TradeApprovalQueryUrl": "/gateway/oaflow/getRequestDataPro",
"ForceEndUrl": "/gateway/oaflow/forceEndOaFlow",
"MobileFlowId": 245103,
"DownloadFileUrl": "/gateway/oaflow/downloadSingleFile",
"SystemToken": "QIkox8sv3rVLtiT1wmI4Dp8wSjX9D84Glkp121vRino9fLobWnDiy8ppmqJcv1676TYuxZsGrYelx0whRCMaPyums2VnzIYrpqCFf5tyc3/x22Elf25xgRmd5pH8tLcnPuVOjMrUf0GsoytQCGFkhnrPajhsKq/mSwYQ4OA=", //OAkey
"SystemName": "onederiv",
"IsNextFlow": 1, // 0
"IsNextFlow": 1, // OA
"TradeApprovalIsNextFlow": 0, // OA
"Creator": "guoj", // OA
"Applicant": "guoj", // OA sqr
"FlowId": 779115,
"UploadUrl": "http://10.250.202.40:13590/gateway/oaflow/uploadDoc"
},
"TradeApprovalOaMock": {
"Enabled": false,
"BaseUrl": "http://localhost:49462",
"SubUrl": "/api/mock/tradeApprovalOa/createOaFlow",
"TradeApprovalQueryUrl": "/gateway/oaflow/getRequestDataPro",
"ForceEndUrl": "/api/mock/tradeApprovalOa/forceEndOaFlow"
},
"GeneralSSO": {
"enable": true, //SSO
"auth_url": "http://192.168.2.96:10117/sso/login", //SSO
+12 -1
View File
@@ -77,10 +77,21 @@
"oa_confg": {
"BaseUrl": "http://ip:port",
"SubUrl": "",
"TradeApprovalQueryUrl": "",
"UploadUrl": "/api/oa/upload", // OA
"SystemToken": "your-system-token", // OAToken
"SystemName": "OTC_TRADE", //
"IsNextFlow": 0
"IsNextFlow": 1, // OA
"TradeApprovalIsNextFlow": 0, // OA
"Creator": "", // OA
"Applicant": "" // OA sqr
},
"TradeApprovalOaMock": {
"Enabled": false,
"BaseUrl": "http://localhost:49462",
"SubUrl": "/api/mock/tradeApprovalOa/createOaFlow",
"TradeApprovalQueryUrl": "/api/mock/tradeApprovalOa/getRequestData",
"ForceEndUrl": "/api/mock/tradeApprovalOa/forceEndOaFlow"
},
"GeneralSSO": {
"enable": true, //SSO
+18
View File
@@ -120,6 +120,24 @@ describe('边界:计算器未返回的值不覆盖、无变化不写', () => {
});
});
describe('疑似到期债券值域闸门:ytm=0 且 净/全价均为面值100UAT 060203.IB 实证的退化形态)', () => {
test('三条件同时成立 → 命中闸门返回真实原因(不回写)', () => {
const resp = { cleanPrice: 100, dirtyPrice: 100, ytm: 0 };
const msg = SwapCalc.getBondCalcErrorMessage(resp);
expect(msg).toContain('疑似已到期');
expect(msg).toContain('手工填写');
});
test('正常券不命中 → 返回 null 放行回写', () => {
expect(SwapCalc.getBondCalcErrorMessage({ cleanPrice: 97.43, dirtyPrice: 100.00001369863016, ytm: 6.738278242318886 })).toBeNull();
});
test('ytm=0 但价格非面值(真实零息平价券)→ 不命中', () => {
expect(SwapCalc.getBondCalcErrorMessage({ cleanPrice: 99.5, dirtyPrice: 100, ytm: 0 })).toBeNull();
});
test('价格=面值 但 ytm 非 0(正常息票平价券)→ 不命中', () => {
expect(SwapCalc.getBondCalcErrorMessage({ cleanPrice: 100, dirtyPrice: 100.5, ytm: 3.2 })).toBeNull();
});
});
describe('错误反馈:getBondCalcErrorMessage(对齐 C# BondCalcHepler 的 errCode 守卫)', () => {
test('空响应 → 提示"无响应",且绝不回写', () => {
expect(SwapCalc.getBondCalcErrorMessage(null)).toBe("债券计算器无响应,已保留手工输入");
+3
View File
@@ -0,0 +1,3 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 3L4.5 21H18L16.5 33L31.5 15H18L19.5 3Z" fill="#C0221E" stroke="#C0221E" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 258 B

@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.20833 1.41666L2.125 9.91666H8.5L7.79167 15.5833L14.875 7.08333H8.5L9.20833 1.41666Z" stroke="white" stroke-width="1.55833" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

@@ -0,0 +1,12 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.1667 2.5H15.8333C8.46954 2.5 2.5 8.46954 2.5 15.8333V34.1667C2.5 41.5305 8.46954 47.5 15.8333 47.5H34.1667C41.5305 47.5 47.5 41.5305 47.5 34.1667V15.8333C47.5 8.46954 41.5305 2.5 34.1667 2.5Z" fill="url(#paint0_linear_4_83)"/>
<path d="M33.9582 3.5415H16.0415C9.13794 3.5415 3.5415 9.13794 3.5415 16.0415V33.9582C3.5415 40.8617 9.13794 46.4582 16.0415 46.4582H33.9582C40.8617 46.4582 46.4582 40.8617 46.4582 33.9582V16.0415C46.4582 9.13794 40.8617 3.5415 33.9582 3.5415Z" stroke="white" stroke-opacity="0.14" stroke-width="1.25"/>
<path d="M22.8398 13.084H22.834V36.917H22.8398V41.418H14.25C13.0077 41.418 12.0005 40.4102 12 39.168C12 37.9253 13.0074 36.918 14.25 36.918H18.334V15.835L13.9922 19.6846C13.0625 20.5083 11.6407 20.4223 10.8164 19.4932C9.99205 18.5633 10.078 17.1408 11.0078 16.3164L19.0908 9.15039C19.5024 8.78555 20.034 8.58301 20.584 8.58301H22.8398V13.084ZM26.834 8.58301C31.4997 8.58324 35.3927 10.0941 38.0918 13.1016C40.7585 16.0734 42 20.2182 42 25.001C41.9999 29.7836 40.7585 33.9278 38.0918 36.8994C35.3927 39.9069 31.4997 41.4168 26.834 41.417H25.8398V36.917H26.834C30.5005 36.9168 33.0666 35.7606 34.7422 33.8936C36.45 31.9905 37.4999 29.0508 37.5 25.001C37.5 20.9511 36.4499 18.0106 34.7422 16.1074C33.0666 14.2404 30.5004 13.0842 26.834 13.084H25.8398V8.58301H26.834Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_4_83" x1="5.83333" y1="4.16667" x2="44.1667" y2="46.6667" gradientUnits="userSpaceOnUse">
<stop stop-color="#E13A33"/>
<stop offset="0.5" stop-color="#C9231F"/>
<stop offset="1" stop-color="#961613"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,4 +1,4 @@
$(function () {
$(function () {
var __jq = {
PostData: typeof PostData !== 'undefined' ? PostData : {},
@@ -259,6 +259,13 @@ function ResetEditPanel(model) {
calcQuotaNameChecked.push(model.CalcQuota[i].Name);
}
}
// 编辑旧模板时,把模板未包含的新增指标(如 Vega(1bp))追加到勾选列表末尾,默认不勾选,保证与新增模板口径一致
var defaultQuota = pageObj && pageObj.DefaultQuota ? pageObj.DefaultQuota.CalcQuota : [];
for (var j = 0; j < defaultQuota.length; j++) {
if (!quotaOrder.includes(defaultQuota[j].Name)) {
quotaOrder.push(defaultQuota[j].Name);
}
}
InitQuotaCheckList(quotaOrder);
if (calcQuotaNameChecked != null && calcQuotaNameChecked.length > 0) {
@@ -559,6 +559,15 @@ function getColModelGrid() {
sortIndex: i++,
formatter: greeksFormat,
sortable: false
}, {
name: 'Vega_1bp',
label: 'Vega(1bp)',
index: 'Vega_1bp',
width: 100,
align: 'center',
sortIndex: i++,
formatter: greeksFormat,
sortable: false
},);
}
@@ -664,7 +673,7 @@ function TradeAmountFormat(cellValue, options, rowObject) {
}
function greeksFormat(cellValue, options, rowObject) {
if (cellValue) {
if (cellValue != null) {
return otcformat.trading.greek(cellValue);
}
return "";
@@ -254,7 +254,7 @@ const vueMarginTemplateName = function () {
}
//计算结果字段
const consCalcFields = Object.freeze(['TotalMargin', 'Pv', 'Delta', 'Gamma', 'Vega', 'Theta', 'Rho', 'PvContainsKnockOut', 'DeltaContainsKnockOut', 'DeltaInLots', 'GammaInLots', 'DeltaCash', 'GammaCash', 'VegaCash', 'GammaContainsKnockOut', 'Delta_r', 'Delta_r_1bp', 'Dv01', 'Gamma_r', 'Gamma_r_1bp', 'Vega_r', 'Vega_r_1bp']);
const consCalcFields = Object.freeze(['TotalMargin', 'Pv', 'Delta', 'Gamma', 'Vega', 'Theta', 'Rho', 'PvContainsKnockOut', 'DeltaContainsKnockOut', 'DeltaInLots', 'GammaInLots', 'DeltaCash', 'GammaCash', 'VegaCash', 'GammaContainsKnockOut', 'Delta_r', 'Delta_r_1bp', 'Dv01', 'Gamma_r', 'Gamma_r_1bp', 'Vega_r', 'Vega_r_1bp', 'Vega_1bp']);
var _trades, _tradeVues, _salesCommissionCtrl;
//交易保存
@@ -1959,13 +1959,14 @@ function createVue(index, baseVue, floating) {
calcResult.PvContainsKnockOut = pricingFormat.tradePrice(result.PvContainsKnockOut);
calcResult.DeltaContainsKnockOut = pricingFormat.greek(result.DeltaContainsKnockOut);
calcResult.GammaContainsKnockOut = pricingFormat.greek(result.GammaContainsKnockOut);
calcResult.Delta_r = result.Delta_r ? pricingFormat.greek(result.Delta_r) : "";
calcResult.Delta_r_1bp = result.Delta_r_1bp ? pricingFormat.greek(result.Delta_r_1bp) : "";
calcResult.Dv01 = result.Dv01 ? pricingFormat.greek(result.Dv01) : "";
calcResult.Gamma_r = result.Gamma_r ? pricingFormat.greek(result.Gamma_r) : "";
calcResult.Gamma_r_1bp = result.Gamma_r_1bp ? pricingFormat.greek(result.Gamma_r_1bp) : "";
calcResult.Vega_r = result.Vega_r ? pricingFormat.greek(result.Vega_r) : "";
calcResult.Vega_r_1bp = result.Vega_r_1bp ? pricingFormat.greek(result.Vega_r_1bp) : "";
calcResult.Delta_r = result.Delta_r != null ? pricingFormat.greek(result.Delta_r) : "";
calcResult.Delta_r_1bp = result.Delta_r_1bp != null ? pricingFormat.greek(result.Delta_r_1bp) : "";
calcResult.Dv01 = result.Dv01 != null ? pricingFormat.greek(result.Dv01) : "";
calcResult.Gamma_r = result.Gamma_r != null ? pricingFormat.greek(result.Gamma_r) : "";
calcResult.Gamma_r_1bp = result.Gamma_r_1bp != null ? pricingFormat.greek(result.Gamma_r_1bp) : "";
calcResult.Vega_r = result.Vega_r != null ? pricingFormat.greek(result.Vega_r) : "";
calcResult.Vega_r_1bp = result.Vega_r_1bp != null ? pricingFormat.greek(result.Vega_r_1bp) : "";
calcResult.Vega_1bp = result.Vega_1bp != null ? pricingFormat.greek(result.Vega_1bp) : "";
calcResult.TotalMargin = 0;
if (data === this.datas[this.datas.length - 1]) {
calcResult.TotalMargin = pageVue.GetTotalMargin(_.map(this.datas, x => x.trade), this.structureType);
@@ -352,6 +352,13 @@
return "债券计算收益率量级异常(" + yd + "),已保留手工输入;" +
"请检查估值日/价格输入或联系管理员核对债券计算服务";
}
// 疑似到期/无剩余现金流:估值日过到期日后 jquantlib 对空现金流求解得 ytm=0、净/全价均为面值100
// (三者同时成立在现实在市券中几乎不可能,UAT 060203.IB 实证)。宁可不回写并提示真实原因,
// 也不用退化值覆盖手工输入。与后端 BondCalcHepler.IsMaturedDegenerate 同口径,勿单边改动判定条件。
if (yd === 0 && cp === 100 && dp === 100) {
return "该债券疑似已到期或无剩余现金流(收益率=0、净价/全价均为面值100),无法反算," +
"请核对债券到期日或手工填写净价/全价/收益率";
}
return null;
}
@@ -523,38 +523,10 @@ function exportTrade() {
var fileName = "互换交易导出" + dateTemp;
main.toExcel("listGrid", fileName, "xls");
}
function showImport() {
layer.open({
type: 1,
title:"互换日终估值导入",
area: ['60%', '60%'],
fixed: false, //不固定
maxmin: true,
content: $("#importEod")
});
}
function importEodFile() {
let fileName = $("#openFile").val();
if (!fileName) {
return main.alert("请选择要导入的文件!");
}
let index = fileName.lastIndexOf(".");
let fileEx = index > 0 ? fileName.substr(index).toLowerCase() : '';
if (fileEx !== '.xlsx') {
return main.alert("只能上传.xlsx类型的文件!");
}
function success(data) {
if (data.success) {
main.alert(!data.totalNum ? '导入成功!' : "共 {0} 条数据,导入成功 {1} 条数据!".template(data.totalNum, data.successNum));
} else {
main.alert("导入失败:" + data.message);
}
}
var url = "/trs_hub_api/valuationEndOfDay/import";
main.confirm("确定导入?", UploadFile.bind(null, { inputId: 'openFile', url: url, success: success }));
}
function importClose() {
layer.closeAll();
}
// EQD-5320 清理:原"互换日终估值导入"弹窗与 importEodFile/showImport/importClose 已删除——
// 页面从未有入口按钮调用 showImport()(弹窗不可达),其内部 /trs_hub_api/valuationEndOfDay/import
// 直连反向代理的调用属死代码,也是全库最后一处 /trs_hub_api 依赖。如需恢复该功能,
// 按 BatchSendSettleEmail 同款模式走 C# 服务端代理(multipart 转发),勿再直连反代。
window.reloadData = reloadtrade;
@@ -26,6 +26,11 @@ function showToolName(cellValue, options, rowObject) {
//var disabled = pageData.canGenerate == true ? "" : "disabled";
//html += "<input type=\"button\" class=\"wentiEdit\" title='修改支付日期' onclick=\"ModifyPayDate('{0}','{1}')\" style=\"width: 120px!important;\" value=\"{2}\" {3}/>"
// .template(rowObject.swap_flow_event.EncryptId, rowObject.swap_flow_event.PayDate, "修改支付日期", disabled);
if (rowObject.ContractDocUrl && pageData.canSendEmail) {
html +=
"<input type=\"button\" class=\"wentiEdit\" title='发送结算确认书邮件到客户' onclick=\"sendSettleEmail('{0}','{1}')\" value=\"{2}\" />"
.template(rowObject.swap_flow_event.EncryptId, rowObject.trade.StructureType, "发送Email");
}
return html;
}
@@ -0,0 +1,40 @@
// 结算确认书:单行发送邮件(复用 /swaptrade2/BatchSendSettleEmail 服务端代理 → bond-oms Java 发信)
// 单独成文件,降低与 tradeEndConfirmList.js 的并发修改冲突;核心发送逻辑与网格渲染解耦,便于单测。
// 纯函数:根据加密平仓流水ID构造请求负载(不依赖 $.ajax / DOM),便于单元验证。
function buildSettleEmailSendPayload(encryptId) {
return { url: "/swaptrade2/BatchSendSettleEmail", swapFlowEventIds: encryptId };
}
// 单行发送结算确认书邮件;encryptId 为空、或与批量 SendEmailEitherReport 一致地跳过"多空组合"时早退。
function sendSettleEmail(encryptId, structureType) {
if (!encryptId) {
main.message("缺少平仓流水ID");
return;
}
if (structureType === "多空组合") {
main.message("多空组合不支持发送结算确认书");
return;
}
var payload = buildSettleEmailSendPayload(encryptId);
$.ajax({
type: "post",
url: payload.url,
data: { swapFlowEventIds: payload.swapFlowEventIds },
dataType: "json",
success: function (res) {
if (res && res.success) {
main.message(res.msg || "发送成功");
SearchClick();
} else {
main.message((res && (res.msg || res.message)) || "发送失败");
}
},
error: function (xhr) {
main.message("发送请求失败(HTTP " + xhr.status + "),请检查服务");
console.error("[单行发送结算确认书] 请求失败:", xhr);
},
beforeSend: function () { main.waitMe(true); },
complete: function () { main.waitMe(false); }
});
}
@@ -308,6 +308,7 @@ var app = new Vue({
approvalGroupId: 0,
conditionConfig: '',
triggerCondition: '',
isOaApproval: false,
_trigger: { tokens: [] }
}
var item2 = {
@@ -321,6 +322,7 @@ var app = new Vue({
approvalGroupId: 0,
conditionConfig: '',
triggerCondition: '',
isOaApproval: false,
_trigger: { tokens: [] }
}
if (selectType == "2") { //交易
@@ -469,6 +471,7 @@ var app = new Vue({
approvalGroupId: 0,
conditionConfig: '',
triggerCondition: '',
isOaApproval: false,
_trigger: { tokens: [] }
}
var childNodes = thisObj.openItems.filter(x => x.Index > index);
@@ -494,6 +497,7 @@ var app = new Vue({
approvalGroupId: 0,
conditionConfig: '',
triggerCondition: '',
isOaApproval: false,
_trigger: { tokens: [] }
}
thisObj.getRuleType();
@@ -956,7 +960,8 @@ var app = new Vue({
approvalGroupId: value.approvalGroupId,
approvalCondition: value.approvalCondition,
conditionConfig: value.conditionConfig,
triggerCondition: value.triggerCondition
triggerCondition: value.triggerCondition,
isOaApproval: value.isOaApproval === true
});
});
@@ -972,7 +977,8 @@ var app = new Vue({
approvalGroupId: value.approvalGroupId,
approvalCondition: value.approvalCondition,
conditionConfig: value.conditionConfig,
triggerCondition: value.triggerCondition
triggerCondition: value.triggerCondition,
isOaApproval: value.isOaApproval === true
});
});
// 需求②:了结/平仓/行权审批流程
@@ -990,6 +996,7 @@ var app = new Vue({
approvalCondition: value.approvalCondition,
conditionConfig: value.conditionConfig,
triggerCondition: value.triggerCondition
,isOaApproval: value.isOaApproval === true
});
});
}
@@ -79,6 +79,13 @@ var colModelGrid = [
width: 60,
align: 'left'
},
{
name: 'OaRemark',
label: 'OA备注',
index: 'OaRemark',
width: 80,
align: 'left'
},
{
name: 'id',
label: 'id',
@@ -1038,4 +1045,4 @@ function ProcessOptDateFilter(cellValue, options, rowObject) {
}
$("#DateFromTradeDate").parent().addClass("trade-wrap");
$("#DateFromOptDate").parent().addClass("time-wrap");
$("#DateFromOptDate").parent().addClass("time-wrap");
@@ -13,7 +13,8 @@
}());
const consSelect = ['MarketCode', 'UnderlyingInstrumentType', 'UnderlyingState', 'UnderlyingStatus', 'UpDownLimitType'];
const consSelect = ['MarketCode', 'UnderlyingInstrumentType', 'UnderlyingState', 'UnderlyingStatus', 'UpDownLimitType', 'EtfSubType'];
var autoUpDownLimit, autoVariety;
@@ -102,11 +103,15 @@ $(function () {
case "OtherBonds":
classType = "Bonds";
break;
case "Fund":
// 重点功能:资产类型为基金及基金专户时,显示所有 Fund 专属字段。
classType = "Fund";
break;
default:
classType = type;
break;
}
$('#editForm').removeClass("form-None form-Stock form-CommodityFutures form-CommoditySpot form-Bonds").addClass("form-" + classType);
$('#editForm').removeClass("form-None form-Stock form-CommodityFutures form-CommoditySpot form-Bonds form-Fund").addClass("form-" + classType);
}).trigger('change');
});
@@ -130,6 +135,11 @@ function saveData() {
return main.alert("资产品种类型 必须填写!");
}
// 重点功能:ETF 子类只对基金及基金专户显示并必填,先在前端阻止无效提交。
if (data.UnderlyingInstrumentType === "Fund" && !data.EtfSubType) {
return main.alert("ETF 子类 必须填写!");
}
if (data.UnderlyingInstrumentType === "CommodityFutures" && !data.MaturityDate) {
return main.alert("到期日 必须填写!");
}
@@ -144,4 +154,4 @@ function saveData() {
main.parentReloadData();
window.location.href = "/underlying_manager/underlying_managerView?enid=" + resp.obj.EncryptId;
});
}
}
+174 -12
View File
@@ -61,6 +61,16 @@
<li class="toctree-l3"><a class="reference internal" href="#id5">2.2 差分法</a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="#id6">3.新增希腊字母指标(利率敏感性指标)</a><ul>
<li class="toctree-l3"><a class="reference internal" href="#delta-r">3.1 Delta_r</a></li>
<li class="toctree-l3"><a class="reference internal" href="#delta-r-1bp">3.2 Delta_r(1BP)</a></li>
<li class="toctree-l3"><a class="reference internal" href="#dv01">3.3 DV01</a></li>
<li class="toctree-l3"><a class="reference internal" href="#gamma-r">3.4 Gamma_r</a></li>
<li class="toctree-l3"><a class="reference internal" href="#gamma-r-1bp">3.5 Gamma_r(1BP)</a></li>
<li class="toctree-l3"><a class="reference internal" href="#vega-r">3.6 Vega_r</a></li>
<li class="toctree-l3"><a class="reference internal" href="#vega-r-1bp">3.7 Vega_r(1BP)</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="model.html">附录2:期权定价方法概述</a></li>
@@ -221,8 +231,8 @@
<tr class="row-even"><td><p>Delta</p></td>
<td><p>设当前标的价格为 <span class="math notranslate nohighlight">\(S\)</span>,设 <span class="math notranslate nohighlight">\(S_\mathrm{u}=S+S_\mathrm{\bigtriangleup},S_\mathrm{d}=S-S_\mathrm{\bigtriangleup}\)</span></p>
<p><span class="math notranslate nohighlight">\(S_\mathrm{u}和S_\mathrm{d}\)</span> 代入定价公式, 得到期权价值 <span class="math notranslate nohighlight">\(V_\mathrm{u}和V_\mathrm{d}\)</span>; 则</p>
<p><span class="math notranslate nohighlight">\(Delta=(V_\mathrm{u}-V_\mathrm{d})/(S*0.01)\)</span></p>
<p>系统中 <span class="math notranslate nohighlight">\(S_\mathrm{\bigtriangleup}\)</span><span class="math notranslate nohighlight">\(0.005*S\)</span></p>
<p><span class="math notranslate nohighlight">\(Delta=(V_\mathrm{u}-V_\mathrm{d})/(2\times S_\mathrm{\bigtriangleup})\)</span></p>
<p>系统中 <span class="math notranslate nohighlight">\(S_\mathrm{\bigtriangleup}\)</span><span class="math notranslate nohighlight">\(0.0001\)</span>(绝对偏移,对应1bp</p>
</td>
</tr>
<tr class="row-odd"><td><p>DeltaCash</p></td>
@@ -232,7 +242,7 @@
<td><p>设当前标的价格为 <span class="math notranslate nohighlight">\(S\)</span>, 期权理论价值为 <span class="math notranslate nohighlight">\(V\)</span>; 设 <span class="math notranslate nohighlight">\(S_\mathrm{u}=S+S_\mathrm{\bigtriangleup},S_\mathrm{d}=S-S_\mathrm{\bigtriangleup}\)</span></p>
<p><span class="math notranslate nohighlight">\(S_\mathrm{u}和S_\mathrm{d}\)</span> 代入定价公式,得到期权价值 <span class="math notranslate nohighlight">\(V_\mathrm{u}和V_\mathrm{d}\)</span>; 则</p>
<p><span class="math notranslate nohighlight">\(Gamma=(V_\mathrm{u}+V_\mathrm{d}-2V)/S_\mathrm{\bigtriangleup}^2\)</span></p>
<p>系统中 <span class="math notranslate nohighlight">\(S_\mathrm{\bigtriangleup}\)</span><span class="math notranslate nohighlight">\(0.01*S\)</span></p>
<p>系统中 <span class="math notranslate nohighlight">\(S_\mathrm{\bigtriangleup}\)</span><span class="math notranslate nohighlight">\(0.0001\)</span>(绝对偏移,对应1bp</p>
</td>
</tr>
<tr class="row-odd"><td><p>GammaCash</p></td>
@@ -242,9 +252,12 @@
<td><p>设当前波动率为𝜎, 设 <span class="math notranslate nohighlight">\(𝜎_\mathrm{u}=𝜎+𝜎_\mathrm{\bigtriangleup},𝜎_\mathrm{d}=𝜎-𝜎_\mathrm{\bigtriangleup}\)</span></p>
<p><span class="math notranslate nohighlight">\(𝜎_\mathrm{u}和𝜎_\mathrm{d}\)</span> 代入定价公式,得到期权价值 <span class="math notranslate nohighlight">\(V_\mathrm{u}和V_\mathrm{d}\)</span>; 则</p>
<p><span class="math notranslate nohighlight">\(Vega=V_\mathrm{u}-V_\mathrm{d}\)</span></p>
<p>系统中 <span class="math notranslate nohighlight">\(𝜎_\mathrm{\bigtriangleup}\)</span> 为 0.5%</p>
<p>系统中 <span class="math notranslate nohighlight">\(𝜎_\mathrm{\bigtriangleup}\)</span> 为 0.0001(绝对偏移,对应1bp</p>
</td>
</tr>
<tr class="row-odd"><td><p>Vega(1bp)</p></td>
<td><p><span class="math notranslate nohighlight">\(Vega(1bp) = Vega \times 0.0001\)</span>(标的波动率 σ 上升 1 个基点对应的价值变动)</p></td>
</tr>
<tr class="row-odd"><td><p>Theta</p></td>
<td><p>设当前期权到期时间为 <span class="math notranslate nohighlight">\(T\)</span>, 期权理论价值为 <span class="math notranslate nohighlight">\(V,T'=T-frac(1),\)</span></p>
<p>其中 <span class="math notranslate nohighlight">\(𝑓𝑟𝑎𝑐(1)\)</span> 是根据设定的日期规则计算一天的年化时间长度。例如:</p>
@@ -255,21 +268,170 @@
</td>
</tr>
<tr class="row-even"><td><p>Rho</p></td>
<td><p>设无风险利率为 <span class="math notranslate nohighlight">\(r\)</span>,期权理论价值为 <span class="math notranslate nohighlight">\(V\)</span>; 设 <span class="math notranslate nohighlight">\(r_\mathrm{u}=r+10bp\)</span>;</p>
<td><p>设无风险利率为 <span class="math notranslate nohighlight">\(r\)</span>, 期权理论价值为 <span class="math notranslate nohighlight">\(V\)</span>; 设 <span class="math notranslate nohighlight">\(r_\mathrm{u}=r+1bp\)</span>;</p>
<p><span class="math notranslate nohighlight">\(r_\mathrm{u}\)</span> 代入定价公式,得到期权价值 <span class="math notranslate nohighlight">\(V_\mathrm{u}\)</span>;则</p>
<p><span class="math notranslate nohighlight">\(Rho=(V_\mathrm{u}-V)/10bp\)</span></p>
<p><span class="math notranslate nohighlight">\(Rho = V_\mathrm{u} - V\)</span></p>
</td>
</tr>
<tr class="row-odd"><td><p>亚式期权Delta</p></td>
<td><p>设当前已观察到的标的平均价格为 <span class="math notranslate nohighlight">\(S_\mathrm{A}\)</span>, 设 <span class="math notranslate nohighlight">\(S_\mathrm{A_\mathrm{u}}=S_\mathrm{A}+S_\mathrm{A_\mathrm{\bigtriangleup}}\)</span></p>
<p><span class="math notranslate nohighlight">\(S_\mathrm{A_\mathrm{u}}\)</span> 代入定价公式,得到期权价值 <span class="math notranslate nohighlight">\(V_\mathrm{u}\)</span>; 则</p>
<p><span class="math notranslate nohighlight">\(Delta=(V_\mathrm{u}-V)/S_\mathrm{A_\mathrm{\bigtriangleup}}\)</span></p>
<p>系统中 <span class="math notranslate nohighlight">\(S_\mathrm{A_\mathrm{\bigtriangleup}}\)</span><span class="math notranslate nohighlight">\(0.01*S_\mathrm{A}\)</span></p>
</td>
<tr class="row-odd"><td><p>RhoQ</p></td>
<td><p>计算逻辑同 Rho</p></td>
</tr>
</tbody>
</table>
</section>
<section id="id6">
<span id="new-greeks"></span><h2>3.新增希腊字母指标(利率敏感性指标)<a class="headerlink" href="#id6" title="Permalink to this headline"></a></h2>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>以下指标<strong>仅适用于</strong>利率收益率、利率债/信用债/其他债券、债券指数、国债期货四类资产。其他资产类别<strong>不计算</strong>这些指标,数值留空。</p>
</div>
<section id="delta-r">
<h3>3.1 Delta_r — 期权对利率的敏感性<a class="headerlink" href="#delta-r" title="Permalink to this headline"></a></h3>
<table class="docutils align-default">
<colgroup>
<col style="width: 20%" />
<col style="width: 15%" />
<col style="width: 65%" />
</colgroup>
<thead>
<tr class="row-odd"><th class="head"><p>资产类型</p></th>
<th class="head"><p>代表标的</p></th>
<th class="head"><p>公式</p></th>
</tr>
</thead>
<tbody>
<tr class="row-even"><td><p>利率收益率</p></td>
<td><p>GB10</p></td>
<td><p><span class="math notranslate nohighlight">\(Delta\_r = -Delta\)</span></p></td>
</tr>
<tr class="row-odd"><td><p>利率债/信用债/其他债券</p></td>
<td><p>210210.IB</p></td>
<td><p><span class="math notranslate nohighlight">\(Delta\_r = Delta \times P \times D\)</span></p></td>
</tr>
<tr class="row-even"><td><p>债券指数</p></td>
<td><p>CBA00621.CS</p></td>
<td><p><span class="math notranslate nohighlight">\(Delta\_r = Delta \times P \times D\)</span></p></td>
</tr>
<tr class="row-odd"><td><p>国债期货</p></td>
<td><p>TS2609</p></td>
<td><p><span class="math notranslate nohighlight">\(Delta\_r = Delta \times P \times D / CF\)</span></p></td>
</tr>
</tbody>
</table>
<div class="admonition note">
<p class="admonition-title">Note</p>
<ul class="simple">
<li><p><strong>P</strong>:债券/债券指数在计算日的全价/价格</p></li>
<li><p><strong>D</strong>:债券/债券指数在计算日的修正久期(债券指数使用平均市值法久期)</p></li>
<li><p><strong>CF</strong>:国债期货对应CTD债券的转换因子</p></li>
</ul>
</div>
</section>
<section id="delta-r-1bp">
<h3>3.2 Delta_r(1BP) — 利率变动1BP时Delta_r的变动<a class="headerlink" href="#delta-r-1bp" title="Permalink to this headline"></a></h3>
<p>适用于全部四种资产类型:</p>
<p><span class="math notranslate nohighlight">\(Delta\_r(1BP) = Delta\_r \times 0.0001\)</span></p>
</section>
<section id="dv01">
<h3>3.3 DV01 — 利率变动1BP时期权价值变动(基点价值)<a class="headerlink" href="#dv01" title="Permalink to this headline"></a></h3>
<p>适用于全部四种资产类型,与 Delta_r(1BP) 等价:</p>
<p><span class="math notranslate nohighlight">\(DV01 = Delta\_r(1BP)\)</span></p>
</section>
<section id="gamma-r">
<h3>3.4 Gamma_r — 期权对利率的二阶敏感性<a class="headerlink" href="#gamma-r" title="Permalink to this headline"></a></h3>
<table class="docutils align-default">
<colgroup>
<col style="width: 20%" />
<col style="width: 15%" />
<col style="width: 65%" />
</colgroup>
<thead>
<tr class="row-odd"><th class="head"><p>资产类型</p></th>
<th class="head"><p>代表标的</p></th>
<th class="head"><p>公式</p></th>
</tr>
</thead>
<tbody>
<tr class="row-even"><td><p>利率收益率</p></td>
<td><p>GB10</p></td>
<td><p><span class="math notranslate nohighlight">\(Gamma\_r = Gamma\)</span></p></td>
</tr>
<tr class="row-odd"><td><p>利率债/信用债/其他债券</p></td>
<td><p>210210.IB</p></td>
<td><p><span class="math notranslate nohighlight">\(Gamma\_r = Delta \times P \times C + (P \times D)^2 \times Gamma\)</span></p></td>
</tr>
<tr class="row-even"><td><p>债券指数</p></td>
<td><p>CBA00621.CS</p></td>
<td><p><span class="math notranslate nohighlight">\(Gamma\_r = Delta \times P \times C + (P \times D)^2 \times Gamma\)</span></p></td>
</tr>
<tr class="row-odd"><td><p>国债期货</p></td>
<td><p>TS2609</p></td>
<td><p><span class="math notranslate nohighlight">\(Gamma\_r = Delta \times P \times C / CF + (P \times D / CF)^2 \times Gamma\)</span></p></td>
</tr>
</tbody>
</table>
<div class="admonition note">
<p class="admonition-title">Note</p>
<ul class="simple">
<li><p><strong>C</strong>:债券/债券指数的凸性(债券指数使用平均市值法凸性)</p></li>
<li><p>P、D、CF 含义同 3.1 节</p></li>
</ul>
</div>
</section>
<section id="gamma-r-1bp">
<h3>3.5 Gamma_r(1BP) — 利率变动1BP时Gamma_r的变动<a class="headerlink" href="#gamma-r-1bp" title="Permalink to this headline"></a></h3>
<p>适用于全部四种资产类型:</p>
<p><span class="math notranslate nohighlight">\(Gamma\_r(1BP) = Gamma\_r \times 0.0001^2\)</span></p>
</section>
<section id="vega-r">
<h3>3.6 Vega_r — 期权对利率波动率的敏感性<a class="headerlink" href="#vega-r" title="Permalink to this headline"></a></h3>
<table class="docutils align-default">
<colgroup>
<col style="width: 20%" />
<col style="width: 15%" />
<col style="width: 65%" />
</colgroup>
<thead>
<tr class="row-odd"><th class="head"><p>资产类型</p></th>
<th class="head"><p>代表标的</p></th>
<th class="head"><p>公式</p></th>
</tr>
</thead>
<tbody>
<tr class="row-even"><td><p>利率收益率</p></td>
<td><p>GB10</p></td>
<td><p><span class="math notranslate nohighlight">\(Vega\_r = Vega\)</span></p></td>
</tr>
<tr class="row-odd"><td><p>利率债/信用债/其他债券</p></td>
<td><p>210210.IB</p></td>
<td><p><span class="math notranslate nohighlight">\(Vega\_r = Vega \times D \times ytm\)</span></p></td>
</tr>
<tr class="row-even"><td><p>债券指数</p></td>
<td><p>CBA00621.CS</p></td>
<td><p><span class="math notranslate nohighlight">\(Vega\_r = Vega \times D \times ytm\)</span></p></td>
</tr>
<tr class="row-odd"><td><p>国债期货</p></td>
<td><p>TS2609</p></td>
<td><p><span class="math notranslate nohighlight">\(Vega\_r = Vega \times D \times ytm\)</span>D和ytm使用对应CTD券的值)</p></td>
</tr>
</tbody>
</table>
<div class="admonition note">
<p class="admonition-title">Note</p>
<ul class="simple">
<li><p><strong>ytm</strong>:债券/债券指数在计算日的到期收益率(债券指数使用平均市值法到期收益率)</p></li>
<li><p>D 含义同 3.1 节(修正久期)</p></li>
</ul>
</div>
</section>
<section id="vega-r-1bp">
<h3>3.7 Vega_r(1BP) — 利率波动率变动1BP时Vega_r的变动<a class="headerlink" href="#vega-r-1bp" title="Permalink to this headline"></a></h3>
<p>适用于全部四种资产类型:</p>
<p><span class="math notranslate nohighlight">\(Vega\_r(1BP) = Vega\_r \times 0.0001\)</span></p>
</section>
</section>
</section>
</section>