refactor(dividend): 优化除权除息计算逻辑并改进数据类型精度
- 将除权除息相关数值字段从 double 类型改为 decimal 类型以提高精度 - 重构了红利比率计算方法 GetRatio,新增 GetRatioDecimal 方法使用 decimal 计算 - 修改价格和持仓数量计算逻辑,统一使用 decimal 进行高精度运算 - 更新数据库查询逻辑,将篮子标的判断从 IsBasket() 方法改为 CommodityCode 条件 - 优化 AddDividendInfos 方法中的批量处理逻辑,增加业务键冲突检测 - 添加数据源标识字段 DataSource 和来源更新时间字段 SourceUpdatedAt - 新增 FindExDividendByBusinessKey 和 MergeNonZeroDividendValues 辅助方法 - 更新结算服务中除权除息信息的获取方式,使用字典查找替代 LINQ JOIN - 修复前端保存除权信息时的响应处理逻辑 - 为基金类型也开放除权功能,不仅限于股票类型 - 添加单元测试验证篮子标的查询翻译逻辑的正确性
This commit is contained in:
@@ -28,7 +28,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
public List<BodTradePosition> Execute(DateTime settleDate, IEnumerable<EodTradePosition> positions)
|
||||
{
|
||||
var result = new List<BodTradePosition>();
|
||||
var dict = DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate == settleDate).ToDictionary(K => K.UnderlyingId, V => V);
|
||||
var dict = GetExDividendQuery(settleDate)
|
||||
.ToDictionary(K => K.UnderlyingId, V => V);
|
||||
foreach (var item in positions)
|
||||
{
|
||||
double cost = item.Cost,
|
||||
@@ -76,7 +77,8 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
useSaveTrades = new List<trade>();
|
||||
useSaveUndedrlyings = new List<underlying_manager>();
|
||||
var result = new List<bod_trade>();
|
||||
var dict = DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate == settleDate).ToDictionary(K => K.UnderlyingId, V => V);
|
||||
var dict = GetExDividendQuery(settleDate)
|
||||
.ToDictionary(K => K.UnderlyingId, V => V);
|
||||
var tradeIds = trades.Select(O => O.id);
|
||||
var dividendRatioDict = new DbRecordChangesService<TradeChanges>(this).GetValue(ConsInfoChangeType.UserChange, tradeIds, nameof(trade.DividendRatio), settleDate).ToDictionary(K => K.RecordId, V => { return double.TryParse(V.NewValue, out var temp) ? (double?)temp : null; });
|
||||
foreach (var t in trades)
|
||||
@@ -713,9 +715,15 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var ratio = overrideDividendRatio != null ? overrideDividendRatio.Value : GetRatio(info);
|
||||
double? result = price / ratio;
|
||||
return Math.Round(result ?? 0, 4, MidpointRounding.AwayFromZero);
|
||||
var decimalRatio = overrideDividendRatio.HasValue
|
||||
? (decimal)overrideDividendRatio.Value
|
||||
: GetRatioDecimal(info);
|
||||
if (decimalRatio == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
var result = (decimal)price / decimalRatio;
|
||||
return (double)Math.Round(result, 4, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -725,10 +733,17 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
/// <returns></returns>
|
||||
public double GetRatio(ex_dividend_info info)
|
||||
{
|
||||
var dividendRate = valuedateBLL.SystemDate.DividendRate / 100;
|
||||
return (double)GetRatioDecimal(info);
|
||||
}
|
||||
|
||||
private decimal GetRatioDecimal(ex_dividend_info info)
|
||||
{
|
||||
var dividendRate = (decimal)valuedateBLL.SystemDate.DividendRate / 100m;
|
||||
var closePrice = new EodPriceProvider(info.ExDividendDate.Value).GetPrice(info.UnderlyingCode, SettlementTypeEnum.ClosePrice);
|
||||
var cDivdPrice = (closePrice * 10.0 - (info.GiveCashAmount * (1 - dividendRate)) + info.RationedSharesAmount * info.RationedSharesPrice) / (10 + info.GiveShareAmount + info.RationedSharesAmount);
|
||||
return closePrice / cDivdPrice;
|
||||
var decimalClosePrice = (decimal)closePrice;
|
||||
var cDivdPrice = (decimalClosePrice * 10m - (info.GiveCashAmount * (1m - dividendRate)) + info.RationedSharesAmount * info.RationedSharesPrice) /
|
||||
(10m + info.GiveShareAmount + info.RationedSharesAmount);
|
||||
return cDivdPrice == 0 ? 0 : decimalClosePrice / cDivdPrice;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -751,18 +766,20 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
/// <returns></returns>
|
||||
public double GetPositionAmount(double amount, ex_dividend_info info)
|
||||
{
|
||||
double? result = amount * (1 + info.GiveShareAmount / 10.0);
|
||||
return Math.Round(result ?? 0, 12);
|
||||
var result = (decimal)amount * (1m + info.GiveShareAmount / 10m);
|
||||
return (double)Math.Round(result, 12, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
public IQueryable<ex_dividend_info> GetExDividendQuery(DateTime valueDate)
|
||||
{
|
||||
return DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate == valueDate);
|
||||
return DbContext.ex_dividend_info
|
||||
.Where(O => O.ValidStatus && O.ExDividendDate == valueDate);
|
||||
}
|
||||
|
||||
public IQueryable<ex_dividend_info> GetExDividendQuery(DateTime dateStart, DateTime dateEnd)
|
||||
{
|
||||
return DbContext.ex_dividend_info.Where(O => O.ValidStatus && O.ExDividendDate >= dateStart && O.ExDividendDate <= dateEnd);
|
||||
return DbContext.ex_dividend_info
|
||||
.Where(O => O.ValidStatus && O.ExDividendDate >= dateStart && O.ExDividendDate <= dateEnd);
|
||||
}
|
||||
|
||||
public IEnumerable<ex_dividend_info> GetExDividends(DateTime valueDate, params int[] underlyingIds)
|
||||
@@ -772,7 +789,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
{
|
||||
query = query.Where(n => underlyingIds.Contains(n.UnderlyingId));
|
||||
}
|
||||
return query.ToArray();
|
||||
return query;
|
||||
}
|
||||
|
||||
public IQueryable<ex_dividend_info> GetExDividendInfos(string underlyingCode)
|
||||
@@ -797,17 +814,17 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
{
|
||||
throw new ServiceException("请使用正确的模板上传");
|
||||
}
|
||||
var dict = new Dictionary<string, ex_dividend_info>();
|
||||
var dividendInfos = new List<ex_dividend_info>();
|
||||
for (var i = 0; i < dt.Rows.Count; i++)
|
||||
{
|
||||
var info = new ex_dividend_info
|
||||
{
|
||||
UnderlyingCode = dt.Rows[i]["股票代码"]?.ToString(),
|
||||
ExDividendDate = DateTime.TryParse(getColValueFromTable(dt.Rows[i], "股权登记日"), out var date) ? date : DateTime.MinValue,
|
||||
GiveCashAmount = double.TryParse(getColValueFromTable(dt.Rows[i], "派息金额"), out var value) ? value : 0,
|
||||
GiveShareAmount = double.TryParse(getColValueFromTable(dt.Rows[i], "送股股数"), out value) ? value : 0,
|
||||
RationedSharesAmount = double.TryParse(getColValueFromTable(dt.Rows[i], "配股股数"), out value) ? value : 0,
|
||||
RationedSharesPrice = double.TryParse(getColValueFromTable(dt.Rows[i], "配股股价"), out value) ? value : 0,
|
||||
GiveCashAmount = decimal.TryParse(getColValueFromTable(dt.Rows[i], "派息金额"), out var value) ? value : 0,
|
||||
GiveShareAmount = decimal.TryParse(getColValueFromTable(dt.Rows[i], "送股股数"), out value) ? value : 0,
|
||||
RationedSharesAmount = decimal.TryParse(getColValueFromTable(dt.Rows[i], "配股股数"), out value) ? value : 0,
|
||||
RationedSharesPrice = decimal.TryParse(getColValueFromTable(dt.Rows[i], "配股股价"), out value) ? value : 0,
|
||||
OptId = OptUser.UserId,
|
||||
OptName = OptUser.UserName,
|
||||
OptDate = DateTime.Now
|
||||
@@ -824,9 +841,9 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
{
|
||||
throw new ServiceException($"第{i + 1}行股权登记日不正确");
|
||||
}
|
||||
dict[$"{info.ExDividendDate}{info.UnderlyingCode}"] = info;
|
||||
dividendInfos.Add(info);
|
||||
}
|
||||
if (!AddDividendInfos(dict.Values, out var errMsg))
|
||||
if (!AddDividendInfos(dividendInfos, out var errMsg))
|
||||
{
|
||||
throw new ServiceException(errMsg);
|
||||
}
|
||||
@@ -841,48 +858,149 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
return "";
|
||||
}
|
||||
|
||||
private ex_dividend_info FindExDividendByBusinessKey(int underlyingId, DateTime exDividendDate, int excludedId = 0)
|
||||
{
|
||||
return DbContext.ex_dividend_info.FirstOrDefault(O => O.UnderlyingId == underlyingId
|
||||
&& O.ExDividendDate >= exDividendDate
|
||||
&& O.ExDividendDate < exDividendDate.AddDays(1)
|
||||
&& (excludedId <= 0 || O.id != excludedId));
|
||||
}
|
||||
|
||||
private static void MergeNonZeroDividendValues(ex_dividend_info target, ex_dividend_info source)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
if (source == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
if (source.GiveCashAmount != 0m)
|
||||
{
|
||||
target.GiveCashAmount = source.GiveCashAmount;
|
||||
}
|
||||
if (source.GiveShareAmount != 0m)
|
||||
{
|
||||
target.GiveShareAmount = source.GiveShareAmount;
|
||||
}
|
||||
if (source.RationedSharesAmount != 0m)
|
||||
{
|
||||
target.RationedSharesAmount = source.RationedSharesAmount;
|
||||
}
|
||||
if (source.RationedSharesPrice != 0m)
|
||||
{
|
||||
target.RationedSharesPrice = source.RationedSharesPrice;
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddDividendInfos(IEnumerable<ex_dividend_info> infos, out string errMsg)
|
||||
{
|
||||
try
|
||||
{
|
||||
var keys = infos.Select(O => $"{O.ExDividendDate?.ToString("yyyy-MM-dd")}{O.UnderlyingCode}");
|
||||
var ids = infos.Select(O => O.id).ToHashSet();
|
||||
|
||||
var data = from dividendDb in DbContext.ex_dividend_info.Where(O => keys.Contains(O.ExDividendDate + O.UnderlyingCode) && O.ValidStatus)
|
||||
where !ids.Contains(dividendDb.id)
|
||||
select dividendDb;
|
||||
if (data.Any())
|
||||
var dividendInfos = infos?.ToList();
|
||||
if (dividendInfos == null || dividendInfos.Count == 0)
|
||||
{
|
||||
var dd = data.Select(O => O.UnderlyingCode + "_" + O.ExDividendDate).ToArray();
|
||||
errMsg = string.Join(",", dd) + "已存在除息信息,请修改原数据";
|
||||
errMsg = "没有可保存的除权除息信息";
|
||||
return false;
|
||||
}
|
||||
|
||||
var basketList =
|
||||
DataCacheProvider.GetUnderlyingDataSource()
|
||||
.AsQueryable().Where(O => O.IsBasket() && O.SubData != null)
|
||||
.Select(O => new { O.UnderlyingCode, O.SubData });
|
||||
IEnumerable<eod_stock_price> priceList = null;
|
||||
foreach (var item in infos)
|
||||
var preparedInfos = new List<(ex_dividend_info Item, underlying_manager Underlying, DateTime ExDividendDate)>();
|
||||
var preparedIndexes = new Dictionary<(int UnderlyingId, DateTime ExDividendDate), int>();
|
||||
var recordKeys = new Dictionary<int, (int UnderlyingId, DateTime ExDividendDate)>();
|
||||
foreach (var item in dividendInfos)
|
||||
{
|
||||
if (item == null || string.IsNullOrWhiteSpace(item.UnderlyingCode))
|
||||
{
|
||||
errMsg = "标的代码信息不存在";
|
||||
return false;
|
||||
}
|
||||
|
||||
var underlying = underlying_managerBLL.GetByCode(item.UnderlyingCode);
|
||||
if (underlying == null)
|
||||
{
|
||||
errMsg = $"{item.UnderlyingCode} 标的信息不存在";
|
||||
return false;
|
||||
}
|
||||
if (!item.ExDividendDate.HasValue)
|
||||
{
|
||||
errMsg = "股权登记日信息不存在";
|
||||
return false;
|
||||
}
|
||||
|
||||
var exDividendDate = item.ExDividendDate.Value.Date;
|
||||
var businessKey = (underlying.id, exDividendDate);
|
||||
if (item.id > 0
|
||||
&& recordKeys.TryGetValue(item.id, out var existingRecordKey)
|
||||
&& existingRecordKey != businessKey)
|
||||
{
|
||||
errMsg = "同一除权信息不能重复保存";
|
||||
return false;
|
||||
}
|
||||
|
||||
item.UnderlyingId = underlying.id;
|
||||
item.GiveCashAmount = item.GiveCashAmount.FormatValue(6);
|
||||
item.RationedSharesAmount = item.RationedSharesAmount.FormatValue(6);
|
||||
item.RationedSharesPrice = item.RationedSharesPrice.FormatValue(6);
|
||||
item.GiveShareAmount = item.GiveShareAmount.FormatValue(6);
|
||||
item.ValidStatus = true;
|
||||
item.OptId = OptUser.UserId;
|
||||
item.OptName = OptUser.UserName;
|
||||
item.OptDate = DateTime.Now;
|
||||
var dividend = item.id > 0 ? DbContext.ex_dividend_info.Where(O => O.id == item.id).FirstOrDefault() : null;
|
||||
item.ExDividendDate = exDividendDate;
|
||||
item.GiveCashAmount = OtcFormatHelper.FormatValue(item.GiveCashAmount, 6);
|
||||
item.RationedSharesAmount = OtcFormatHelper.FormatValue(item.RationedSharesAmount, 6);
|
||||
item.RationedSharesPrice = OtcFormatHelper.FormatValue(item.RationedSharesPrice, 6);
|
||||
item.GiveShareAmount = OtcFormatHelper.FormatValue(item.GiveShareAmount, 6);
|
||||
|
||||
if (preparedIndexes.TryGetValue(businessKey, out var preparedIndex))
|
||||
{
|
||||
var preparedItem = preparedInfos[preparedIndex].Item;
|
||||
// 批量保存除权信息时,检测同一业务键(标的+日期)下是否存在冲突的数据库记录。
|
||||
if ((preparedItem.id == 0) != (item.id == 0)
|
||||
|| preparedItem.id > 0 && item.id > 0 && preparedItem.id != item.id)
|
||||
{
|
||||
errMsg = $"{item.UnderlyingCode} {exDividendDate:yyyy-MM-dd}除权信息不能合并不同记录";
|
||||
return false;
|
||||
}
|
||||
|
||||
MergeNonZeroDividendValues(preparedItem, item);
|
||||
if (item.id > 0)
|
||||
{
|
||||
recordKeys[item.id] = businessKey;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.id > 0)
|
||||
{
|
||||
recordKeys[item.id] = businessKey;
|
||||
}
|
||||
preparedIndexes.Add(businessKey, preparedInfos.Count);
|
||||
preparedInfos.Add((item, underlying, exDividendDate));
|
||||
}
|
||||
|
||||
var basketList =
|
||||
DataCacheProvider.GetUnderlyingDataSource()
|
||||
.AsQueryable().Where(O => O.CommodityCode == "篮子标的" && O.SubData != null)
|
||||
.Select(O => new { O.UnderlyingCode, O.SubData });
|
||||
IEnumerable<eod_stock_price> priceList = null;
|
||||
foreach (var prepared in preparedInfos)
|
||||
{
|
||||
var item = prepared.Item;
|
||||
var underlying = prepared.Underlying;
|
||||
var itemDate = prepared.ExDividendDate;
|
||||
var dividend = item.id > 0
|
||||
? DbContext.ex_dividend_info.FirstOrDefault(O => O.id == item.id)
|
||||
: FindExDividendByBusinessKey(underlying.id, itemDate);
|
||||
if (dividend == null)
|
||||
{ DbContext.ex_dividend_info.Add(item); }
|
||||
{
|
||||
if (item.id > 0)
|
||||
{
|
||||
errMsg = "未找到要修改的除权除息信息";
|
||||
return false;
|
||||
}
|
||||
item.DataSource = ExDividendDataSources.Manual;
|
||||
item.SourceUpdatedAt = null;
|
||||
item.ValidStatus = true;
|
||||
item.OptId = OptUser.UserId;
|
||||
item.OptName = OptUser.UserName;
|
||||
item.OptDate = DateTime.Now;
|
||||
DbContext.ex_dividend_info.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (checkDividendInfoExecuteStatus(dividend))
|
||||
@@ -890,6 +1008,13 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
errMsg = $"{dividend.UnderlyingCode} {dividend.ExDividendDate?.ToString("yyyy-MM-dd")}除权信息保存失败,该信息已被执行,不允许修改!";
|
||||
return false;
|
||||
}
|
||||
var conflictingDividend = FindExDividendByBusinessKey(underlying.id, itemDate, dividend.id);
|
||||
if (conflictingDividend != null)
|
||||
{
|
||||
errMsg = $"{item.UnderlyingCode} {itemDate:yyyy-MM-dd}除权信息已存在,不能修改为该业务键";
|
||||
return false;
|
||||
}
|
||||
var sourceUpdatedAt = dividend.SourceUpdatedAt;
|
||||
dividend.UnderlyingCode = item.UnderlyingCode;
|
||||
dividend.UnderlyingId = item.UnderlyingId;
|
||||
dividend.ExDividendDate = item.ExDividendDate;
|
||||
@@ -897,21 +1022,23 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
dividend.RationedSharesAmount = item.RationedSharesAmount;
|
||||
dividend.RationedSharesPrice = item.RationedSharesPrice;
|
||||
dividend.GiveShareAmount = item.GiveShareAmount;
|
||||
dividend.ValidStatus = item.ValidStatus;
|
||||
dividend.OptId = item.OptId;
|
||||
dividend.OptName = item.OptName;
|
||||
dividend.OptDate = item.OptDate;
|
||||
dividend.ValidStatus = true;
|
||||
dividend.DataSource = ExDividendDataSources.Manual;
|
||||
dividend.SourceUpdatedAt = sourceUpdatedAt;
|
||||
dividend.OptId = OptUser.UserId;
|
||||
dividend.OptName = OptUser.UserName;
|
||||
dividend.OptDate = DateTime.Now;
|
||||
}
|
||||
if (!basketList.Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var codes = basketList.Where(O => O.SubData.Contains(item.UnderlyingCode)).Select(O => O.UnderlyingCode);
|
||||
if (!codes.Any())
|
||||
var basketCodes = basketList.Where(O => O.SubData.Contains(item.UnderlyingCode)).Select(O => O.UnderlyingCode);
|
||||
if (!basketCodes.Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var removePriceList = DbContext.eod_stock_price.Where(O => codes.Contains(O.UnderlyingCode) && O.ValueDate > item.ExDividendDate);
|
||||
var removePriceList = DbContext.eod_stock_price.Where(O => basketCodes.Contains(O.UnderlyingCode) && O.ValueDate > item.ExDividendDate);
|
||||
if (!removePriceList.Any())
|
||||
{
|
||||
continue;
|
||||
@@ -954,7 +1081,7 @@ namespace YLErp.Modules.TradeModule.DealModule
|
||||
return true;
|
||||
}
|
||||
//查询篮子标的对应交易是否执行过收盘操作;
|
||||
var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(O => O.IsBasket() && O.SubData != null && O.SubData.Contains(info.UnderlyingCode)).Select(O => O.UnderlyingCode).ToArray();
|
||||
var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(O => O.CommodityCode == "篮子标的" && O.SubData != null && O.SubData.Contains(info.UnderlyingCode)).Select(O => O.UnderlyingCode).ToArray();
|
||||
tradeQuery = from t in DbContext.trade.Where(O => umList.Contains(O.UnderlyingCode) && O.TradeDate <= info.ExDividendDate && O.ExerciseDate >= info.ExDividendDate && O.DividendDate >= O.TradeDate)
|
||||
join et in DbContext.eod_trade.Where(O => ConsTrade.LiveTradeStatusList.Contains(O.TradeStatus))
|
||||
on new { t.id, ValueDate = t.TradeDate.Value } equals new { id = et.TradeId, et.ValueDate }
|
||||
|
||||
Reference in New Issue
Block a user