176 lines
9.4 KiB
C#
176 lines
9.4 KiB
C#
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<=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);
|
||
}
|
||
}
|
||
}
|