Files
zszq-trs/YLErpDAL/Modules/SwapModule/Margin/SwapSpanMarginCalc.cs
T

176 lines
9.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using YLErp.DBModels;
using YLErp.DBModels.Enums;
namespace YLErp.Modules.SwapModule.Margin
{
/// <summary>
/// R2 区间追保结构(规则15)维持保证金纯函数计算(实现方案阶段三 §3.1)。
/// 数据来源约定(与阶段一 SpanConfig 结构、docx 确认书模板一致):
/// - 多头(客户看多):第1层 [Lower, +∞)(上不封顶),第n层 [Lower, Upper);各层 Lower 逐层严格递减;
/// - 空头(客户看空):第1层 (−∞, Upper](下不设限),第n层 (Lower, Upper];各层 Upper 逐层严格递增;
/// - 区间边界为"×期初净价的百分比"小数(0.95=95%),价格口径:债券用净价,指数/ETF 用收盘价;
/// - AmountRate 为累计到该层的追保金额比例(与确认书追保表每行"合计追保金额"同口径,直取不求和),
/// 追保金额 = AmountRate × 期初全价 × 券面总额(债券)/ × 期初价格 × 名义份额(指数/ETF);
/// - 维持保证金 = (初始保证金 + 总追加保证金) × 方向(我方净收取 +1 / 净支付 −1);
/// - 价格跌破最深一层(平仓线外)按最深层计(追保金额不再上升)。
/// 公式参数化(EQD-6948 待业务校验),所有口径集中在本类便于校验后调整。
/// </summary>
public static class SwapSpanMarginCalc
{
/// <summary>
/// 判断 SpanConfig 是否已按方案B录入新区间结构(任一方向有边界/比例或预警/平仓线)。
/// 全空视为存量 x/y 配置,引擎回落旧 名义×y 公式。
/// </summary>
public static bool HasSpanConfig(SpanConfig cfg)
{
if (cfg == null)
{
return false;
}
return cfg.WarnLine.HasValue || cfg.CloseLine.HasValue
|| HasTierValue(cfg.LongSpans) || HasTierValue(cfg.ShortSpans);
}
private static bool HasTierValue(List<SpanTierConfig> tiers)
{
return tiers != null && tiers.Any(t => t != null && (t.Lower.HasValue || t.Upper.HasValue || t.AmountRate.HasValue));
}
/// <summary>
/// 客户方向判定(与确认书 IsCustomerLong 同规则):
/// 我方方向 = 收取端(PosiDirection=收取)与 PositionType 同向、支付端反向;客户方向取反。
/// </summary>
public static bool IsCustomerLong(int posiDirection, int positionType)
{
var isOurLong = posiDirection == (int)SwapDirectionEnum.支付
? positionType == (int)PositionTypeFlag.Short
: positionType == (int)PositionTypeFlag.Long;
return !isOurLong;
}
/// <summary>
/// 按当前价格相对期初价的比例落档。返回命中的层;价格在所有层区间之外(平仓线外)返回最深一层,无可用层返回 null。
/// 多头:priceRatio ∈ [Lower, Upper)(第1层无上界);空头:priceRatio ∈ (Lower, Upper](第1层无下界)。
/// </summary>
public static SpanTierConfig MatchTier(List<SpanTierConfig> tiers, bool isCustomerLong, double priceRatio)
{
if (tiers == null)
{
return null;
}
var valid = tiers.Where(t => t != null && (t.Lower.HasValue || t.Upper.HasValue)).ToList();
if (valid.Count == 0)
{
return null;
}
foreach (var tier in valid)
{
var inLower = !tier.Lower.HasValue || (isCustomerLong ? priceRatio >= tier.Lower.Value : priceRatio > tier.Lower.Value);
var inUpper = !tier.Upper.HasValue || (isCustomerLong ? priceRatio < tier.Upper.Value : priceRatio <= tier.Upper.Value);
if (inLower && inUpper)
{
return tier;
}
}
//未落任何层分两种情形:
//① 价格穿出最深一层边界(多头低于最深层下界/空头高于最深层上界)→ 按最深层计(追保金额不再上升);
// 最深层按边界值取(多头=最小下界、空头=最大上界),不依赖配置数组顺序(BUG-25 引擎侧防御)。
//② 层间空隙(如空头 (0.99,1.00]——价格在期初附近小幅波动、未触发追保的区间)→ 返回 null,追加保证金按 0。
// 此前兜底不分情形一律按最深层计,空隙价格被错误收取最深档追保
// 2026-08-27 交易2538实证:08-24净价100→ratio 0.99999 落空头(0.99,1.00]空档,被按0.04最深档收80.8万)。
var deepest = isCustomerLong
? valid.OrderBy(t => t.Lower ?? double.MinValue).First()
: valid.OrderByDescending(t => t.Upper ?? double.MaxValue).First();
if (isCustomerLong)
{
if (priceRatio < (deepest.Lower ?? double.MinValue))
{
return deepest;
}
}
else if (priceRatio > (deepest.Upper ?? double.MaxValue))
{
return deepest;
}
return null;
}
/// <summary>
/// 总追加保证金 = 命中层 AmountRate × 期初全价 × 券面总额(债券)/ × 期初价格 × 名义份额(指数/ETF)。
/// 数据上两者同为 期初价(PosiGrossPrice) × 数量(PosiQuantity),口径差异由调用方注释说明。
/// </summary>
public static double CalcAdditionalMargin(SpanTierConfig tier, double initPrice, double quantity)
{
if (tier == null || !tier.AmountRate.HasValue)
{
return 0;
}
return tier.AmountRate.Value * initPrice * quantity;
}
/// <summary>
/// 维持保证金 = (初始保证金 + 总追加保证金) × 方向(我方净收取 +1 / 净支付 −1)。
/// </summary>
public static double CalcMaintenanceMargin(double initialMargin, double additionalMargin, double direction)
{
return (initialMargin + additionalMargin) * direction;
}
/// <summary>
/// 阶段三 §3.1 单笔交易引擎计算(纯函数,收盘价由调用方解析后传入——债券取中债估值净价、指数/ETF取收盘价)。
/// closePrice&lt;=0 视为未取到收盘价:追加保证金按 0、维持保证金=初始保证金(不抛错,由调用方记日志)。
/// isInitialCalc=true(试算初始):只出初始项,追加保证金为收盘后口径不参与。
/// 返回 null 表示缺有效标的腿(期初价),调用方跳过该交易不产出 trade_span。
/// </summary>
public static double? CalcTradeMaintenanceMargin(double? tradeInitialMargin, SpanConfig spanCfg,
List<swap_position> legs, bool isInitialCalc, double closePrice)
{
//标的腿(多空):期初价格、数量、客户方向
var underlyingLeg = legs?.FirstOrDefault(x => x.PositionType == (int)PositionTypeFlag.Long || x.PositionType == (int)PositionTypeFlag.Short);
if (underlyingLeg == null || underlyingLeg.PosiGrossPrice <= 0)
{
return null;
}
var isCustomerLong = IsCustomerLong(underlyingLeg.PosiDirection, underlyingLeg.PositionType);
//初始保证金与方向:初始预付金腿(InterestMode=5)收付净额(多腿按净收取定方向);无腿时回落交易录入值(客户应付常态)
var initMarginLegs = legs.Where(x => x.InterestMode == (int)InterestModeEnum.初始预付金).ToList();
double initialMargin;
double direction;
if (initMarginLegs.Any())
{
var netReceive = initMarginLegs.Sum(x => x.InterestDirection == (int)SwapDirectionEnum.收取 ? x.InterestPrincipalFix : -x.InterestPrincipalFix);
initialMargin = Math.Abs((double)netReceive);
direction = netReceive >= 0 ? 1 : -1;
}
else
{
initialMargin = tradeInitialMargin ?? 0;
direction = 1;
}
if (isInitialCalc)
{
return CalcMaintenanceMargin(initialMargin, 0, direction);
}
var additional = 0.0;
//期初价比基:债券/指数/ETF统一为期初净价(PosiNetNoFeePrice,确认书"参考标的期初净价"同源),缺省回落期初全价
var initNetPrice = (double)(underlyingLeg.PosiNetNoFeePrice ?? 0m);
if (initNetPrice <= 0)
{
initNetPrice = (double)underlyingLeg.PosiGrossPrice;
}
if (closePrice > 0 && initNetPrice > 0)
{
var tiers = isCustomerLong ? spanCfg.LongSpans : spanCfg.ShortSpans;
var tier = MatchTier(tiers, isCustomerLong, closePrice / initNetPrice);
//追保金额基数:期初全价×券面总额(债券)/ 期初价格×名义份额(指数/ETF),同为 期初价×数量
additional = CalcAdditionalMargin(tier, (double)underlyingLeg.PosiGrossPrice, (double)underlyingLeg.PosiQuantity);
}
return CalcMaintenanceMargin(initialMargin, additional, direction);
}
}
}