#EQD-6948 国联民生-实现保证金规则(2)追保金额的产生与收盘计算 收盘计算和授信占用
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Enums;
|
||||
using YLErp.Modules.MarginModule;
|
||||
|
||||
namespace YLErp.Modules.CalcModules
|
||||
{
|
||||
/// <summary>
|
||||
/// R1 模板三层级找到即停回退集成测试(连 dev 库,MarginTemplateV2RateHelper.ResolveTieredTemplate/GetTradeMarginRate):
|
||||
/// 交易绑定(自定义)→ 客户默认(client_margin_template 按客户)→ 全局默认(IsDefault&&!IsForClient)。
|
||||
/// 测试数据全部带 "ZZZ-R1回退测试-" 名称前缀,TestInitialize/TestCleanup 双向清理,不触碰真实交易。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MarginTemplateV2RateHelperFallbackTest
|
||||
{
|
||||
private const string Marker = "ZZZ-R1回退测试-";
|
||||
private const int SentinelTradeId = 1900000001;
|
||||
private const int SentinelTradeId2 = 1900000002;
|
||||
private DateTime EffectiveDate = new DateTime(2000, 1, 1);
|
||||
|
||||
private YLContext db;
|
||||
private int clientId;
|
||||
|
||||
[TestInitialize]
|
||||
public void Init()
|
||||
{
|
||||
db = new YLContext();
|
||||
Cleanup();
|
||||
//取一个真实客户做客户级绑定(只写 client_margin_template,不动客户数据)
|
||||
using (var clientDb = DbContextFactory.GetClientDbContext(OptUserInfo.SystemUser))
|
||||
{
|
||||
clientId = clientDb.client.Where(c => c.id > 0).OrderBy(c => c.id).Select(c => c.id).First();
|
||||
}
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void CleanupFixture()
|
||||
{
|
||||
Cleanup();
|
||||
db.Dispose();
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
var templateIds = db.margin_template_v2.Where(x => x.Name.StartsWith(Marker)).Select(x => x.id).ToList();
|
||||
if (templateIds.Count > 0)
|
||||
{
|
||||
db.margin_template_detail.RemoveRange(db.margin_template_detail.Where(x => templateIds.Contains(x.MarginTemplateId)));
|
||||
db.trade_margin_template.RemoveRange(db.trade_margin_template.Where(x => templateIds.Contains(x.MarginTemplateId)));
|
||||
db.client_margin_template.RemoveRange(db.client_margin_template.Where(x => templateIds.Contains(x.MarginTemplateId)));
|
||||
db.margin_template_v2.RemoveRange(db.margin_template_v2.Where(x => templateIds.Contains(x.id)));
|
||||
db.SaveChanges();
|
||||
}
|
||||
db.trade_margin_template.RemoveRange(db.trade_margin_template.Where(x => x.TradeId == SentinelTradeId || x.TradeId == SentinelTradeId2));
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private margin_template_v2 AddTemplate(string name, bool isDefault, bool isForClient, string tradeTypes = "收益互换", bool isValid = true, int ruleType = (int)MarginRuleTypeEnum.区间追保结构)
|
||||
{
|
||||
var t = new margin_template_v2
|
||||
{
|
||||
Name = name,
|
||||
IsDefault = isDefault,
|
||||
IsForClient = isForClient,
|
||||
IsValid = isValid,
|
||||
TradeTypes = tradeTypes,
|
||||
RuleType = ruleType,
|
||||
ValueDate = EffectiveDate
|
||||
};
|
||||
db.margin_template_v2.Add(t);
|
||||
db.SaveChanges();
|
||||
return t;
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_001_交易绑定优先_压过客户与全局()
|
||||
{
|
||||
var custom = AddTemplate(Marker + "自定义", isDefault: false, isForClient: false);
|
||||
var clientTpl = AddTemplate(Marker + "客户", isDefault: true, isForClient: true);
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.trade_margin_template.Add(new trade_margin_template { TradeId = SentinelTradeId, MarginTemplateId = custom.id, ValueDate = EffectiveDate, IsLatest = true });
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = clientTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(SentinelTradeId, clientId, DateTime.Today, db);
|
||||
Assert.IsNotNull(resolved);
|
||||
Assert.AreEqual(custom.id, resolved.id, "交易绑定(自定义)应找到即停,压过客户与全局默认");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_002_无交易绑定_落到客户默认()
|
||||
{
|
||||
var clientTpl = AddTemplate(Marker + "客户", isDefault: true, isForClient: true);
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = clientTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
Assert.IsNotNull(resolved);
|
||||
Assert.AreEqual(clientTpl.id, resolved.id, "无交易绑定时应命中客户默认,压过全局默认");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_003_无交易无客户绑定_落到全局默认()
|
||||
{
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
AddTemplate(Marker + "客户", isDefault: true, isForClient: true); //未绑定不应被取到
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
AssertGlobal(resolved, globalTpl);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_004_客户绑定指向非互换模板_跳过落全局()
|
||||
{
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
var optionTpl = AddTemplate(Marker + "期权客户模板", isDefault: true, isForClient: true, tradeTypes: "香草期权");
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = optionTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
AssertGlobal(resolved, globalTpl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同日并存"互换绑定+期权绑定"(页面互斥只挡适用结构重叠,此组合允许保存):
|
||||
/// 应命中互换绑定,不因先取到期权绑定被过滤而误穿透到全局。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TF_007_同日并存互换与期权绑定_命中间换绑定()
|
||||
{
|
||||
var swapTpl = AddTemplate(Marker + "客户", isDefault: true, isForClient: true);
|
||||
var optionTpl = AddTemplate(Marker + "期权客户模板", isDefault: true, isForClient: true, tradeTypes: "香草期权");
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = swapTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.client_margin_template.Add(new client_margin_template { ClientId = clientId, MarginTemplateId = optionTpl.id, ValueDate = EffectiveDate, ClientLevel = "" });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(tradeId: null, clientId, DateTime.Today, db);
|
||||
Assert.IsNotNull(resolved);
|
||||
Assert.AreEqual(swapTpl.id, resolved.id, "同日期权+互换绑定并存时应命中互换绑定(过滤后再取最新),而非穿透全局");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TF_005_绑定模板已失效_找到即停返回null()
|
||||
{
|
||||
var invalidTpl = AddTemplate(Marker + "已失效", isDefault: false, isForClient: false, isValid: false);
|
||||
var globalTpl = AddTemplate(Marker + "全局", isDefault: true, isForClient: false);
|
||||
db.trade_margin_template.Add(new trade_margin_template { TradeId = SentinelTradeId2, MarginTemplateId = invalidTpl.id, ValueDate = EffectiveDate, IsLatest = true });
|
||||
db.SaveChanges();
|
||||
|
||||
var resolved = MarginTemplateV2RateHelper.ResolveTieredTemplate(SentinelTradeId2, clientId, DateTime.Today, db);
|
||||
Assert.IsNull(resolved, "交易绑定指向已失效模板时应找到即停(不向下回退到全局默认)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 端到端:交易绑定(区间追保结构 + 明细 x/y)经 GetTradeMarginRate 完整取到率——
|
||||
/// 兼容回归交易级取数路径在新回退结构下行为不变。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TF_006_交易绑定端到端取率()
|
||||
{
|
||||
var custom = AddTemplate(Marker + "自定义", isDefault: false, isForClient: false);
|
||||
db.margin_template_detail.Add(new margin_template_detail
|
||||
{
|
||||
MarginTemplateId = custom.id,
|
||||
ValueDate = EffectiveDate,
|
||||
UnderlyingType = UnderlyingTypeEnum.None,
|
||||
MarginRatio1 = 0.05,
|
||||
MarginRatio2 = 0.03
|
||||
});
|
||||
db.trade_margin_template.Add(new trade_margin_template { TradeId = SentinelTradeId, MarginTemplateId = custom.id, ValueDate = EffectiveDate, IsLatest = true });
|
||||
db.SaveChanges();
|
||||
|
||||
var rate = MarginTemplateV2RateHelper.GetTradeMarginRate(SentinelTradeId, "240004.IB", "TBonds", DateTime.Today, db);
|
||||
Assert.IsNotNull(rate);
|
||||
Assert.AreEqual(custom.id, rate.Template.id);
|
||||
Assert.AreEqual(0.05m, rate.InitRate.Value);
|
||||
Assert.AreEqual(0.03m, rate.MaintainRate.Value);
|
||||
}
|
||||
|
||||
private void AssertGlobal(margin_template_v2 resolved, margin_template_v2 expected)
|
||||
{
|
||||
Assert.IsNotNull(resolved);
|
||||
if (resolved.Name.StartsWith(Marker))
|
||||
{
|
||||
Assert.AreEqual(expected.id, resolved.id, "应命中本用例创建的全局默认模板");
|
||||
}
|
||||
//dev 库存在其他真实全局默认模板时,按 ValueDate 最新者胜出,不做更严格断言
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using YLErp.Modules.MarginModule;
|
||||
|
||||
namespace YLErp.Modules.CalcModules
|
||||
{
|
||||
/// <summary>
|
||||
/// 实现方案阶段一 §1.6 测试要点:GetTradeMarginRate 分类判定钩子默认行为回归。
|
||||
/// 钩子本期默认返回 null → 走"全部/通配行"兜底,取数行为与现状一致(阶段二标签功能不改变取数链路)。
|
||||
/// 完整 GetTradeMarginRate 依赖 YLContext(MySQL),无法纯内存测试,此处覆盖钩子契约。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class MarginTemplateV2RateHelperTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void MT_001_分类判定钩子_默认返回null_走通配兜底()
|
||||
{
|
||||
Assert.IsNull(MarginTemplateV2RateHelper.GetUnderlyingCategory("019546.XSHG", null));
|
||||
Assert.IsNull(MarginTemplateV2RateHelper.GetUnderlyingCategory("200002.XSHE", "Fund"));
|
||||
Assert.IsNull(MarginTemplateV2RateHelper.GetUnderlyingCategory(null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// BUG-01 方向守护(2026-08-24):授信出入表金额口径 占用记正/释放记负——
|
||||
/// 已使用授信 = SUM(amount) 随占用上升、随释放回落;可用授信 = 有效授信 − 已使用授信 随占用收缩。
|
||||
/// 连 dev 库闭环验证 Occupy/Release/GetUsedCredit(哨兵 client_id 自建自清,不触碰真实客户)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ClientCreditInoutSignTest
|
||||
{
|
||||
private const int SentinelClientId = 99064001; //哨兵客户号(不存在的客户,仅授信出入表按 client_id 记账)
|
||||
private YLContext db;
|
||||
private ClientCreditInoutService service;
|
||||
|
||||
[TestInitialize]
|
||||
public void Init()
|
||||
{
|
||||
db = new YLContext();
|
||||
Cleanup();
|
||||
service = new ClientCreditInoutService(OptUserInfo.SystemUser);
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
public void Teardown()
|
||||
{
|
||||
Cleanup();
|
||||
db.Dispose();
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
var olds = db.client_credit_inout.Where(x => x.client_id == SentinelClientId).ToList();
|
||||
if (olds.Count > 0)
|
||||
{
|
||||
db.client_credit_inout.RemoveRange(olds);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>占用记正数:占用越多已使用授信越大(修复前为负——"越占越多"BUG-01)</summary>
|
||||
[TestMethod]
|
||||
public void CR_001_占用记正_已使用授信上升()
|
||||
{
|
||||
service.Occupy(SentinelClientId, 900001L, 800001, 10000, DateTime.Today, "方向守护-占用1");
|
||||
Assert.AreEqual(10000, ClientCreditInoutService.GetUsedCredit(SentinelClientId, db), 1e-6);
|
||||
|
||||
service.Occupy(SentinelClientId, 900002L, 800002, 5000, DateTime.Today, "方向守护-占用2");
|
||||
Assert.AreEqual(15000, ClientCreditInoutService.GetUsedCredit(SentinelClientId, db), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>释放记负数:已使用授信回落,快照 used_after 同口径</summary>
|
||||
[TestMethod]
|
||||
public void CR_002_释放记负_已使用授信回落()
|
||||
{
|
||||
service.Occupy(SentinelClientId, 900001L, 800001, 10000, DateTime.Today, "方向守护-占用");
|
||||
service.Release(SentinelClientId, 900001L, 800001, 4000, DateTime.Today, "方向守护-释放");
|
||||
|
||||
var used = ClientCreditInoutService.GetUsedCredit(SentinelClientId, db);
|
||||
Assert.AreEqual(6000, used, 1e-6);
|
||||
//快照列与求和口径一致(最后一次写完后的已使用授信)
|
||||
var last = db.client_credit_inout.Where(x => x.client_id == SentinelClientId).OrderByDescending(x => x.id).First();
|
||||
Assert.AreEqual(6000, last.used_after, 1e-6);
|
||||
Assert.AreEqual(-4000, last.amount, 1e-6, "释放行 amount 记负数");
|
||||
}
|
||||
|
||||
/// <summary>方向传导:可用授信 = 有效授信 − 已使用授信,占用使其收缩(BUG-01 修复后的业务语义)</summary>
|
||||
[TestMethod]
|
||||
public void CR_003_可用授信随占用收缩()
|
||||
{
|
||||
service.Occupy(SentinelClientId, null, 800003, 10000, DateTime.Today, "方向守护-占用");
|
||||
var used = ClientCreditInoutService.GetUsedCredit(SentinelClientId, db);
|
||||
var effective = 30000d; //有效授信给定量
|
||||
Assert.AreEqual(20000, effective - used, 1e-6, "占用 10000 后可用授信应从 30000 收缩到 20000");
|
||||
}
|
||||
|
||||
/// <summary>Occupy/Release 入口对传入符号容错(一律按数量取绝对值定方向),防调用方残留旧负号</summary>
|
||||
[TestMethod]
|
||||
public void CR_004_语义入口符号容错()
|
||||
{
|
||||
//即使调用方按旧习惯传负数,Occupy 也记正(数量口径),防止新代码库中残留旧符号调用点
|
||||
var rec = service.Occupy(SentinelClientId, null, 800004, -7000, DateTime.Today, "方向守护-负号容错");
|
||||
Assert.AreEqual(7000, rec.amount, 1e-6);
|
||||
var rel = service.Release(SentinelClientId, null, 800004, -3000, DateTime.Today, "方向守护-负号容错");
|
||||
Assert.AreEqual(-3000, rel.amount, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using YLErp.BLL;
|
||||
using YLErp.DBModels;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 新建表列名规范测试(2026-08-20 约定:新建表所有数据库列一律小写下划线)。
|
||||
/// 只构建 EF 模型(纯内存,不连库),断言映射列名不含大写字母:
|
||||
/// client_credit_inout 全表、swap_position.fund_tag、credit.original_credit/max_credit_use_ratio。
|
||||
/// 基类操作人列(OptId/OptName/OptTime)经 override+[Column] 覆写为小写,此处一并守护。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class DbColumnNamingTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void DB_001_授信出入表_全部列名小写()
|
||||
{
|
||||
using var db = new YLContext();
|
||||
var entity = db.Model.FindEntityType(typeof(client_credit_inout));
|
||||
Assert.IsNotNull(entity, "client_credit_inout 未注册到 YLContext");
|
||||
var table = StoreObjectIdentifier.Table("client_credit_inout", null);
|
||||
foreach (var property in entity.GetProperties())
|
||||
{
|
||||
var column = property.GetColumnName(table);
|
||||
Assert.IsNotNull(column, $"属性 {property.Name} 未映射列名");
|
||||
Assert.IsFalse(column.Any(char.IsUpper), $"列名应全小写: {column}(属性 {property.Name})");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DB_002_互换持仓与授信表_新增列名小写()
|
||||
{
|
||||
using var db = new YLContext();
|
||||
AssertColumn(db, typeof(swap_position), nameof(swap_position.FundTag), "fund_tag");
|
||||
AssertColumn(db, typeof(CreditTable), nameof(CreditTable.OriginalCredit), "original_credit");
|
||||
AssertColumn(db, typeof(CreditTable), nameof(CreditTable.MaxCreditUseRatio), "max_credit_use_ratio");
|
||||
}
|
||||
|
||||
private static void AssertColumn(YLContext db, Type entityType, string propertyName, string expectedColumn)
|
||||
{
|
||||
var entity = db.Model.FindEntityType(entityType);
|
||||
Assert.IsNotNull(entity, $"{entityType.Name} 未注册到 YLContext");
|
||||
var property = entity.FindProperty(propertyName);
|
||||
Assert.IsNotNull(property, $"属性 {propertyName} 不在 {entityType.Name} 映射中");
|
||||
var table = StoreObjectIdentifier.Create(entity, StoreObjectType.Table).Value;
|
||||
var column = property.GetColumnName(table);
|
||||
Assert.AreEqual(expectedColumn, column, $"{entityType.Name}.{propertyName} 映射列名不符");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R4 授信/现金标签 分配与返还分流纯函数测试(实现方案阶段二 §2.3/§2.4 测试要点)。
|
||||
/// 资金来源是预付金腿上的录入项(逐腿选择),覆盖:标签赋值四种情形、拆单金额核对、
|
||||
/// 平仓按标签返还、存量无标签按现金。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class FundTagCalcTest
|
||||
{
|
||||
private static LegAmount Leg(long id, double amount, bool preferCredit)
|
||||
=> new() { Leg = new swap_position { id = id }, Amount = amount, PreferCredit = preferCredit };
|
||||
|
||||
// ================================================================
|
||||
// §2.3 标签赋值四种情形(FundTagCalc.AllocateByLegPreference,逐腿)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_001_腿未选资金来源_全额现金()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: false) }, 5000, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_002_腿选现金_全额现金_不消耗额度()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: false) }, 0, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_003_腿选授信_额度充足_全额授信()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 5000, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(1000, plans[0].CreditAmount);
|
||||
Assert.AreEqual(0, plans[0].CashAmount);
|
||||
Assert.IsFalse(plans[0].NeedSplit);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_004_腿选授信_额度不足_拆分为剩余授信加现金差额()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 300, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(300, plans[0].CreditAmount);
|
||||
Assert.AreEqual(700, plans[0].CashAmount);
|
||||
Assert.IsTrue(plans[0].NeedSplit);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_005_腿选授信_额度为零_全额现金()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 0, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_006_特批_忽略腿上选择_全额现金不占授信()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, 1000, preferCredit: true) }, 5000, ignoreMoneyCheck: true);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(1000, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_007_客户净收取的腿_即使选授信也不占用授信()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount> { Leg(101, -500, preferCredit: true) }, 5000, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(-500, plans[0].CashAmount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_008_混合偏好_现金腿不消耗授信额度()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount>
|
||||
{
|
||||
Leg(101, 600, preferCredit: false), //现金腿
|
||||
Leg(102, 400, preferCredit: true), //授信腿
|
||||
}, 500, ignoreMoneyCheck: false);
|
||||
Assert.AreEqual(0, plans[0].CreditAmount);
|
||||
Assert.AreEqual(600, plans[0].CashAmount);
|
||||
//授信腿可用额度仍是 500(现金腿未消耗)
|
||||
Assert.AreEqual(400, plans[1].CreditAmount);
|
||||
Assert.AreEqual(0, plans[1].CashAmount);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §2.3 拆单:多腿授信额度按顺序消耗(FundTagCalc.AllocateByLegPreference)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_010_多条授信腿_额度跨在第二腿_第二腿拆单()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount>
|
||||
{
|
||||
Leg(101, 600, preferCredit: true),
|
||||
Leg(102, 600, preferCredit: true),
|
||||
}, creditAvailable: 1000, ignoreMoneyCheck: false);
|
||||
|
||||
Assert.AreEqual(2, plans.Count);
|
||||
//第一腿全额授信
|
||||
Assert.AreEqual(600, plans[0].CreditAmount);
|
||||
Assert.AreEqual(0, plans[0].CashAmount);
|
||||
Assert.IsFalse(plans[0].NeedSplit);
|
||||
//第二腿跨界拆单:授信400 + 现金200
|
||||
Assert.AreEqual(400, plans[1].CreditAmount);
|
||||
Assert.AreEqual(200, plans[1].CashAmount);
|
||||
Assert.IsTrue(plans[1].NeedSplit);
|
||||
//金额守恒:授信合计=可用额度,现金合计=差额
|
||||
Assert.AreEqual(1000, plans.Sum(p => p.CreditAmount));
|
||||
Assert.AreEqual(200, plans.Sum(p => p.CashAmount));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_011_额度耗尽后_后续授信腿全额现金()
|
||||
{
|
||||
var plans = FundTagCalc.AllocateByLegPreference(new List<LegAmount>
|
||||
{
|
||||
Leg(101, 600, preferCredit: true),
|
||||
Leg(102, 600, preferCredit: true),
|
||||
Leg(103, 600, preferCredit: true),
|
||||
}, creditAvailable: 600, ignoreMoneyCheck: false);
|
||||
|
||||
Assert.AreEqual(600, plans[0].CreditAmount);
|
||||
Assert.AreEqual(0, plans[1].CreditAmount);
|
||||
Assert.AreEqual(600, plans[1].CashAmount);
|
||||
Assert.AreEqual(0, plans[2].CreditAmount);
|
||||
Assert.AreEqual(600, plans[2].CashAmount);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// §2.4 平仓/到期按原标签返还(FundTagCalc.SplitUnwindByTag)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_020_平仓返还_授信腿不产生现金_按腿生成释放明细()
|
||||
{
|
||||
var settlements = new List<MarginLegSettlement>
|
||||
{
|
||||
//授信腿:返还本金 400,返息 50 → 全部不进资金,写释放
|
||||
new() { PositionId = 101, Tag = ConsFundTag.Credit, MarginAmount = 400m, RebateAmount = 50m },
|
||||
//现金腿:返还本金 300,返息 20 → 正常资金流水
|
||||
new() { PositionId = 102, Tag = ConsFundTag.Cash, MarginAmount = 300m, RebateAmount = 20m },
|
||||
//存量无标签(EffectiveTag 后按现金)
|
||||
new() { PositionId = 103, Tag = ConsFundTag.Cash, MarginAmount = 100m, RebateAmount = 0m },
|
||||
};
|
||||
var split = FundTagCalc.SplitUnwindByTag(settlements);
|
||||
|
||||
Assert.AreEqual(300 + 100, split.CashMargin);
|
||||
Assert.AreEqual(400, split.CreditMargin);
|
||||
Assert.AreEqual(20, split.CashRebate);
|
||||
Assert.AreEqual(50, split.CreditRebate);
|
||||
//释放明细按 position_id 匹配原占用记录
|
||||
Assert.AreEqual(1, split.Releases.Count);
|
||||
Assert.AreEqual(101L, split.Releases[0].PositionId);
|
||||
Assert.AreEqual(400, split.Releases[0].Amount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FT_021_平仓返还_全部授信_现金部分为零()
|
||||
{
|
||||
var settlements = new List<MarginLegSettlement>
|
||||
{
|
||||
new() { PositionId = 201, Tag = ConsFundTag.Credit, MarginAmount = 800m, RebateAmount = 30m },
|
||||
};
|
||||
var split = FundTagCalc.SplitUnwindByTag(settlements);
|
||||
Assert.AreEqual(0, split.CashMargin);
|
||||
Assert.AreEqual(0, split.CashRebate);
|
||||
Assert.AreEqual(800, split.CreditMargin);
|
||||
Assert.AreEqual(1, split.Releases.Count);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 平仓利息事件 → 逐腿结算额构造(MarginSettlementBuilder)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void FT_030_由利息事件构造结算额_方向比与标签过滤正确()
|
||||
{
|
||||
var events = new List<swap_flow_event>
|
||||
{
|
||||
//预付金腿(InterestMode=5,无标的代码):InterestDirection=2 → 返还本金 = principal × 1
|
||||
new() { PositionId = 101, InterestMode = (int)InterestModeEnum.初始预付金, InterestDirection = 2, InterestPrincipal = 400m, InterestClosePnL = 50m },
|
||||
//预付金腿:InterestDirection=1(收取)→ 返还本金 = principal × -1
|
||||
new() { PositionId = 102, InterestMode = (int)InterestModeEnum.初始预付金, InterestDirection = 1, InterestPrincipal = 300m, InterestClosePnL = 0m },
|
||||
//浮动腿(有标的代码):不参与
|
||||
new() { PositionId = 103, InterestMode = 0, UnderlyingCode = "123456.SH", InterestPrincipal = 999m },
|
||||
//利息腿(非预付金、无标的):不参与
|
||||
new() { PositionId = 104, InterestMode = (int)InterestModeEnum.固定值, InterestPrincipal = 555m },
|
||||
};
|
||||
var tags = new Dictionary<long, string> { { 101, ConsFundTag.Credit } };
|
||||
var settlements = MarginSettlementBuilder.Build(tags, events);
|
||||
|
||||
Assert.AreEqual(2, settlements.Count);
|
||||
Assert.AreEqual(ConsFundTag.Credit, settlements[0].Tag);
|
||||
Assert.AreEqual(400m, settlements[0].MarginAmount);
|
||||
Assert.AreEqual(50m, settlements[0].RebateAmount);
|
||||
//102 标签字典缺失(存量无标签)→ 按现金;方向比 -1
|
||||
Assert.AreEqual(ConsFundTag.Cash, settlements[1].Tag);
|
||||
Assert.AreEqual(-300m, settlements[1].MarginAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R3 阶段四 §4.1 合约维度追加保证金计算测试(纯函数):
|
||||
/// 目标 = 维持保证金 − 应付预付金净收额(≤0 不追);当日新增 = 目标 − 已补足(现金记录累计+授信占用累计);
|
||||
/// 授信优先分配;重跑幂等(新增=0 不写);追保回落不返还(负缺口走可用资金公式)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapAdditionalMarginCalcTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void AM_001_目标计算_维持大于应付()
|
||||
{
|
||||
//维持 180 − 应付净收 100 = 80
|
||||
Assert.AreEqual(80, SwapAdditionalMarginCalc.CalcTarget(180, 100), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_002_目标计算_应付覆盖维持_不追()
|
||||
{
|
||||
Assert.AreEqual(0, SwapAdditionalMarginCalc.CalcTarget(80, 100), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_003_目标计算_维持为负_我方净支付方向不追()
|
||||
{
|
||||
Assert.AreEqual(0, SwapAdditionalMarginCalc.CalcTarget(-50, 100), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_004_分配_授信充足_全额授信()
|
||||
{
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(80, 100);
|
||||
Assert.AreEqual(80, credit, 1e-6);
|
||||
Assert.AreEqual(0, cash, 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_005_分配_授信不足_拆为授信加现金()
|
||||
{
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(80, 30);
|
||||
Assert.AreEqual(30, credit, 1e-6);
|
||||
Assert.AreEqual(50, cash, 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_006_分配_无授信_全额现金()
|
||||
{
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(80, 0);
|
||||
Assert.AreEqual(0, credit, 1e-6);
|
||||
Assert.AreEqual(80, cash, 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AM_007_分配_新增为零或负_不产生()
|
||||
{
|
||||
Assert.AreEqual((0d, 0d), SwapAdditionalMarginCalc.Allocate(0, 100));
|
||||
Assert.AreEqual((0d, 0d), SwapAdditionalMarginCalc.Allocate(-5, 100));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 幂等与跨日增量(资金记录存累计值的口径验证):
|
||||
/// 第1日 维持180/应付100 → 新增80;第2日 维持不变、已补足80 → 新增0(重跑不写);
|
||||
/// 第3日 维持升到200 → 目标100 − 已补足80 = 新增20(记录累计值更新为100,历史增量自然承载)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_008_跨日增量与幂等()
|
||||
{
|
||||
double fundedCash = 0, fundedCredit = 0;
|
||||
var payableNet = 100d;
|
||||
|
||||
var day1 = Math.Round(SwapAdditionalMarginCalc.CalcTarget(180, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(80, day1, 1e-6);
|
||||
var (c1, m1) = SwapAdditionalMarginCalc.Allocate(day1, 30);
|
||||
fundedCredit += c1;
|
||||
fundedCash += m1;
|
||||
Assert.AreEqual(30, fundedCredit, 1e-6);
|
||||
Assert.AreEqual(50, fundedCash, 1e-6);
|
||||
|
||||
//重跑同日:目标/已补足不变 → 新增 0
|
||||
var day1Rerun = Math.Round(SwapAdditionalMarginCalc.CalcTarget(180, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(0, day1Rerun, 1e-6);
|
||||
|
||||
//第2日 维持不变 → 已补足=目标 → 新增 0
|
||||
var day2 = Math.Round(SwapAdditionalMarginCalc.CalcTarget(180, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(0, day2, 1e-6);
|
||||
|
||||
//第3日 维持上升 → 只补差额
|
||||
var day3 = Math.Round(SwapAdditionalMarginCalc.CalcTarget(200, payableNet) - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||||
Assert.AreEqual(20, day3, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追保回落(维持下降到应付之下)不返还:目标为 0 → 已补足保持,新增 0;
|
||||
/// 超付部分由可用资金公式的负缺口(Σ维持−累计)体现,不产生返还记录。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_009_追保回落不返还()
|
||||
{
|
||||
var target = SwapAdditionalMarginCalc.CalcTarget(50, 100);
|
||||
Assert.AreEqual(0, target, 1e-6);
|
||||
var (credit, cash) = SwapAdditionalMarginCalc.Allocate(target - 80, 100);
|
||||
Assert.AreEqual(0, credit, 1e-6);
|
||||
Assert.AreEqual(0, cash, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 追加保证金授信出入记录的 remark 前缀识别(RemoveByTrade 保留判定与累计口径共用)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void AM_010_追加保证金记录识别()
|
||||
{
|
||||
Assert.IsTrue(ClientCreditInoutService.IsAdditionalMarginRecord(
|
||||
new client_credit_inout { change_type = client_credit_inout.ChangeTypeOccupy, remark = "追加保证金占用" }));
|
||||
Assert.IsFalse(ClientCreditInoutService.IsAdditionalMarginRecord(
|
||||
new client_credit_inout { change_type = client_credit_inout.ChangeTypeOccupy, remark = "簿记授信占用" }));
|
||||
Assert.IsFalse(ClientCreditInoutService.IsAdditionalMarginRecord(
|
||||
new client_credit_inout { remark = null }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.2 可用资金公式测试(需求拆分 R2 口径,实时/EOD/报告三处共用 SwapSpanBalanceCalc):
|
||||
/// 客户维度 = Max(现金结存 + 授信 − 已使用授信 + 初始保证金 − 维持保证金, 0);
|
||||
/// 合约维度 = Max(现金结存 + 授信 − 已使用授信 − 交易维度追加保证金合计, 0)。
|
||||
/// 授信额度为 credit.Credit 合计(保存时已折算比例),现金结存=期末结存(阶段二起授信不进资金)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanBalanceCalcTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void SB_001_客户维度_现金充足()
|
||||
{
|
||||
//现金200 + 授信100 − 已使用20 + 初始100 − 维持150 = 230
|
||||
Assert.AreEqual(230, SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
cashBalance: 200, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 150), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_002_客户维度_现金不足_截断为0()
|
||||
{
|
||||
//现金20 + 0 − 0 + 100 − 130 = −10 → 0(追加10未补足时无可用资金)
|
||||
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 0, usedCredit: 0, initialMargin: 100, maintenanceMargin: 130), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_003_客户维度_授信可用额参与覆盖()
|
||||
{
|
||||
//现金20 + 授信100 − 已使用20 + 100 − 130 = 70
|
||||
Assert.AreEqual(70, SwapSpanBalanceCalc.CalcClientDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_004_合约维度_按交易追加合计扣减()
|
||||
{
|
||||
//现金20 + 授信100 − 已使用20 − 追加合计30 = 70
|
||||
Assert.AreEqual(70, SwapSpanBalanceCalc.CalcContractDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 100, usedCredit: 20, tradeAdditionalMarginSum: 30), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_005_合约维度_扣尽截断为0()
|
||||
{
|
||||
Assert.AreEqual(0, SwapSpanBalanceCalc.CalcContractDimensionAvailable(
|
||||
cashBalance: 20, totalCredit: 0, usedCredit: 0, tradeAdditionalMarginSum: 30), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段三口径一致性:无追加保证金流水时(阶段四前),两维度公式数值一致——
|
||||
/// 交易追加合计 = Σ(维持−累计) = 维持 − 初始,与客户维度的 (初始−维持) 项互相抵消。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_006_阶段三两维度数值一致()
|
||||
{
|
||||
var cash = 20d;
|
||||
var credit = 100d;
|
||||
var used = 20d;
|
||||
var initial = 100d;
|
||||
var maintenance = 130d;
|
||||
var clientDimension = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance);
|
||||
var contractDimension = SwapSpanBalanceCalc.CalcContractDimensionAvailable(cash, credit, used, maintenance - initial);
|
||||
Assert.AreEqual(clientDimension, contractDimension, 1e-9);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段四 §4.2 客户维度双向追保:正数=需追保,不以 0 截断。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_010_客户维度追保金额_正数需追保()
|
||||
{
|
||||
//追保差额 (维持150 − 初始100)=50;资金 现金5+授信0−已使用0=5 → 追保 = 50 − 5 = 45
|
||||
Assert.AreEqual(45, SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
|
||||
cashBalance: 5, totalCredit: 0, usedCredit: 0, initialMargin: 100, maintenanceMargin: 150), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_011_客户维度追保金额_负数可返还()
|
||||
{
|
||||
//差额 (维持130−初始100)=30,资金 20+100−20=100 → 追保 = 30 − 100 = −70(可返还,不截断为 0)
|
||||
Assert.AreEqual(-70, SwapSpanBalanceCalc.CalcClientDimensionCallMargin(
|
||||
cashBalance: 20, totalCredit: 100, usedCredit: 20, initialMargin: 100, maintenanceMargin: 130), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 客户维度追保金额与可用资金公式互为反向(去 Max 截断):追保 = −(未截断可用资金)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_012_客户维度追保与可用资金反向一致()
|
||||
{
|
||||
var cash = 20d;
|
||||
var credit = 100d;
|
||||
var used = 20d;
|
||||
var initial = 100d;
|
||||
var maintenance = 130d;
|
||||
var available = SwapSpanBalanceCalc.CalcClientDimensionAvailable(cash, credit, used, initial, maintenance);
|
||||
var callMargin = SwapSpanBalanceCalc.CalcClientDimensionCallMargin(cash, credit, used, initial, maintenance);
|
||||
var availableUnfloored = cash + credit - used + initial - maintenance;
|
||||
Assert.AreEqual(-availableUnfloored, callMargin, 1e-9);
|
||||
//可用资金被 0 截断时追保为正(需追保),两者不矛盾
|
||||
if (availableUnfloored < 0)
|
||||
{
|
||||
Assert.AreEqual(0, available, 1e-9);
|
||||
Assert.IsTrue(callMargin > 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合约维度追保金额(需求原文 现金+授信−已使用 的应追加方向取值):账户透支为正=应补足,盈余为负。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SB_013_合约维度追保金额_透支为正()
|
||||
{
|
||||
//现金−80 + 授信100 − 已使用30 = −10 → 追保 = 10(应补足)
|
||||
Assert.AreEqual(10, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
cashBalance: -80, totalCredit: 100, usedCredit: 30), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SB_014_合约维度追保金额_盈余为负()
|
||||
{
|
||||
//现金50 + 授信100 − 已使用20 = 130 → 追保 = −130(盈余可返还方向)
|
||||
Assert.AreEqual(-130, SwapSpanBalanceCalc.CalcContractDimensionCallMargin(
|
||||
cashBalance: 50, totalCredit: 100, usedCredit: 20), 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.1 区间追保纯函数测试:落档边界值(恰好等于档界)、看多/看空方向、
|
||||
/// 追保金额=AmountRate×期初全价×券面总额(累计到所落档位,直取)、
|
||||
/// 维持保证金=(初始+总追加)×我方净收取方向。
|
||||
/// 区间结构(docx 确认书追保表同型):多头 第1层[Lower,+∞)、第n层[Lower,Upper);空头 第1层(−∞,Upper]、第n层(Lower,Upper]。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanMarginCalcTest
|
||||
{
|
||||
private static SpanTierConfig Tier(double? lower, double? upper, double? amountRate)
|
||||
=> new() { Lower = lower, Upper = upper, AmountRate = amountRate };
|
||||
|
||||
/// <summary>客户看多 4 层(A=3% 口径:97/94/91/88,比例 0/3%/6%/9%)</summary>
|
||||
private static List<SpanTierConfig> LongTiers() => new()
|
||||
{
|
||||
Tier(0.97, null, 0.00),
|
||||
Tier(0.94, 0.97, 0.03),
|
||||
Tier(0.91, 0.94, 0.06),
|
||||
Tier(0.88, 0.91, 0.09),
|
||||
};
|
||||
|
||||
/// <summary>客户看空 4 层(A=3% 口径:103/106/109/112,比例 0/3%/6%/9%)</summary>
|
||||
private static List<SpanTierConfig> ShortTiers() => new()
|
||||
{
|
||||
Tier(null, 1.03, 0.00),
|
||||
Tier(1.03, 1.06, 0.03),
|
||||
Tier(1.06, 1.09, 0.06),
|
||||
Tier(1.09, 1.12, 0.09),
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// HasSpanConfig:方案B结构判定(存量 x/y 配置回落旧公式)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_001_空配置_视为存量xy()
|
||||
{
|
||||
Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(null));
|
||||
Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig()));
|
||||
Assert.IsFalse(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig
|
||||
{
|
||||
LongSpans = new List<SpanTierConfig> { new SpanTierConfig(), null },
|
||||
ShortSpans = new List<SpanTierConfig> { new SpanTierConfig() }
|
||||
}));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_002_任一方向或线有值_视为新结构()
|
||||
{
|
||||
Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { WarnLine = 0.91 }));
|
||||
Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { LongSpans = new List<SpanTierConfig> { Tier(0.97, null, 0) } }));
|
||||
Assert.IsTrue(SwapSpanMarginCalc.HasSpanConfig(new SpanConfig { ShortSpans = new List<SpanTierConfig> { Tier(null, 1.03, 0) } }));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// IsCustomerLong:与确认书同规则(收取端同向、支付端反向,客户取反)
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_003_客户方向判定()
|
||||
{
|
||||
//我方收取端:PositionType 同向 → 我方多头 → 客户看空
|
||||
Assert.IsFalse(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 1, positionType: 1));
|
||||
Assert.IsTrue(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 1, positionType: 2));
|
||||
//我方支付端:PositionType 反向 → 我方空头 → 客户看多
|
||||
Assert.IsTrue(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 2, positionType: 1));
|
||||
Assert.IsFalse(SwapSpanMarginCalc.IsCustomerLong(posiDirection: 2, positionType: 2));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MatchTier 多头:[Lower, Upper),第1层上不封顶;边界恰好等于档界
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_004_多头落档_区间内与边界值()
|
||||
{
|
||||
var tiers = LongTiers();
|
||||
//未触发(价格充足)→ 第1层
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, true, 1.00));
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, true, 0.97)); //恰好=第1层下界
|
||||
//第2层 [0.94, 0.97)
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9699));
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, true, 0.94)); //恰好=第2层下界
|
||||
//第3层 [0.91, 0.94)
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9399));
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, true, 0.91));
|
||||
//第4层 [0.88, 0.91)
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.9099));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.88)); //恰好=最深层下界
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_005_多头穿出最深层_按最深层计()
|
||||
{
|
||||
var tiers = LongTiers();
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.8799));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, true, 0.50));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// MatchTier 空头:(Lower, Upper],第1层下不设限;边界恰好等于档界
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_006_空头落档_区间内与边界值()
|
||||
{
|
||||
var tiers = ShortTiers();
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, false, 1.00));
|
||||
Assert.AreSame(tiers[0], SwapSpanMarginCalc.MatchTier(tiers, false, 1.03)); //恰好=第1层上界
|
||||
//第2层 (1.03, 1.06]
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0301));
|
||||
Assert.AreSame(tiers[1], SwapSpanMarginCalc.MatchTier(tiers, false, 1.06)); //恰好=第2层上界
|
||||
//第3层 (1.06, 1.09]
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0601));
|
||||
Assert.AreSame(tiers[2], SwapSpanMarginCalc.MatchTier(tiers, false, 1.09));
|
||||
//第4层 (1.09, 1.12]
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.0901));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.12));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_007_空头穿出最深层_按最深层计()
|
||||
{
|
||||
var tiers = ShortTiers();
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 1.1201));
|
||||
Assert.AreSame(tiers[3], SwapSpanMarginCalc.MatchTier(tiers, false, 2.00));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_008_无可用层_返回null()
|
||||
{
|
||||
Assert.IsNull(SwapSpanMarginCalc.MatchTier(null, true, 0.9));
|
||||
Assert.IsNull(SwapSpanMarginCalc.MatchTier(new List<SpanTierConfig>(), false, 1.0));
|
||||
Assert.IsNull(SwapSpanMarginCalc.MatchTier(new List<SpanTierConfig> { new SpanTierConfig() }, true, 0.9));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 追保金额与维持保证金公式
|
||||
// ================================================================
|
||||
|
||||
[TestMethod]
|
||||
public void SS_009_追保金额_比例乘期初全价乘券面()
|
||||
{
|
||||
//第2层 3%:0.03 × 期初全价101.5 × 券面10000 = 30450
|
||||
Assert.AreEqual(30450, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.94, 0.97, 0.03), 101.5, 10000), 1e-6);
|
||||
//第1层 0% → 0
|
||||
Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.97, null, 0), 101.5, 10000), 1e-6);
|
||||
//未配置比例/null 层 → 0
|
||||
Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(null, 101.5, 10000), 1e-6);
|
||||
Assert.AreEqual(0, SwapSpanMarginCalc.CalcAdditionalMargin(Tier(0.94, 0.97, null), 101.5, 10000), 1e-6);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SS_010_维持保证金_初始加追加乘方向()
|
||||
{
|
||||
//我方净收取 +1
|
||||
Assert.AreEqual(80450, SwapSpanMarginCalc.CalcMaintenanceMargin(50000, 30450, 1), 1e-6);
|
||||
//我方净支付 −1(负值=客户应收)
|
||||
Assert.AreEqual(-80450, SwapSpanMarginCalc.CalcMaintenanceMargin(50000, 30450, -1), 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>docx 数值复算(客户看多,A=3%):期初净价98.5 → 当前净价92.0 落第3层,追加=6%×101×100000</summary>
|
||||
[TestMethod]
|
||||
public void SS_011_客户看多数值复算()
|
||||
{
|
||||
var tiers = LongTiers();
|
||||
var ratio = 92.0 / 98.5;
|
||||
var tier = SwapSpanMarginCalc.MatchTier(tiers, isCustomerLong: true, priceRatio: ratio);
|
||||
Assert.AreSame(tiers[2], tier);
|
||||
var additional = SwapSpanMarginCalc.CalcAdditionalMargin(tier, initPrice: 101.0, quantity: 100000);
|
||||
Assert.AreEqual(0.06 * 101.0 * 100000, additional, 1e-6);
|
||||
var maintenance = SwapSpanMarginCalc.CalcMaintenanceMargin(500000, additional, direction: 1);
|
||||
Assert.AreEqual(500000 + 0.06 * 101.0 * 100000, maintenance, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>docx 数值复算(客户看空,A=3%):期初价98.5 → 收盘价104.0 落第2层,追加=3%×101×100000</summary>
|
||||
[TestMethod]
|
||||
public void SS_012_客户看空数值复算()
|
||||
{
|
||||
var tiers = ShortTiers();
|
||||
var ratio = 104.0 / 98.5;
|
||||
var tier = SwapSpanMarginCalc.MatchTier(tiers, isCustomerLong: false, priceRatio: ratio);
|
||||
Assert.AreSame(tiers[1], tier);
|
||||
var additional = SwapSpanMarginCalc.CalcAdditionalMargin(tier, initPrice: 101.0, quantity: 100000);
|
||||
Assert.AreEqual(0.03 * 101.0 * 100000, additional, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BUG-25 回归:配置层未按浅→深排序时,穿出最深层的回落不依赖数组顺序——
|
||||
/// 多头取最小下界层、空头取最大上界层(Last() 假定有序,乱序时回落错层)。
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void SS_013_穿层回落_乱序配置按边界取最深层()
|
||||
{
|
||||
//多头乱序(浅→深:0.97/0.94/0.91/0.88 → 打乱为 0.91/0.97/0.88/0.94)
|
||||
var longShuffled = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(0.91, 0.94, 0.06),
|
||||
Tier(0.97, null, 0.00),
|
||||
Tier(0.88, 0.91, 0.09),
|
||||
Tier(0.94, 0.97, 0.03),
|
||||
};
|
||||
var longDeepest = SwapSpanMarginCalc.MatchTier(longShuffled, isCustomerLong: true, priceRatio: 0.85);
|
||||
Assert.AreEqual(0.88, longDeepest.Lower);
|
||||
Assert.AreEqual(0.09, longDeepest.AmountRate);
|
||||
|
||||
//空头乱序(浅→深:1.03/1.06/1.09/1.12 → 打乱为 1.09/1.03/1.12/1.06)
|
||||
var shortShuffled = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(1.06, 1.09, 0.06),
|
||||
Tier(null, 1.03, 0.00),
|
||||
Tier(1.09, 1.12, 0.09),
|
||||
Tier(1.03, 1.06, 0.03),
|
||||
};
|
||||
var shortDeepest = SwapSpanMarginCalc.MatchTier(shortShuffled, isCustomerLong: false, priceRatio: 1.15);
|
||||
Assert.AreEqual(1.12, shortDeepest.Upper);
|
||||
Assert.AreEqual(0.09, shortDeepest.AmountRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.1 引擎单笔计算场景测试(SwapSpanMarginCalc.CalcTradeMaintenanceMargin,收盘价由调用方解析后注入,
|
||||
/// 覆盖"需要收盘价"的各场景:取到价落档、未取到价兜底、试算初始、期初价缺省、方向与腿缺失)。
|
||||
/// 收盘价解析(债券中债估值净价/ETF收盘价/取不到置0)在 MarginCalculationBase.CalcSwapSpanMaintenanceMargin 胶水层,
|
||||
/// 价格源本身的读库行为见 SwapSpanPriceSourceTest。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanMarginEngineTest
|
||||
{
|
||||
private static SpanTierConfig Tier(double? lower, double? upper, double? amountRate)
|
||||
=> new() { Lower = lower, Upper = upper, AmountRate = amountRate };
|
||||
|
||||
private static SpanConfig LongShortCfg() => new()
|
||||
{
|
||||
LongSpans = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(0.97, null, 0.00), Tier(0.94, 0.97, 0.03), Tier(0.91, 0.94, 0.06), Tier(0.88, 0.91, 0.09)
|
||||
},
|
||||
ShortSpans = new List<SpanTierConfig>
|
||||
{
|
||||
Tier(null, 1.03, 0.00), Tier(1.03, 1.06, 0.03), Tier(1.06, 1.09, 0.06), Tier(1.09, 1.12, 0.09)
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>标的腿(多空),posiDirection/positionType 组合出方向</summary>
|
||||
private static swap_position UnderlyingLeg(int posiDirection, int positionType,
|
||||
decimal grossPrice, decimal? netPrice, decimal quantity, string code = "240004.IB")
|
||||
=> new()
|
||||
{
|
||||
PosiDirection = posiDirection,
|
||||
PositionType = positionType,
|
||||
UnderlyingCode = code,
|
||||
PosiGrossPrice = grossPrice,
|
||||
PosiNetNoFeePrice = netPrice,
|
||||
PosiQuantity = quantity
|
||||
};
|
||||
|
||||
/// <summary>初始预付金腿(InterestMode=5),interestDirection 1=收取 2=支付</summary>
|
||||
private static swap_position MarginLeg(int interestDirection, decimal principal)
|
||||
=> new() { InterestMode = 5, InterestDirection = interestDirection, InterestPrincipalFix = principal };
|
||||
|
||||
// ================================================================
|
||||
// 收盘价取到 → 落档计算
|
||||
// ================================================================
|
||||
|
||||
/// <summary>债券客户看多:收取端标的腿 PositionType=Short → 客户看多;净价 92.0/期初净价 98.5 落第3层</summary>
|
||||
[TestMethod]
|
||||
public void SE_001_债券客户看多_净价落第3层()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(posiDirection: 1, positionType: 2, grossPrice: 101.0m, netPrice: 98.5m, quantity: 100000m),
|
||||
MarginLeg(interestDirection: 1, principal: 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(
|
||||
tradeInitialMargin: null, spanCfg: LongShortCfg(), legs, isInitialCalc: false, closePrice: 92.0);
|
||||
//追加 = 6% × 期初全价101 × 券面100000 = 606000;维持 = 500000 + 606000
|
||||
Assert.AreEqual(1106000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>ETF客户看空:收取端标的腿 PositionType=Long → 客户看空;收盘 1.32/期初净价 1.25 落第2层</summary>
|
||||
[TestMethod]
|
||||
public void SE_002_ETF客户看空_收盘价落第2层()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(posiDirection: 1, positionType: 1, grossPrice: 1.25m, netPrice: 1.25m, quantity: 1000000m, code: "511010.SH"),
|
||||
MarginLeg(interestDirection: 1, principal: 200000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(
|
||||
tradeInitialMargin: null, spanCfg: LongShortCfg(), legs, isInitialCalc: false, closePrice: 1.32);
|
||||
//ratio=1.056 ∈ (1.03,1.06] → 追加 = 3% × 1.25 × 1000000 = 37500;维持 = 200000 + 37500
|
||||
Assert.AreEqual(237500, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>收盘价大幅下跌穿出最深层:按最深层比例计(追加=9%),不叠加也不归零</summary>
|
||||
[TestMethod]
|
||||
public void SE_003_价格穿出最深层_按最深层计()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, grossPrice: 101.0m, netPrice: 98.5m, quantity: 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 80.0);
|
||||
Assert.AreEqual(500000 + 0.09 * 101.0 * 100000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 收盘价未取到 / 试算 —— 兜底行为
|
||||
// ================================================================
|
||||
|
||||
/// <summary>当天无收盘价(closePrice=0):不抛错,追加按0、维持=初始保证金(引擎侧另记告警日志)</summary>
|
||||
[TestMethod]
|
||||
public void SE_004_无收盘价_追加为0维持等于初始()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, isInitialCalc: false, closePrice: 0);
|
||||
Assert.AreEqual(500000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>试算初始(isInitialCalc=true):即使有收盘价也只出初始项——追加保证金为收盘后口径,试算不产出</summary>
|
||||
[TestMethod]
|
||||
public void SE_005_试算初始_不参与追加()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, isInitialCalc: true, closePrice: 92.0);
|
||||
Assert.AreEqual(500000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>期初净价缺失:比基回落期初全价(close/initGross)</summary>
|
||||
[TestMethod]
|
||||
public void SE_006_期初净价缺省_回落期初全价()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, grossPrice: 98.5m, netPrice: null, quantity: 100000m),
|
||||
MarginLeg(1, 500000m)
|
||||
};
|
||||
//close=92.0/98.5 落第3层
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0);
|
||||
Assert.AreEqual(500000 + 0.06 * 98.5 * 100000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 腿数据缺失与方向
|
||||
// ================================================================
|
||||
|
||||
/// <summary>无标的腿(缺期初价):返回 null,引擎跳过该交易不产出 trade_span</summary>
|
||||
[TestMethod]
|
||||
public void SE_007_无标的腿_返回null()
|
||||
{
|
||||
var legs = new List<swap_position> { MarginLeg(1, 500000m) };
|
||||
Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0));
|
||||
Assert.IsNull(SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), new List<swap_position>(), false, 92.0));
|
||||
}
|
||||
|
||||
/// <summary>我方支付初始保证金(InterestDirection=支付):方向取 −1,维持保证金为负(客户应收)</summary>
|
||||
[TestMethod]
|
||||
public void SE_008_我方支付预付金_维持为负()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(interestDirection: 2, principal: 500000m)
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 92.0);
|
||||
Assert.AreEqual(-(500000 + 0.06 * 101.0 * 100000), maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>无预付金腿:初始保证金回落交易录入值 trade.InitialMargin(客户应付常态,方向+1)</summary>
|
||||
[TestMethod]
|
||||
public void SE_009_无预付金腿_回落交易录入初始保证金()
|
||||
{
|
||||
var legs = new List<swap_position> { UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m) };
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(300000, LongShortCfg(), legs, false, closePrice: 92.0);
|
||||
Assert.AreEqual(300000 + 0.06 * 101.0 * 100000, maintenance.Value, 1e-6);
|
||||
}
|
||||
|
||||
/// <summary>多条预付金腿按收付净额定初始与方向(净支付 → −1)</summary>
|
||||
[TestMethod]
|
||||
public void SE_010_多预付金腿_按净收取定方向()
|
||||
{
|
||||
var legs = new List<swap_position>
|
||||
{
|
||||
UnderlyingLeg(1, 2, 101.0m, 98.5m, 100000m),
|
||||
MarginLeg(1, 200000m), //收取 20万
|
||||
MarginLeg(2, 500000m) //支付 50万 → 净支付 30万
|
||||
};
|
||||
var maintenance = SwapSpanMarginCalc.CalcTradeMaintenanceMargin(null, LongShortCfg(), legs, false, closePrice: 0);
|
||||
Assert.AreEqual(-300000, maintenance.Value, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.DataProviderModule;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
/// <summary>
|
||||
/// R2 阶段三 §3.1 收盘价取数链路冒烟测试(只读 dev 库,不构造数据):
|
||||
/// 债券净价源(china_bond_valuation,GetBondPrice 口径 SettlePrice=净价、ClosePrice=全价,≤计算日 取最近——盘中/非交易日回退到最近已有估值);
|
||||
/// 指数/ETF收盘价源(eod_stock_price)。
|
||||
/// 两个源在"价格同步作业跑完前"决定引擎行为:取不到 → 追加按0、维持=初始(见 SwapSpanMarginEngineTest.SE_004)。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SwapSpanPriceSourceTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void SP_001_债券估值净价源_可取且净价为正()
|
||||
{
|
||||
using var db = DbContextFactory.GetYLDbContext();
|
||||
var latest = db.china_bond_valuation
|
||||
.Where(x => x.net_price > 0 && x.dirty_price_close > 0)
|
||||
.OrderByDescending(x => x.valuation_date)
|
||||
.Select(x => new { x.valuation_date, x.bond_id })
|
||||
.FirstOrDefault();
|
||||
if (latest == null)
|
||||
{
|
||||
Assert.Inconclusive("dev 库无中债估值数据,跳过");
|
||||
}
|
||||
|
||||
//当日可取
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetBondEodPrice(latest.valuation_date, latest.bond_id, out var price));
|
||||
Assert.IsTrue(price.SettlePrice > 0, "净价(SettlePrice)应为正");
|
||||
Assert.IsTrue(price.ClosePrice > 0, "全价(ClosePrice)应为正");
|
||||
|
||||
//≤计算日 取最近:往未来多取几天仍回退到最近一条估值(盘中跑引擎即此语义)
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetBondEodPrice(latest.valuation_date.AddDays(5), latest.bond_id, out var fallback));
|
||||
Assert.AreEqual(price.SettlePrice, fallback.SettlePrice, 1e-9);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SP_002_ETF收盘价源_可取()
|
||||
{
|
||||
using var db = DbContextFactory.GetYLDbContext();
|
||||
var latest = db.eod_stock_price
|
||||
.Where(x => x.ClosePrice > 0)
|
||||
.OrderByDescending(x => x.ValueDate)
|
||||
.Select(x => new { x.ValueDate, x.UnderlyingCode })
|
||||
.FirstOrDefault();
|
||||
if (latest == null)
|
||||
{
|
||||
Assert.Inconclusive("dev 库无股票/ETF日终价格数据,跳过");
|
||||
}
|
||||
|
||||
Assert.IsTrue(EodPriceQueryService.TryGetEodPrice(latest.ValueDate, latest.UnderlyingCode, out var price));
|
||||
Assert.IsTrue(price.GetPrice(SettlementTypeEnum.ClosePrice) > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Newtonsoft.Json;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.DBModels.Enums;
|
||||
using YLErp.Modules.SwapModule.Margin;
|
||||
|
||||
namespace YLErp.Modules.SwapModule
|
||||
{
|
||||
@@ -113,6 +114,11 @@ namespace YLErp.Modules.SwapModule
|
||||
CloseReCheckCallCount++;
|
||||
}
|
||||
|
||||
// R4 按标签分流释放:纯内存测试不连库,stub 为全现金(与既有断言语义一致)
|
||||
protected override UnwindTagSplit ReleaseMarginByFundTag(trade td, DateTime valueDate, List<swap_flow_event> interestEvents,
|
||||
decimal marginAmount, decimal marginRebate)
|
||||
=> new UnwindTagSplit { CashMargin = Convert.ToDouble(marginAmount), CashRebate = Convert.ToDouble(marginRebate) };
|
||||
|
||||
protected override void SaveAllChanges() { SaveAllChangesCount++; }
|
||||
protected override void ExecuteInTransaction(Action action) => action(); // 不包事务,直接执行
|
||||
protected override void CallSaveSwapTradeClientCash(trade td, DateTime valueDate) { } // 空操作
|
||||
|
||||
@@ -122,5 +122,14 @@ namespace YLErp.Modules.SwapModule
|
||||
ClientCashCalls.Add((amount, action));
|
||||
return _nextId++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4 自动互换返息按标签分流:纯内存测试不连库(查 swap_position 标签),
|
||||
/// stub 为全额现金返息(与既有断言语义一致)。
|
||||
/// </summary>
|
||||
protected override decimal GetAutoSwapCashRebate(trade td, List<swap_flow_event> flowEvents, decimal totalRebate)
|
||||
{
|
||||
return totalRebate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user