Initial commit: Add project files
ci / backend (push) Failing after 12s
ci / frontend (push) Failing after 19s
ci / static (push) Failing after 45s

This commit is contained in:
2026-08-02 05:15:36 +09:00
commit dcd1322d41
636 changed files with 122352 additions and 0 deletions
@@ -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);
}