Initial commit: Add project files
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
using KArtSell.Modules.SignalEngine.Domain;
|
||||
using KArtSell.Modules.SignalEngine.Domain.Policies;
|
||||
|
||||
namespace KArtSell.SignalEngine.UnitTests;
|
||||
|
||||
public sealed class SellPolicyChainTests
|
||||
{
|
||||
private static SellPolicyChain CreateSut() => new(new ISellPolicy[]
|
||||
{
|
||||
new OpportunityCostPolicy(),
|
||||
new ConcentrationLiquidityPolicy(),
|
||||
new TwoCloseFloorBreachPolicy(),
|
||||
new GapFloorBreachPolicy(),
|
||||
new PortfolioSurvivalPolicy(),
|
||||
new HardImpairmentPolicy()
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void Hard_impairment_cannot_be_overridden_by_lower_priority_policy()
|
||||
{
|
||||
var input = BaseInput() with
|
||||
{
|
||||
HardImpairmentApproved = true,
|
||||
CapitalFloorBreached = true,
|
||||
GapBelowFloorAtr = 2.0m,
|
||||
ConsecutiveCloseBreaches = 3,
|
||||
OpportunityEdgeLowerBound = 0.10m,
|
||||
OpportunitySellRatioOfLot = 0.25m
|
||||
};
|
||||
|
||||
var result = CreateSut().Evaluate(input);
|
||||
|
||||
Assert.Equal("ALG-SELL-001", result.PolicyId);
|
||||
Assert.Equal(SellAction.FullSell, result.Action);
|
||||
Assert.Equal(1m, result.SellRatioOfLot);
|
||||
Assert.False(result.ReentryEligible);
|
||||
Assert.Single(result.PolicyTrace);
|
||||
Assert.Equal(PolicyDisposition.Applied, result.PolicyTrace[0].Disposition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Portfolio_survival_outranks_profit_floor_and_may_cross_strategic_core()
|
||||
{
|
||||
var input = BaseInput() with
|
||||
{
|
||||
CurrentSecurityPortfolioWeight = 0.40m,
|
||||
CurrentLotPortfolioWeight = 0.20m,
|
||||
StrategicCoreFloorWeight = 0.30m,
|
||||
CapitalFloorBreached = true,
|
||||
SurvivalSellRatioOfLot = 0.75m,
|
||||
GapBelowFloorAtr = 2.0m
|
||||
};
|
||||
|
||||
var result = CreateSut().Evaluate(input);
|
||||
|
||||
Assert.Equal("ALG-SELL-PORT-001", result.PolicyId);
|
||||
Assert.Equal(0.75m, result.SellRatioOfLot);
|
||||
Assert.Equal(0.25m, result.TargetSecurityPortfolioWeightAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lot_relative_ratio_uses_lot_weight_not_whole_security_weight()
|
||||
{
|
||||
var input = BaseInput() with
|
||||
{
|
||||
CurrentSecurityPortfolioWeight = 0.60m,
|
||||
CurrentLotPortfolioWeight = 0.20m,
|
||||
StrategicCoreFloorWeight = 0.50m,
|
||||
GapBelowFloorAtr = 2.0m
|
||||
};
|
||||
|
||||
var result = CreateSut().Evaluate(input);
|
||||
|
||||
Assert.Equal("ALG-SELL-002", result.PolicyId);
|
||||
Assert.Equal(0.40m, result.SellRatioOfLot);
|
||||
Assert.Equal(0.52m, result.TargetSecurityPortfolioWeightAfter);
|
||||
Assert.False(result.PolicyTrace.Single(x => x.PolicyId == "ALG-SELL-002").StrategicCoreClampApplied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Strategic_core_clamps_lot_ratio_when_only_part_of_lot_is_sellable()
|
||||
{
|
||||
var input = BaseInput() with
|
||||
{
|
||||
CurrentSecurityPortfolioWeight = 0.60m,
|
||||
CurrentLotPortfolioWeight = 0.40m,
|
||||
StrategicCoreFloorWeight = 0.50m,
|
||||
GapBelowFloorAtr = 2.0m
|
||||
};
|
||||
|
||||
var result = CreateSut().Evaluate(input);
|
||||
|
||||
Assert.Equal(0.25m, result.SellRatioOfLot);
|
||||
Assert.Equal(0.50m, result.TargetSecurityPortfolioWeightAfter);
|
||||
Assert.True(result.PolicyTrace.Single(x => x.PolicyId == "ALG-SELL-002").StrategicCoreClampApplied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Opportunity_sell_requires_positive_lower_confidence_edge()
|
||||
{
|
||||
var blocked = CreateSut().Evaluate(BaseInput() with
|
||||
{
|
||||
OpportunityEdgeLowerBound = 0m,
|
||||
OpportunitySellRatioOfLot = 0.20m
|
||||
});
|
||||
|
||||
var allowed = CreateSut().Evaluate(BaseInput() with
|
||||
{
|
||||
OpportunityEdgeLowerBound = 0.01m,
|
||||
OpportunitySellRatioOfLot = 0.20m
|
||||
});
|
||||
|
||||
Assert.Equal(SellAction.Hold, blocked.Action);
|
||||
Assert.Equal("ALG-SELL-005", allowed.PolicyId);
|
||||
Assert.Equal(0.20m, allowed.SellRatioOfLot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Positive_opportunity_edge_with_zero_requested_ratio_cannot_create_a_sell()
|
||||
{
|
||||
var result = CreateSut().Evaluate(BaseInput() with
|
||||
{
|
||||
OpportunityEdgeLowerBound = 0.02m,
|
||||
OpportunitySellRatioOfLot = 0m
|
||||
});
|
||||
|
||||
Assert.Equal(SellAction.Hold, result.Action);
|
||||
var trace = Assert.Single(result.PolicyTrace, x => x.PolicyId == SellPolicyContract.OpportunityCostPolicyId);
|
||||
Assert.Equal(PolicyDisposition.Blocked, trace.Disposition);
|
||||
Assert.Equal("OPPORTUNITY_RATIO_NOT_POSITIVE", trace.ReasonCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Future_published_evidence_is_rejected()
|
||||
{
|
||||
var input = BaseInput() with
|
||||
{
|
||||
PublishedAtCutoff = DateTimeOffset.Parse("2026-08-01T08:00:00Z"),
|
||||
AsOf = DateTimeOffset.Parse("2026-08-01T07:00:00Z")
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => CreateSut().Evaluate(input));
|
||||
}
|
||||
|
||||
private static SellDecisionInput BaseInput() => new(
|
||||
Guid.Parse("00000000-0000-0000-0000-000000000001"),
|
||||
Guid.Parse("00000000-0000-0000-0000-000000000002"),
|
||||
"evidence-1",
|
||||
"dataset-1",
|
||||
"model-1",
|
||||
"config-1",
|
||||
"code-sha-1",
|
||||
DateTimeOffset.Parse("2026-08-01T07:00:00Z"),
|
||||
DateTimeOffset.Parse("2026-08-01T06:00:00Z"),
|
||||
0.60m,
|
||||
0.20m,
|
||||
0.30m,
|
||||
false,
|
||||
false,
|
||||
0m,
|
||||
0m,
|
||||
0,
|
||||
true,
|
||||
0m,
|
||||
0m,
|
||||
0m);
|
||||
}
|
||||
Reference in New Issue
Block a user