Merge branch 'glms/feature/0812_zmr_divPower' into glms/feature/1.4.2
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user