Initial commit: Add project files
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
|
||||
public interface ISellPolicy
|
||||
{
|
||||
int Priority { get; }
|
||||
string PolicyId { get; }
|
||||
SellPolicyResult Evaluate(SellDecisionInput input);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain.Policies;
|
||||
|
||||
public sealed class ConcentrationLiquidityPolicy : ISellPolicy
|
||||
{
|
||||
public int Priority => SellPolicyContract.ConcentrationLiquidityPriority;
|
||||
public string PolicyId => SellPolicyContract.ConcentrationLiquidityPolicyId;
|
||||
|
||||
public SellPolicyResult Evaluate(SellDecisionInput input)
|
||||
{
|
||||
var requested = Math.Clamp(input.ConcentrationSellRatioOfLot, 0m, 1m);
|
||||
if (requested <= 0m)
|
||||
{
|
||||
return SellPolicyResult.NotApplicable(PolicyId, Priority, "NO_CONCENTRATION_EXCESS");
|
||||
}
|
||||
|
||||
if (!input.CooldownSatisfied)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "COOLDOWN_NOT_SATISFIED", requested);
|
||||
}
|
||||
|
||||
var max = input.MaxSellRatioPreservingStrategicCore();
|
||||
var applied = Math.Min(requested, max);
|
||||
if (applied <= 0m)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "STRATEGIC_CORE_BLOCKED", requested);
|
||||
}
|
||||
|
||||
var decision = new SellDecision(
|
||||
SellAction.PartialSell,
|
||||
applied,
|
||||
input.TargetSecurityPortfolioWeightAfter(applied),
|
||||
PolicyId,
|
||||
Priority,
|
||||
"CONCENTRATION_OR_LIQUIDITY",
|
||||
input.EvidenceId,
|
||||
input.DatasetId,
|
||||
input.ModelVersion,
|
||||
input.ConfigVersion,
|
||||
input.CodeSha,
|
||||
true,
|
||||
Array.Empty<PolicyTraceEntry>());
|
||||
return SellPolicyResult.Applied(decision, requested, applied < requested);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain.Policies;
|
||||
|
||||
public sealed class GapFloorBreachPolicy : ISellPolicy
|
||||
{
|
||||
public int Priority => SellPolicyContract.GapFloorBreachPriority;
|
||||
public string PolicyId => SellPolicyContract.GapFloorBreachPolicyId;
|
||||
|
||||
public SellPolicyResult Evaluate(SellDecisionInput input)
|
||||
{
|
||||
const decimal requested = SellPolicyContract.GapFloorSellRatioOfLot;
|
||||
if (input.GapBelowFloorAtr < SellPolicyContract.GapFloorAtrThreshold)
|
||||
{
|
||||
return SellPolicyResult.NotApplicable(PolicyId, Priority, "GAP_BELOW_1_5_ATR");
|
||||
}
|
||||
|
||||
if (!input.CooldownSatisfied)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "COOLDOWN_NOT_SATISFIED", requested);
|
||||
}
|
||||
|
||||
var max = input.MaxSellRatioPreservingStrategicCore();
|
||||
var applied = Math.Min(requested, max);
|
||||
if (applied <= 0m)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "STRATEGIC_CORE_BLOCKED", requested);
|
||||
}
|
||||
|
||||
var decision = Create(input, applied, "GAP_FLOOR_BREACH");
|
||||
return SellPolicyResult.Applied(decision, requested, applied < requested);
|
||||
}
|
||||
|
||||
private SellDecision Create(SellDecisionInput input, decimal ratio, string reason)
|
||||
=> new(
|
||||
SellAction.PartialSell,
|
||||
ratio,
|
||||
input.TargetSecurityPortfolioWeightAfter(ratio),
|
||||
PolicyId,
|
||||
Priority,
|
||||
reason,
|
||||
input.EvidenceId,
|
||||
input.DatasetId,
|
||||
input.ModelVersion,
|
||||
input.ConfigVersion,
|
||||
input.CodeSha,
|
||||
true,
|
||||
Array.Empty<PolicyTraceEntry>());
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain.Policies;
|
||||
|
||||
public sealed class HardImpairmentPolicy : ISellPolicy
|
||||
{
|
||||
public int Priority => SellPolicyContract.HardImpairmentPriority;
|
||||
public string PolicyId => SellPolicyContract.HardImpairmentPolicyId;
|
||||
|
||||
public SellPolicyResult Evaluate(SellDecisionInput input)
|
||||
{
|
||||
if (!input.HardImpairmentApproved)
|
||||
{
|
||||
return SellPolicyResult.NotApplicable(PolicyId, Priority, "HARD_IMPAIRMENT_NOT_APPROVED");
|
||||
}
|
||||
|
||||
const decimal ratio = SellPolicyContract.HardImpairmentSellRatioOfLot;
|
||||
var decision = Create(input, ratio, SellAction.FullSell, "HARD_IMPAIRMENT", false);
|
||||
return SellPolicyResult.Applied(decision, ratio, false);
|
||||
}
|
||||
|
||||
private SellDecision Create(
|
||||
SellDecisionInput input,
|
||||
decimal ratio,
|
||||
SellAction action,
|
||||
string reason,
|
||||
bool reentryEligible)
|
||||
=> new(
|
||||
action,
|
||||
ratio,
|
||||
input.TargetSecurityPortfolioWeightAfter(ratio),
|
||||
PolicyId,
|
||||
Priority,
|
||||
reason,
|
||||
input.EvidenceId,
|
||||
input.DatasetId,
|
||||
input.ModelVersion,
|
||||
input.ConfigVersion,
|
||||
input.CodeSha,
|
||||
reentryEligible,
|
||||
Array.Empty<PolicyTraceEntry>());
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain.Policies;
|
||||
|
||||
public sealed class OpportunityCostPolicy : ISellPolicy
|
||||
{
|
||||
public int Priority => SellPolicyContract.OpportunityCostPriority;
|
||||
public string PolicyId => SellPolicyContract.OpportunityCostPolicyId;
|
||||
|
||||
public SellPolicyResult Evaluate(SellDecisionInput input)
|
||||
{
|
||||
if (input.OpportunityEdgeLowerBound <= 0m)
|
||||
{
|
||||
return SellPolicyResult.NotApplicable(PolicyId, Priority, "EDGE_LOWER_BOUND_NOT_POSITIVE");
|
||||
}
|
||||
|
||||
if (input.OpportunitySellRatioOfLot <= 0m)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "OPPORTUNITY_RATIO_NOT_POSITIVE");
|
||||
}
|
||||
|
||||
var requested = Math.Clamp(
|
||||
input.OpportunitySellRatioOfLot,
|
||||
SellPolicyContract.OpportunityMinimumSellRatioOfLot,
|
||||
SellPolicyContract.OpportunityMaximumSellRatioOfLot);
|
||||
|
||||
if (!input.CooldownSatisfied)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "COOLDOWN_NOT_SATISFIED", requested);
|
||||
}
|
||||
|
||||
var max = input.MaxSellRatioPreservingStrategicCore();
|
||||
var applied = Math.Min(requested, max);
|
||||
if (applied <= 0m)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "STRATEGIC_CORE_BLOCKED", requested);
|
||||
}
|
||||
|
||||
var decision = new SellDecision(
|
||||
SellAction.PartialSell,
|
||||
applied,
|
||||
input.TargetSecurityPortfolioWeightAfter(applied),
|
||||
PolicyId,
|
||||
Priority,
|
||||
"OPPORTUNITY_REPLACEMENT",
|
||||
input.EvidenceId,
|
||||
input.DatasetId,
|
||||
input.ModelVersion,
|
||||
input.ConfigVersion,
|
||||
input.CodeSha,
|
||||
true,
|
||||
Array.Empty<PolicyTraceEntry>());
|
||||
return SellPolicyResult.Applied(decision, requested, applied < requested);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain.Policies;
|
||||
|
||||
/// <summary>Capital-floor protection may cross the strategic core because survival outranks profit protection.</summary>
|
||||
public sealed class PortfolioSurvivalPolicy : ISellPolicy
|
||||
{
|
||||
public int Priority => SellPolicyContract.PortfolioSurvivalPriority;
|
||||
public string PolicyId => SellPolicyContract.PortfolioSurvivalPolicyId;
|
||||
|
||||
public SellPolicyResult Evaluate(SellDecisionInput input)
|
||||
{
|
||||
if (!input.CapitalFloorBreached)
|
||||
{
|
||||
return SellPolicyResult.NotApplicable(PolicyId, Priority, "CAPITAL_FLOOR_NOT_BREACHED");
|
||||
}
|
||||
|
||||
var requested = Math.Clamp(input.SurvivalSellRatioOfLot, 0m, 1m);
|
||||
if (!input.CooldownSatisfied)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "COOLDOWN_NOT_SATISFIED", requested);
|
||||
}
|
||||
|
||||
if (requested <= 0m)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "SURVIVAL_RATIO_NOT_POSITIVE", requested);
|
||||
}
|
||||
|
||||
var action = requested >= 1m ? SellAction.FullSell : SellAction.PartialSell;
|
||||
var decision = new SellDecision(
|
||||
action,
|
||||
requested,
|
||||
input.TargetSecurityPortfolioWeightAfter(requested),
|
||||
PolicyId,
|
||||
Priority,
|
||||
"PORTFOLIO_SURVIVAL",
|
||||
input.EvidenceId,
|
||||
input.DatasetId,
|
||||
input.ModelVersion,
|
||||
input.ConfigVersion,
|
||||
input.CodeSha,
|
||||
true,
|
||||
Array.Empty<PolicyTraceEntry>());
|
||||
return SellPolicyResult.Applied(decision, requested, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain.Policies;
|
||||
|
||||
public sealed class TwoCloseFloorBreachPolicy : ISellPolicy
|
||||
{
|
||||
public int Priority => SellPolicyContract.TwoCloseFloorBreachPriority;
|
||||
public string PolicyId => SellPolicyContract.TwoCloseFloorBreachPolicyId;
|
||||
|
||||
public SellPolicyResult Evaluate(SellDecisionInput input)
|
||||
{
|
||||
const decimal requested = SellPolicyContract.TwoCloseSellRatioOfLot;
|
||||
if (input.ConsecutiveCloseBreaches < SellPolicyContract.TwoCloseBreachCount)
|
||||
{
|
||||
return SellPolicyResult.NotApplicable(PolicyId, Priority, "TWO_CLOSE_NOT_CONFIRMED");
|
||||
}
|
||||
|
||||
if (!input.CooldownSatisfied)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "COOLDOWN_NOT_SATISFIED", requested);
|
||||
}
|
||||
|
||||
var max = input.MaxSellRatioPreservingStrategicCore();
|
||||
var applied = Math.Min(requested, max);
|
||||
if (applied <= 0m)
|
||||
{
|
||||
return SellPolicyResult.Blocked(PolicyId, Priority, "STRATEGIC_CORE_BLOCKED", requested);
|
||||
}
|
||||
|
||||
var decision = new SellDecision(
|
||||
SellAction.PartialSell,
|
||||
applied,
|
||||
input.TargetSecurityPortfolioWeightAfter(applied),
|
||||
PolicyId,
|
||||
Priority,
|
||||
"TWO_CLOSE_FLOOR_BREACH",
|
||||
input.EvidenceId,
|
||||
input.DatasetId,
|
||||
input.ModelVersion,
|
||||
input.ConfigVersion,
|
||||
input.CodeSha,
|
||||
true,
|
||||
Array.Empty<PolicyTraceEntry>());
|
||||
return SellPolicyResult.Applied(decision, requested, applied < requested);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
|
||||
public enum ReentryState
|
||||
{
|
||||
Watching,
|
||||
Ready,
|
||||
Reentered,
|
||||
Open,
|
||||
Expired,
|
||||
Closed
|
||||
}
|
||||
|
||||
public sealed record ReentryInput(
|
||||
int SessionsSinceSell,
|
||||
int SessionsSinceLastStage,
|
||||
bool RisingSma50Reclaimed,
|
||||
bool Prior20SessionHighBroken,
|
||||
bool AdditionalAssetConfirmationPassed,
|
||||
bool HardImpairment,
|
||||
bool Expired,
|
||||
bool StageExecuted = false,
|
||||
bool HasRemainingStages = false);
|
||||
|
||||
public static class ReentryStateMachine
|
||||
{
|
||||
public static ReentryState Evaluate(ReentryState current, ReentryInput input)
|
||||
{
|
||||
if (input.HardImpairment)
|
||||
{
|
||||
return ReentryState.Closed;
|
||||
}
|
||||
|
||||
if (input.Expired && current is not ReentryState.Open)
|
||||
{
|
||||
return ReentryState.Expired;
|
||||
}
|
||||
|
||||
if (current is ReentryState.Expired or ReentryState.Closed or ReentryState.Open)
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
if (current is ReentryState.Ready && input.StageExecuted)
|
||||
{
|
||||
return ReentryState.Reentered;
|
||||
}
|
||||
|
||||
if (current is ReentryState.Reentered)
|
||||
{
|
||||
return input.HasRemainingStages ? ReentryState.Watching : ReentryState.Open;
|
||||
}
|
||||
|
||||
var ready = input.SessionsSinceSell >= 10
|
||||
&& input.SessionsSinceLastStage >= 10
|
||||
&& input.RisingSma50Reclaimed
|
||||
&& input.Prior20SessionHighBroken
|
||||
&& input.AdditionalAssetConfirmationPassed;
|
||||
|
||||
return ready ? ReentryState.Ready : ReentryState.Watching;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
|
||||
public enum SellAction
|
||||
{
|
||||
Hold = 0,
|
||||
PartialSell = 1,
|
||||
FullSell = 2
|
||||
}
|
||||
|
||||
public sealed record SellDecision(
|
||||
SellAction Action,
|
||||
decimal SellRatioOfLot,
|
||||
decimal TargetSecurityPortfolioWeightAfter,
|
||||
string PolicyId,
|
||||
int Priority,
|
||||
string ReasonCode,
|
||||
string EvidenceId,
|
||||
string DatasetId,
|
||||
string ModelVersion,
|
||||
string ConfigVersion,
|
||||
string CodeSha,
|
||||
bool ReentryEligible,
|
||||
IReadOnlyList<PolicyTraceEntry> PolicyTrace)
|
||||
{
|
||||
public static SellDecision Hold(SellDecisionInput input, IReadOnlyList<PolicyTraceEntry> trace)
|
||||
=> new(
|
||||
SellAction.Hold,
|
||||
0m,
|
||||
input.CurrentSecurityPortfolioWeight,
|
||||
"ALG-HOLD-001",
|
||||
0,
|
||||
"NO_SELL_CONDITION",
|
||||
input.EvidenceId,
|
||||
input.DatasetId,
|
||||
input.ModelVersion,
|
||||
input.ConfigVersion,
|
||||
input.CodeSha,
|
||||
false,
|
||||
trace);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
public sealed record SellDecisionEvidence(
|
||||
string EvidenceId, string DatasetId, string ModelVersion, string ConfigVersion, string CodeSha,
|
||||
DateTimeOffset AsOf, DateTimeOffset PublishedAtCutoff, decimal SellRatioOfLot,
|
||||
decimal CurrentPortfolioWeight, decimal StrategicCoreFloorWeight, decimal TargetPortfolioWeightAfter);
|
||||
public static class SellDecisionEvidenceGuard
|
||||
{
|
||||
public static void EnsureValid(SellDecisionEvidence evidence)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(evidence.EvidenceId) || string.IsNullOrWhiteSpace(evidence.DatasetId) || string.IsNullOrWhiteSpace(evidence.ModelVersion) || string.IsNullOrWhiteSpace(evidence.ConfigVersion) || string.IsNullOrWhiteSpace(evidence.CodeSha))
|
||||
throw new InvalidOperationException("Evidence and VersionSet fields are required.");
|
||||
if (evidence.PublishedAtCutoff > evidence.AsOf) throw new InvalidOperationException("Look-ahead evidence is forbidden.");
|
||||
EnsureUnitInterval(evidence.SellRatioOfLot,nameof(evidence.SellRatioOfLot));
|
||||
EnsureUnitInterval(evidence.CurrentPortfolioWeight,nameof(evidence.CurrentPortfolioWeight));
|
||||
EnsureUnitInterval(evidence.StrategicCoreFloorWeight,nameof(evidence.StrategicCoreFloorWeight));
|
||||
EnsureUnitInterval(evidence.TargetPortfolioWeightAfter,nameof(evidence.TargetPortfolioWeightAfter));
|
||||
if (evidence.TargetPortfolioWeightAfter > evidence.CurrentPortfolioWeight) throw new InvalidOperationException("Sell decision cannot increase portfolio weight.");
|
||||
}
|
||||
private static void EnsureUnitInterval(decimal value,string name) { if (value<0m || value>1m) throw new ArgumentOutOfRangeException(name,value,"Expected 0..1."); }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Immutable point-in-time input resolved server-side from an approved EvidenceSnapshot/read model.
|
||||
/// Security weight and lot weight are intentionally separate: a lot-relative sell ratio must not be
|
||||
/// multiplied by the whole security weight.
|
||||
/// </summary>
|
||||
public sealed record SellDecisionInput(
|
||||
Guid PositionLotId,
|
||||
Guid CycleId,
|
||||
string EvidenceId,
|
||||
string DatasetId,
|
||||
string ModelVersion,
|
||||
string ConfigVersion,
|
||||
string CodeSha,
|
||||
DateTimeOffset AsOf,
|
||||
DateTimeOffset PublishedAtCutoff,
|
||||
decimal CurrentSecurityPortfolioWeight,
|
||||
decimal CurrentLotPortfolioWeight,
|
||||
decimal StrategicCoreFloorWeight,
|
||||
bool HardImpairmentApproved,
|
||||
bool CapitalFloorBreached,
|
||||
decimal SurvivalSellRatioOfLot,
|
||||
decimal GapBelowFloorAtr,
|
||||
int ConsecutiveCloseBreaches,
|
||||
bool CooldownSatisfied,
|
||||
decimal ConcentrationSellRatioOfLot,
|
||||
decimal OpportunityEdgeLowerBound,
|
||||
decimal OpportunitySellRatioOfLot)
|
||||
{
|
||||
public void EnsureValid()
|
||||
{
|
||||
if (PublishedAtCutoff > AsOf)
|
||||
{
|
||||
throw new InvalidOperationException("PublishedAtCutoff cannot be later than AsOf.");
|
||||
}
|
||||
|
||||
if (CurrentSecurityPortfolioWeight is < 0m or > 1m)
|
||||
{
|
||||
throw new InvalidOperationException("CurrentSecurityPortfolioWeight must be between 0 and 1.");
|
||||
}
|
||||
|
||||
if (CurrentLotPortfolioWeight is < 0m or > 1m
|
||||
|| CurrentLotPortfolioWeight > CurrentSecurityPortfolioWeight)
|
||||
{
|
||||
throw new InvalidOperationException("CurrentLotPortfolioWeight must be between 0 and the security weight.");
|
||||
}
|
||||
|
||||
if (StrategicCoreFloorWeight is < 0m or > 1m)
|
||||
{
|
||||
throw new InvalidOperationException("StrategicCoreFloorWeight must be between 0 and 1.");
|
||||
}
|
||||
}
|
||||
|
||||
public decimal MaxSellRatioPreservingStrategicCore()
|
||||
{
|
||||
if (CurrentLotPortfolioWeight <= 0m)
|
||||
{
|
||||
return 0m;
|
||||
}
|
||||
|
||||
var sellableSecurityWeight = Math.Max(0m, CurrentSecurityPortfolioWeight - StrategicCoreFloorWeight);
|
||||
return Math.Clamp(sellableSecurityWeight / CurrentLotPortfolioWeight, 0m, 1m);
|
||||
}
|
||||
|
||||
public decimal TargetSecurityPortfolioWeightAfter(decimal sellRatioOfLot)
|
||||
=> decimal.Round(
|
||||
Math.Max(0m, CurrentSecurityPortfolioWeight
|
||||
- CurrentLotPortfolioWeight * Math.Clamp(sellRatioOfLot, 0m, 1m)),
|
||||
8,
|
||||
MidpointRounding.ToEven);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
|
||||
public sealed class SellPolicyChain(IEnumerable<ISellPolicy> policies)
|
||||
{
|
||||
private readonly ISellPolicy[] _policies = policies
|
||||
.OrderByDescending(x => x.Priority)
|
||||
.ThenBy(x => x.PolicyId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
public SellDecision Evaluate(SellDecisionInput input)
|
||||
{
|
||||
input.EnsureValid();
|
||||
var trace = new List<PolicyTraceEntry>(_policies.Length);
|
||||
|
||||
foreach (var policy in _policies)
|
||||
{
|
||||
var result = policy.Evaluate(input);
|
||||
trace.Add(result.Trace);
|
||||
if (result.Decision is not null)
|
||||
{
|
||||
return result.Decision with { PolicyTrace = trace.AsReadOnly() };
|
||||
}
|
||||
}
|
||||
|
||||
return SellDecision.Hold(input, trace.AsReadOnly());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// Approved implementation contract for the v12.3 research-candidate sell policy chain.
|
||||
/// Values are mirrored in contracts/policies/sell-policy-contract.v1.json and checked by validate_v123.py.
|
||||
/// Changing any value requires a Model Change record, Golden vectors and OOS impact evidence.
|
||||
/// </summary>
|
||||
public static class SellPolicyContract
|
||||
{
|
||||
public const string ContractVersion = "sell-policy.v1";
|
||||
public const string DecisionContractVersion = "sell-decision.v2";
|
||||
public const int PolicyTraceSchemaVersion = 2;
|
||||
|
||||
public const string HardImpairmentPolicyId = "ALG-SELL-001";
|
||||
public const string PortfolioSurvivalPolicyId = "ALG-SELL-PORT-001";
|
||||
public const string GapFloorBreachPolicyId = "ALG-SELL-002";
|
||||
public const string TwoCloseFloorBreachPolicyId = "ALG-SELL-003";
|
||||
public const string ConcentrationLiquidityPolicyId = "ALG-SELL-004";
|
||||
public const string OpportunityCostPolicyId = "ALG-SELL-005";
|
||||
|
||||
public const int HardImpairmentPriority = 1000;
|
||||
public const int PortfolioSurvivalPriority = 900;
|
||||
public const int GapFloorBreachPriority = 800;
|
||||
public const int TwoCloseFloorBreachPriority = 700;
|
||||
public const int ConcentrationLiquidityPriority = 600;
|
||||
public const int OpportunityCostPriority = 500;
|
||||
|
||||
public const decimal HardImpairmentSellRatioOfLot = 1.00m;
|
||||
public const decimal GapFloorAtrThreshold = 1.50m;
|
||||
public const decimal GapFloorSellRatioOfLot = 0.40m;
|
||||
public const int TwoCloseBreachCount = 2;
|
||||
public const decimal TwoCloseSellRatioOfLot = 0.20m;
|
||||
public const decimal OpportunityMinimumSellRatioOfLot = 0.10m;
|
||||
public const decimal OpportunityMaximumSellRatioOfLot = 0.25m;
|
||||
|
||||
public static IReadOnlyList<SellPolicyDefinition> Definitions { get; } =
|
||||
[
|
||||
new(HardImpairmentPolicyId, HardImpairmentPriority, true),
|
||||
new(PortfolioSurvivalPolicyId, PortfolioSurvivalPriority, true),
|
||||
new(GapFloorBreachPolicyId, GapFloorBreachPriority, false),
|
||||
new(TwoCloseFloorBreachPolicyId, TwoCloseFloorBreachPriority, false),
|
||||
new(ConcentrationLiquidityPolicyId, ConcentrationLiquidityPriority, false),
|
||||
new(OpportunityCostPolicyId, OpportunityCostPriority, false)
|
||||
];
|
||||
}
|
||||
|
||||
public sealed record SellPolicyDefinition(string PolicyId, int Priority, bool MayCrossStrategicCore);
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace KArtSell.Modules.SignalEngine.Domain;
|
||||
|
||||
public enum PolicyDisposition
|
||||
{
|
||||
NotApplicable = 0,
|
||||
Blocked = 1,
|
||||
Applied = 2
|
||||
}
|
||||
|
||||
public sealed record PolicyTraceEntry(
|
||||
string PolicyId,
|
||||
int Priority,
|
||||
PolicyDisposition Disposition,
|
||||
string ReasonCode,
|
||||
decimal RequestedSellRatioOfLot,
|
||||
decimal AppliedSellRatioOfLot,
|
||||
bool StrategicCoreClampApplied);
|
||||
|
||||
public sealed record SellPolicyResult(
|
||||
PolicyTraceEntry Trace,
|
||||
SellDecision? Decision)
|
||||
{
|
||||
public static SellPolicyResult NotApplicable(string policyId, int priority, string reasonCode)
|
||||
=> new(new PolicyTraceEntry(policyId, priority, PolicyDisposition.NotApplicable, reasonCode, 0m, 0m, false), null);
|
||||
|
||||
public static SellPolicyResult Blocked(
|
||||
string policyId,
|
||||
int priority,
|
||||
string reasonCode,
|
||||
decimal requestedSellRatioOfLot = 0m)
|
||||
=> new(new PolicyTraceEntry(
|
||||
policyId,
|
||||
priority,
|
||||
PolicyDisposition.Blocked,
|
||||
reasonCode,
|
||||
requestedSellRatioOfLot,
|
||||
0m,
|
||||
false), null);
|
||||
|
||||
public static SellPolicyResult Applied(
|
||||
SellDecision decision,
|
||||
decimal requestedSellRatioOfLot,
|
||||
bool strategicCoreClampApplied)
|
||||
=> new(new PolicyTraceEntry(
|
||||
decision.PolicyId,
|
||||
decision.Priority,
|
||||
PolicyDisposition.Applied,
|
||||
decision.ReasonCode,
|
||||
requestedSellRatioOfLot,
|
||||
decision.SellRatioOfLot,
|
||||
strategicCoreClampApplied), decision);
|
||||
}
|
||||
Reference in New Issue
Block a user