- 修改 CalcPayment 方法添加 useBondPriceScale 参数区分债券和股票/基金的金额计算口径 - 债券利息按每100元面值票息通过BondPriceConverter转为入库金额,股票基金分红直接计算 - 在BondPaymentService中添加详细的参数说明文档注释 - 更新SwapDealService中分红计算逻辑,根据标的类型自动选择合适的金额转换方式 - 新增CorporateActionEventLifecycleTest单元测试验证公司行为事件生命周期管理 - 添加SplitCorporateActionTddTest测试验证拆合股功能 - 优化FundCorporateActionRollbackAndUnwindTest扩展到股票类型测试 - 更新前端OperationHistory页面表格列宽和显示格式支持更长的说明信息
342 lines
14 KiB
C#
342 lines
14 KiB
C#
using Newtonsoft.Json;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.Linq;
|
||
using System.Linq.Expressions;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using YLErp.DBModels;
|
||
|
||
namespace YLErp.Modules.SwapModule
|
||
{
|
||
public class SwapEventService : SwapTradeBaseService
|
||
{
|
||
public SwapEventService(OptUserInfo optUser) : base(optUser)
|
||
{
|
||
|
||
}
|
||
public SwapEventService(YLBaseService baseService) : base(baseService)
|
||
{
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成事件
|
||
/// </summary>
|
||
/// <param name="tradeDate">日期</param>
|
||
/// <param name="tradeId">互换交易码</param>
|
||
/// <param name="eventType">事件类型</param>
|
||
/// <param name="data">json数据</param>
|
||
/// <param name="save">是否保存</param>
|
||
/// <param name="clientCashId"></param>
|
||
/// <param name="eventRession"></param>
|
||
/// <param name="invalid">是否无效</param>
|
||
/// <returns></returns>
|
||
public swap_event AddSwapEventDate(DateTime tradeDate, int tradeId, int eventType, string data, int clientCashId, bool save = false,string eventRession="",bool invalid=false)
|
||
{
|
||
var swap_Event = new swap_event()
|
||
{
|
||
EventType = eventType,
|
||
EventData = data,
|
||
EventReason= eventRession,
|
||
SwapTradeId = tradeId,
|
||
ClientCashId = clientCashId,
|
||
Invalid = invalid,
|
||
ValueDate = tradeDate,
|
||
};
|
||
UpdateDbOption(swap_Event);
|
||
DbContext.swap_event.Add(swap_Event);
|
||
if (save)
|
||
{
|
||
DbContext.SaveChanges();
|
||
}
|
||
return swap_Event;
|
||
}
|
||
public swap_event UpdateSwapEvent()
|
||
{
|
||
var swapEvent= DbContext.swap_event.Where(x=>x.EventType== (int)SwapEventTypeEnum.新增交易 || x.EventType == (int)SwapEventTypeEnum.修改交易).OrderByDescending(o=>o.id).FirstOrDefault();
|
||
if (swapEvent!=null)
|
||
{
|
||
swapEvent.Invalid= false;
|
||
DbContext.SaveChanges();
|
||
}
|
||
return swapEvent;
|
||
}
|
||
/// <summary>
|
||
/// 获取上一日终归档日期
|
||
/// </summary>
|
||
/// <param name="tradeDate">当前清算日期</param>
|
||
/// <returns></returns>
|
||
public DateTime GetSwapEventDate(DateTime tradeDate, int tradeId, int? eventType)
|
||
{
|
||
Expression<Func<swap_event, bool>> expression = x => x.SwapTradeId == tradeId && x.ValueDate < tradeDate && !x.Invalid;
|
||
if (eventType.HasValue)
|
||
{
|
||
expression = expression.And(x => x.EventType == eventType);
|
||
}
|
||
var swapEvent = DbContext.swap_event.Where(expression).OrderByDescending(o => o.ValueDate).FirstOrDefault();
|
||
if (swapEvent != null)
|
||
{
|
||
return swapEvent.ValueDate;//上一日终归档日期
|
||
}
|
||
else//不存在归档日期
|
||
{
|
||
return tradeDate;
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 保存展期信息
|
||
/// </summary>
|
||
/// <param name="swap_Event"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public swap_event SaveExtensionTime(swap_event swap_Event)
|
||
{
|
||
var obj = DbContext.swap_event.Where(O => O.id == swap_Event.id).FirstOrDefault();
|
||
var trade = DbContext.trade.Where(O => O.id == swap_Event.SwapTradeId).FirstOrDefault();
|
||
if (trade == null)
|
||
{
|
||
throw new ServiceException("未找到交易,请刷新页面后再次尝试!");
|
||
}
|
||
var oldMaturityDate = trade.ExerciseDate.Value;
|
||
if (obj != null && !string.IsNullOrEmpty(obj.EventData))
|
||
{
|
||
oldMaturityDate = JsonConvert.DeserializeObject<ExtenstionData>(obj.EventData).OldMaturityDate;
|
||
}
|
||
if (swap_Event.extenstionData.NewMaturityDate <= oldMaturityDate)
|
||
{
|
||
throw new ServiceException("新到期日应晚于原到期日!");
|
||
}
|
||
if (obj != null && obj.ValueDate >= swap_Event.ValueDate)
|
||
{
|
||
throw new ServiceException("新展期日应晚于原展期日!");
|
||
}
|
||
if (swap_Event.extenstionData.NewMaturityDate< trade.StartDate)
|
||
{
|
||
throw new ServiceException("新到期日不应早于交易开始日期!");
|
||
}
|
||
swap_Event.extenstionData.OldMaturityDate = oldMaturityDate;
|
||
string data = JsonConvert.SerializeObject(swap_Event.extenstionData);
|
||
if (obj != null)
|
||
{
|
||
obj.Invalid = true;
|
||
}
|
||
swap_Event.EventReason = "交易展期";
|
||
trade.ExerciseDate = swap_Event.extenstionData.NewMaturityDate;
|
||
//修改互换观察日到期日
|
||
new SwapTradeService(this).UpdateObservationDay(trade.id, swap_Event.extenstionData.OldMaturityDate, trade.ExerciseDate.Value);
|
||
swap_Event = AddSwapEventDate(swap_Event.ValueDate, swap_Event.SwapTradeId, (int)SwapEventTypeEnum.展期, data, 0,false, swap_Event.EventReason);
|
||
DbContext.SaveChanges();
|
||
return swap_Event;
|
||
}
|
||
/// <summary>
|
||
/// 删除 展期
|
||
/// </summary>
|
||
/// <param name="id"></param>
|
||
/// <exception cref="ServiceException"></exception>
|
||
public void DeleteExtensionTime(long id)
|
||
{
|
||
var obj = DbContext.swap_event.Where(O => O.id == id).FirstOrDefault();
|
||
if (obj==null)
|
||
{
|
||
throw new ServiceException("未找到展期记录,请刷新页面后再次尝试!");
|
||
}
|
||
var trade = DbContext.trade.Where(O => O.id == obj.SwapTradeId).FirstOrDefault();
|
||
obj.Invalid = true;
|
||
obj.EventReason = "交易展期撤销";
|
||
if (!string.IsNullOrEmpty(obj.EventData))
|
||
{
|
||
obj.extenstionData = JsonConvert.DeserializeObject<ExtenstionData>(obj.EventData);
|
||
//修改到期日,互换观察日
|
||
new SwapTradeService(this).UpdateObservationDay(trade.id, trade.ExerciseDate.Value, obj.extenstionData.OldMaturityDate);
|
||
trade.ExerciseDate = obj.extenstionData.OldMaturityDate;
|
||
}
|
||
DbContext.SaveChanges();
|
||
}
|
||
/// <summary>
|
||
/// 获取当前展期记录
|
||
/// </summary>
|
||
/// <param name="tradeId"></param>
|
||
/// <returns></returns>
|
||
public swap_event GetExtensionTime(int tradeId)
|
||
{
|
||
var trade = DbContext.trade.Where(O => O.id == tradeId).FirstOrDefault();
|
||
if (trade == null)
|
||
{
|
||
throw new ServiceException("未找到交易,请刷新页面后再次尝试!");
|
||
}
|
||
var extension = DbContext.swap_event.Where(x => !x.Invalid && x.SwapTradeId == tradeId && x.EventType == (int)SwapEventTypeEnum.展期).FirstOrDefault();
|
||
|
||
if (extension==null)
|
||
{
|
||
extension = new swap_event();
|
||
extension.SwapTradeId = tradeId;
|
||
extension.ValueDate = DateTime.Now;
|
||
extension.extenstionData = new ExtenstionData()
|
||
{
|
||
OldMaturityDate = trade.ExerciseDate.Value,
|
||
NewMaturityDate=DateTime.Now
|
||
};
|
||
}
|
||
else
|
||
{
|
||
if (!string.IsNullOrEmpty(extension.EventData))
|
||
{
|
||
extension.extenstionData = JsonConvert.DeserializeObject<ExtenstionData>(extension.EventData);
|
||
}
|
||
}
|
||
extension.SwapTradeNumber = trade.TradeNumber;
|
||
return extension;
|
||
}
|
||
/// <summary>
|
||
/// 获取历史展期信息
|
||
/// </summary>
|
||
/// <param name="tradeId"></param>
|
||
/// <returns></returns>
|
||
public List<swap_event> GetExtenstionTimeEvents(int tradeId)
|
||
{
|
||
var events= DbContext.swap_event.Where(x => x.Invalid && x.SwapTradeId == tradeId&&x.EventType==(int)SwapEventTypeEnum.展期).OrderByDescending(o => o.ValueDate).ToList();
|
||
events.ForEach(x =>
|
||
{
|
||
if (!string.IsNullOrEmpty(x.EventData))
|
||
{
|
||
x.extenstionData = JsonConvert.DeserializeObject<ExtenstionData>(x.EventData);
|
||
}
|
||
});
|
||
return events;
|
||
}
|
||
/// <summary>
|
||
/// 获取交易操作历史。返回前会过滤掉登记日创建且尚未应用(Applied=false)
|
||
/// 的公司行为事件,避免交易详情在真实调整前展示一条已完成历史。
|
||
/// </summary>
|
||
/// <param name="tradeId">交易id</param>
|
||
/// <returns></returns>
|
||
public List<swap_event> GetOpreationHistorys(int tradeId)
|
||
{
|
||
List<swap_event> list = DbContext.swap_event
|
||
.Where(x => x.SwapTradeId == tradeId)
|
||
.OrderByDescending(o => o.id)
|
||
.ToList();
|
||
return FilterOperationHistory(list);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 过滤登记日创建且 Applied=false 的待生效公司行为事件,避免交易详情在真正
|
||
/// 调整前展示一条“已完成”历史。其他事件仍保留;无法解析的旧格式公司行为也
|
||
/// 保持可见,审计查询不能因为新 JSON 结构而静默丢失历史记录。
|
||
/// </summary>
|
||
public static List<swap_event> FilterOperationHistory(IEnumerable<swap_event> events)
|
||
{
|
||
return (events ?? Enumerable.Empty<swap_event>())
|
||
.Where(x => !IsPendingCorporateActionEvent(x))
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断指定事件是否为待生效的公司行为事件:EventType 为公司行为(13),
|
||
/// 且 EventData 反序列化后 Applied=false(登记日写入、尚未在真实除权日补齐调整后数据)。
|
||
/// 非公司行为类型、无法解析的旧格式或已应用的事件均返回 false,保证历史审计记录不被误删。
|
||
/// </summary>
|
||
public static bool IsPendingCorporateActionEvent(swap_event swapEvent)
|
||
{
|
||
if (swapEvent == null || swapEvent.EventType != (int)SwapEventTypeEnum.公司行为)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!TryDeserializeCorporateActionEventData(swapEvent, out var data))
|
||
{
|
||
// 非快照格式的历史公司行为保持可见,避免误删审计记录。
|
||
return false;
|
||
}
|
||
|
||
return !data.Applied;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 swap_event.EventData 安全反序列化为公司行为快照。事件为空、EventData
|
||
/// 为空白或 JSON 格式不匹配时返回 false 并将 data 置 null,调用方据此保留旧格式记录。
|
||
/// </summary>
|
||
public static bool TryDeserializeCorporateActionEventData(
|
||
swap_event swapEvent,
|
||
out CorporateActionEventData data)
|
||
{
|
||
data = null;
|
||
if (swapEvent == null || string.IsNullOrWhiteSpace(swapEvent.EventData))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
try
|
||
{
|
||
data = JsonConvert.DeserializeObject<CorporateActionEventData>(swapEvent.EventData);
|
||
return data != null;
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 公司行为说明使用稳定的键值格式,完整保留调整前后名义本金、价格、数量、
|
||
/// 待实现分红和现金流变化,操作历史无需重新计算即可核对。
|
||
/// </summary>
|
||
public static string BuildCorporateActionEventReason(CorporateActionEventData data)
|
||
{
|
||
if (data == null)
|
||
{
|
||
return "公司行为快照为空";
|
||
}
|
||
|
||
// 使用 InvariantCulture 固定小数与日期格式,说明文本不随服务器区域设置变化。
|
||
string D(decimal value) => value.ToString(CultureInfo.InvariantCulture);
|
||
string Date(DateTime? value) => value.HasValue
|
||
? value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||
: "";
|
||
|
||
return string.Join("; ", new[]
|
||
{
|
||
$"公司行为[{data.UnderlyingCode}]",
|
||
$"ExDividendDate={Date(data.ExDividendDate)}",
|
||
$"EffectiveDate={Date(data.EffectiveDate)}",
|
||
$"ExDividendInfoId={data.ExDividendInfoId}",
|
||
$"PositionId={data.PositionId}",
|
||
$"GiveCashAmount={D(data.GiveCashAmount)}",
|
||
$"GiveShareAmount={D(data.GiveShareAmount)}",
|
||
$"Split={(data.Split.HasValue ? D(data.Split.Value) : "")}",
|
||
$"RationedSharesAmount={D(data.RationedSharesAmount)}",
|
||
$"RationedSharesPrice={D(data.RationedSharesPrice)}",
|
||
"调整前",
|
||
$"BeforeNotional={D(data.BeforeNotional)}",
|
||
$"BeforePrice={D(data.BeforePrice)}",
|
||
$"BeforeQuantity={D(data.BeforeQuantity)}",
|
||
$"BeforePendingDividend={D(data.BeforePendingDividend)}",
|
||
"调整后",
|
||
$"AfterNotional={D(data.AfterNotional)}",
|
||
$"AfterPrice={D(data.AfterPrice)}",
|
||
$"AfterQuantity={D(data.AfterQuantity)}",
|
||
$"AfterPendingDividend={D(data.AfterPendingDividend)}",
|
||
$"CashFlowChange={D(data.CashFlowChange)}",
|
||
$"Applied={data.Applied}"
|
||
});
|
||
}
|
||
|
||
public void DeleteEvent(int tradeId)
|
||
{
|
||
var events = DbContext.swap_event.Where(x => x.Invalid && x.SwapTradeId == tradeId).ToList();
|
||
var flowEvents= DbContext.swap_flow_event.Where(x => x.DataState>0 && x.SwapTradeId == tradeId).ToList();
|
||
events.ForEach((x) =>
|
||
{
|
||
x.Invalid = true;
|
||
});
|
||
flowEvents.ForEach((x) =>
|
||
{
|
||
x.DataState = (int)SwapFlowDateStateEnum.废弃;
|
||
});
|
||
DbContext.SaveChanges();
|
||
}
|
||
}
|
||
}
|