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

This commit is contained in:
张名锐
2026-08-14 14:51:56 +08:00
8 changed files with 330 additions and 84 deletions
@@ -0,0 +1,68 @@
using System;
using System.IO;
namespace YLErp.Modules.EodModule
{
[TestClass]
public class DividendBasketQueryTranslationTest
{
[TestMethod]
public void DividendBasketQueriesUseEfTranslatableCommodityCondition()
{
var source = ReadDividendServiceSource();
var addDividendQuery = ExtractQuery(
source,
"var basketList =",
"IEnumerable<eod_stock_price> priceList = null;");
var executeStatusQuery = ExtractQuery(
source,
"var umList = DataCacheProvider.GetUnderlyingDataSource().AsQueryable(",
").Select(O => O.UnderlyingCode).ToArray();");
AssertQueryUsesCommodityCondition(addDividendQuery, "AddDividendInfos");
AssertQueryUsesCommodityCondition(executeStatusQuery, "checkDividendInfoExecuteStatus");
}
private static void AssertQueryUsesCommodityCondition(string query, string methodName)
{
Assert.IsFalse(
query.Contains("IsBasket()", StringComparison.Ordinal),
$"{methodName} must not put IsBasket() in an IQueryable predicate.");
Assert.IsTrue(
query.Contains("O.CommodityCode == \"篮子标的\"", StringComparison.Ordinal),
$"{methodName} must filter baskets with the EF-translatable CommodityCode condition.");
}
private static string ExtractQuery(string source, string startMarker, string endMarker)
{
var start = source.IndexOf(startMarker, StringComparison.Ordinal);
Assert.IsTrue(start >= 0, $"Could not find query marker: {startMarker}");
var end = source.IndexOf(endMarker, start + startMarker.Length, StringComparison.Ordinal);
Assert.IsTrue(end >= 0, $"Could not find query end marker: {endMarker}");
return source.Substring(start, end + endMarker.Length - start);
}
private static string ReadDividendServiceSource()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory != null)
{
var path = Path.Combine(
directory.FullName,
"YLErpDAL",
"Modules",
"TradeModule",
"DealModule",
"DividendService.cs");
if (File.Exists(path))
{
return File.ReadAllText(path);
}
directory = directory.Parent;
}
Assert.Fail("Could not locate DividendService.cs from the test output directory.");
return string.Empty;
}
}
}
@@ -123,7 +123,7 @@ namespace YLErp.Modules.EodModule
{
UnderlyingCode = "002043.SZ",
ExDividendDate = new DateTime(2020, 7, 6),
GiveCashAmount = 2.5,
GiveCashAmount = 2.5m,
GiveShareAmount = 0,
RationedSharesAmount = 0,
RationedSharesPrice = 0,
@@ -135,7 +135,7 @@ namespace YLErp.Modules.EodModule
{
UnderlyingCode = "600406.SH",
ExDividendDate = new DateTime(2020, 7, 8),
GiveCashAmount = 2.9,
GiveCashAmount = 2.9m,
GiveShareAmount = 0,
RationedSharesAmount = 0,
RationedSharesPrice = 0,
@@ -147,7 +147,7 @@ namespace YLErp.Modules.EodModule
{
UnderlyingCode = "600406.SH",
ExDividendDate = new DateTime(2020, 7, 8),
GiveCashAmount = 2.9,
GiveCashAmount = 2.9m,
GiveShareAmount = 0,
RationedSharesAmount = 0,
RationedSharesPrice = 0,
@@ -159,7 +159,7 @@ namespace YLErp.Modules.EodModule
{
UnderlyingCode = "601021.SH",
ExDividendDate = new DateTime(2020, 7, 8),
GiveCashAmount = 2.0006,
GiveCashAmount = 2.0006m,
GiveShareAmount = 0,
RationedSharesAmount = 0,
RationedSharesPrice = 0,
@@ -171,7 +171,7 @@ namespace YLErp.Modules.EodModule
{
UnderlyingCode = "300001.SZ",
ExDividendDate = new DateTime(2020, 7, 13),
GiveCashAmount = 0.2,
GiveCashAmount = 0.2m,
GiveShareAmount = 0,
RationedSharesAmount = 0,
RationedSharesPrice = 0,
+22 -4
View File
@@ -5,6 +5,12 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace YLErp.DBModels
{
public static class ExDividendDataSources
{
public const string Manual = "Manual";
public const string MarketData = "MarketData";
}
[Table("ex_dividend_info")]
public class ex_dividend_info : DBModelWithOperator
{
@@ -35,29 +41,41 @@ namespace YLErp.DBModels
/// 派息金额
/// </summary>
[DisplayName("派息金额")]
public double GiveCashAmount { get; set; }
public decimal GiveCashAmount { get; set; }
/// <summary>
/// 送股手数
/// </summary>
[DisplayName("送股股数")]
public double GiveShareAmount { get; set; }
public decimal GiveShareAmount { get; set; }
/// <summary>
/// 配股手数
/// </summary>
[DisplayName("配股股数")]
public double RationedSharesAmount { get; set; }
public decimal RationedSharesAmount { get; set; }
/// <summary>
/// 配股手数
/// </summary>
[DisplayName("配股价")]
public double RationedSharesPrice { get; set; }
public decimal RationedSharesPrice { get; set; }
/// <summary>
/// 是否有效
/// </summary>
public bool ValidStatus { get; set; }
/// <summary>
/// Ownership of the record. Manual records always take precedence over imports.
/// </summary>
[DisplayName("数据来源"), Required, MaxLength(32)]
public string DataSource { get; set; } = ExDividendDataSources.Manual;
/// <summary>
/// Last update timestamp supplied by the market-data provider.
/// </summary>
[DisplayName("来源更新时间")]
public DateTime? SourceUpdatedAt { get; set; }
}
public class ex_dividend_infoReq : BaseSearchReq
@@ -39,14 +39,15 @@ namespace YLErp.Modules.EodModule
predicate = PredicateBuilder.Create<T>(n => n.ValueDate == settleDate).And(predicate);
}
// 除权数据不在这里做 SQL 左连接:同一标的一天只允许一条有效除权记录,
// 但历史脏数据可能存在重复行。左连接会把一条 EOD 持仓扩成多行,进而重复
// 参与后续风险/结算计算。先取得 EOD+BOD 的唯一持仓结果,再按标的代码匹配
// 除权记录,可以把重复业务键暴露为 ToDictionary 异常,而不是静默扩行。
var query = from eod in DbContext.Set<T>().Where(predicate)
join bod in DbContext.BodTradePosition.Where(n => n.ValueDate == bodDate)
on new { eod.BookId, eod.TradeType, eod.PositionType, eod.UnderlyingCode, ExchangeOptionCode = eod.ExchangeOptionCode ?? string.Empty }
equals new { bod.BookId, bod.TradeType, bod.PositionType, bod.UnderlyingCode, ExchangeOptionCode = bod.ExchangeOptionCode ?? string.Empty } into t_bod
from bod in t_bod.DefaultIfEmpty()
join dividend in DbContext.ex_dividend_info.Where(O => O.ExDividendDate == settleDate && O.ValidStatus)
on eod.UnderlyingCode equals dividend.UnderlyingCode into t_dividend
from dividend in t_dividend.DefaultIfEmpty()
select new
{
eod,
@@ -55,24 +56,31 @@ namespace YLErp.Modules.EodModule
bod.Amount,
bod.Cost,
//bod.AveragePrice
},
dividend
}
};
var datas = query.ToArray();
var diviService = new TradeModule.DealModule.DividendService(OptUser);
// 除权查询集中复用 DividendService 的有效记录条件。字典使用不区分大小写的
// UnderlyingCode 匹配,兼容 EOD 与除权表代码大小写差异;如果同日同代码仍有
// 多条有效记录,ToDictionary 会失败,提示迁移/结算前先清理重复数据。
var dividendDict = diviService.GetExDividendQuery(settleDate)
.ToDictionary(O => O.UnderlyingCode, O => O, StringComparer.OrdinalIgnoreCase);
var eodPriceProvider = new EodPriceProvider(settleDate);
return datas.Select(data =>
{
var eod = data.eod;
var bod = data.bod;
if (data.dividend != null)
// 命中除权数据后仍沿用原有股票结算分支:只重算除权后的收盘价和数量,
// 并保留原 Pv 的正负方向。其他 TradeType 当前不进入该分支,避免扩大
// 本次查询重构的业务范围。
if (dividendDict.TryGetValue(eod.UnderlyingCode, out var dividend))
{
if (data.eod.TradeType == "股票")
{
var SettlePrice = eodPriceProvider.GetPrice(data.eod.UnderlyingCode, SettlementTypeEnum.ClosePrice);
SettlePrice = diviService.GetPrice(SettlePrice, data.dividend);
var amount = diviService.GetPositionAmount(data.eod.Amount, data.dividend);
SettlePrice = diviService.GetPrice(SettlePrice, dividend);
var amount = diviService.GetPositionAmount(data.eod.Amount, dividend);
eod.Pv = eod.Pv > 0 ? Math.Abs(amount * SettlePrice) : -Math.Abs(amount * SettlePrice);
}
}
@@ -104,14 +112,14 @@ namespace YLErp.Modules.EodModule
predicate = PredicateBuilder.Create<TPos>(n => n.ValueDate == settleDate).And(predicate);
}
// 带风险数据的重载与上面的持仓重载采用相同策略:除权记录不参与 SQL 左连接,
// 先完成 EOD、BOD、Risk 的行级关联,再在内存中按标的代码查找唯一除权记录,
// 防止除权表重复行复制风险记录。
var query = from eod in DbContext.Set<TPos>().AsNoTracking().Where(predicate)
join bod in DbContext.BodTradePosition.Where(n => n.ValueDate == bodDate)
on new { eod.BookId, eod.TradeType, eod.PositionType, eod.UnderlyingCode, ExchangeOptionCode = eod.ExchangeOptionCode ?? string.Empty }
equals new { bod.BookId, bod.TradeType, bod.PositionType, bod.UnderlyingCode, ExchangeOptionCode = bod.ExchangeOptionCode ?? string.Empty } into t_bod
from bod in t_bod.DefaultIfEmpty()
join dividend in DbContext.ex_dividend_info.AsNoTracking().Where(O => O.ExDividendDate == settleDate && O.ValidStatus)
on eod.UnderlyingCode equals dividend.UnderlyingCode into t_dividend
from dividend in t_dividend.DefaultIfEmpty()
join risk in DbContext.Set<TRisk>().AsNoTracking().Where(n => n.ValueDate == settleDate && n.TradeId > 0) on new { eod.ValueDate, eod.TradeId } equals new { risk.ValueDate, risk.TradeId } into risk_t
from risk in risk_t.DefaultIfEmpty()
select new
@@ -123,24 +131,28 @@ namespace YLErp.Modules.EodModule
bod.Cost,
//bod.AveragePrice
},
dividend,
risk
};
var datas = query.ToArray();
var diviService = new TradeModule.DealModule.DividendService(OptUser);
// 与无风险重载保持同一数据来源、日期条件和大小写无关的代码匹配规则;重复
// 有效记录会在这里显式失败,而不是让一条持仓对应多条风险结果。
var dividendDict = diviService.GetExDividendQuery(settleDate)
.ToDictionary(O => O.UnderlyingCode, O => O, StringComparer.OrdinalIgnoreCase);
var eodPriceProvider = new EodPriceProvider(settleDate);
return datas.Select(data =>
{
var pos = data.eod;
var bod = data.bod;
if (data.dividend != null)
// 风险对象的除权 Pv 重算规则与上一个重载保持一致,仅在股票交易类型下执行。
if (dividendDict.TryGetValue(pos.UnderlyingCode, out var dividend))
{
if (data.eod.TradeType == "股票")
{
var settlePrice = eodPriceProvider.GetPrice(data.eod.UnderlyingCode, SettlementTypeEnum.ClosePrice);
settlePrice = diviService.GetPrice(settlePrice, data.dividend);
var amount = diviService.GetPositionAmount(data.eod.Amount, data.dividend);
settlePrice = diviService.GetPrice(settlePrice, dividend);
var amount = diviService.GetPositionAmount(data.eod.Amount, dividend);
pos.Pv = pos.Pv > 0 ? Math.Abs(amount * settlePrice) : -Math.Abs(amount * settlePrice);
}
}
@@ -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,163 @@ namespace YLErp.Modules.TradeModule.DealModule
return "";
}
private ex_dividend_info FindExDividendByBusinessKey(int underlyingId, DateTime exDividendDate, int excludedId = 0)
{
// 业务唯一键按“标的 + 自然日”定义,而不是按完整 DateTime 定义。
// 因此这里使用 [当天 00:00, 次日 00:00) 查询,兼容历史数据中可能存在的时分秒。
// excludedId 用于编辑已有记录时排除自身,避免把当前记录误判为重复记录。
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;
}
// 保存前统一截断时间部分,确保 Excel/接口传入的同一天不同时间
// 能命中同一个自然日业务键,也与数据库的一行模型保持一致。
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;
// 同一业务键下允许重复的是同一条记录(两个新对象都为 id=0,
// 或两个对象的 id 相同);不同 id 代表不同存量记录,不能静默合并。
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;
// id>0 表示前端正在编辑指定的存量记录;id=0 时先按自然日业务键
// 查找数据库旧记录,使“新增导入”也能与已有记录合并,而不是重复插入。
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,28 +1022,37 @@ 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;
dividend.GiveCashAmount = item.GiveCashAmount;
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;
// 数据库已有记录也必须走与批次内重复行相同的合并规则:导入字段非零
// 才覆盖旧值,导入字段为零则保留数据库存量值,避免一次不完整导入
// 把旧的派息/送股/配股信息误清零。
MergeNonZeroDividendValues(dividend, item);
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 +1095,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 }
@@ -98,6 +98,10 @@ namespace YLErp.Web.Controllers
else
{
r.ValidStatus = false;
r.DataSource = ExDividendDataSources.Manual;
r.OptId = CurUser.UserId;
r.OptName = CurUser.UserName;
r.OptDate = DateTime.Now;
yldb.SaveChanges();
return JsonSuccess("删除成功");
}
@@ -28,10 +28,13 @@ function saveInfo(dataId, rowId) {
g_grid.jqGrid('saveRow', rowId,
{
successfunc: function (response) {
var msg = response.responseJSON.msg;
var result = response.responseJSON || {};
var msg = result.msg || "保存失败";
main.message(msg);
$("#systemTip").text(new Date().toLocaleString() + " " + msg);
if (!result.success) return false;
g_grid.trigger('reloadGrid');
return true;
},
"url": "/ex_dividend_info/SaveDividend",
"extraparam": data,
@@ -150,4 +153,4 @@ $(function () {
onPaging: onJqgridPaging
};
g_grid = jQuery('#listGrid').jqGrid(obj);
});
});
@@ -164,7 +164,7 @@ $(function () {
//设置除权
function SetDividEnd(cellValue, options, rowObject) {
if (rowObject.UnderlyingInstrumentType !== "Stock" || rowObject.CommodityCode === "篮子标的" || !g_dividend) return "";
if (["Stock", "Fund"].indexOf(rowObject.UnderlyingInstrumentType) < 0 || rowObject.CommodityCode === "篮子标的" || !g_dividend) return "";
var imageHtml = "<input type=\"button\" value=\"除权\" class=\"wentiEdit btn-info\" onclick=\"openDividEnd('" + rowObject.EncryptId + "');\" />";
return imageHtml;
}