Initial commit: Add project files
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
namespace KArtSell.Modules.ModelOperations.FeedbackLoop;
|
||||
|
||||
public enum ModelFeedbackCycleState
|
||||
{
|
||||
Planned,
|
||||
PredictionFrozen,
|
||||
OutcomesMaturing,
|
||||
Evaluated,
|
||||
ImprovementProposed,
|
||||
ChallengerPlanned,
|
||||
IndependentlyValidated,
|
||||
PromotionReviewPending,
|
||||
BusinessHold,
|
||||
Closed
|
||||
}
|
||||
|
||||
public sealed record ModelFeedbackTransition(
|
||||
ModelFeedbackCycleState From,
|
||||
ModelFeedbackCycleState To,
|
||||
string ReasonCode,
|
||||
string EvidenceHash,
|
||||
string ActorId,
|
||||
DateTimeOffset OccurredAt);
|
||||
|
||||
public sealed class ModelFeedbackCycle
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<ModelFeedbackCycleState, IReadOnlySet<ModelFeedbackCycleState>> Allowed =
|
||||
new Dictionary<ModelFeedbackCycleState, IReadOnlySet<ModelFeedbackCycleState>>
|
||||
{
|
||||
[ModelFeedbackCycleState.Planned] = Set(ModelFeedbackCycleState.PredictionFrozen, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.PredictionFrozen] = Set(ModelFeedbackCycleState.OutcomesMaturing, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.OutcomesMaturing] = Set(ModelFeedbackCycleState.Evaluated, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.Evaluated] = Set(ModelFeedbackCycleState.ImprovementProposed, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.ImprovementProposed] = Set(ModelFeedbackCycleState.ChallengerPlanned, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.ChallengerPlanned] = Set(ModelFeedbackCycleState.IndependentlyValidated, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.IndependentlyValidated] = Set(ModelFeedbackCycleState.PromotionReviewPending, ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.PromotionReviewPending] = Set(ModelFeedbackCycleState.Closed, ModelFeedbackCycleState.BusinessHold),
|
||||
[ModelFeedbackCycleState.BusinessHold] = Set(ModelFeedbackCycleState.Planned, ModelFeedbackCycleState.Closed),
|
||||
[ModelFeedbackCycleState.Closed] = new HashSet<ModelFeedbackCycleState>()
|
||||
};
|
||||
|
||||
private readonly List<ModelFeedbackTransition> transitions = [];
|
||||
|
||||
public ModelFeedbackCycle(Guid cycleId, string scopeKey, string baseModelVersion, DateTimeOffset startedAt)
|
||||
{
|
||||
if (cycleId == Guid.Empty) throw new ArgumentException("Cycle ID is required.", nameof(cycleId));
|
||||
if (string.IsNullOrWhiteSpace(scopeKey)) throw new ArgumentException("Scope key is required.", nameof(scopeKey));
|
||||
if (string.IsNullOrWhiteSpace(baseModelVersion)) throw new ArgumentException("Base model version is required.", nameof(baseModelVersion));
|
||||
|
||||
CycleId = cycleId;
|
||||
ScopeKey = scopeKey;
|
||||
BaseModelVersion = baseModelVersion;
|
||||
StartedAt = startedAt;
|
||||
State = ModelFeedbackCycleState.Planned;
|
||||
Revision = 1;
|
||||
LastOccurredAt = startedAt;
|
||||
}
|
||||
|
||||
public Guid CycleId { get; }
|
||||
public string ScopeKey { get; }
|
||||
public string BaseModelVersion { get; }
|
||||
public DateTimeOffset StartedAt { get; }
|
||||
public ModelFeedbackCycleState State { get; private set; }
|
||||
public int Revision { get; private set; }
|
||||
public DateTimeOffset LastOccurredAt { get; private set; }
|
||||
public DateTimeOffset? ClosedAt { get; private set; }
|
||||
public IReadOnlyList<ModelFeedbackTransition> Transitions => transitions;
|
||||
|
||||
public ModelFeedbackTransition MoveTo(
|
||||
ModelFeedbackCycleState next,
|
||||
string reasonCode,
|
||||
string evidenceHash,
|
||||
string actorId,
|
||||
DateTimeOffset occurredAt)
|
||||
{
|
||||
if (!Allowed[State].Contains(next))
|
||||
throw new InvalidOperationException($"Feedback cycle transition {State} -> {next} is not allowed.");
|
||||
if (string.IsNullOrWhiteSpace(reasonCode)) throw new ArgumentException("Reason code is required.", nameof(reasonCode));
|
||||
if (string.IsNullOrWhiteSpace(evidenceHash)) throw new ArgumentException("Evidence hash is required.", nameof(evidenceHash));
|
||||
if (string.IsNullOrWhiteSpace(actorId)) throw new ArgumentException("Actor ID is required.", nameof(actorId));
|
||||
if (occurredAt < LastOccurredAt) throw new InvalidOperationException("Feedback transition time cannot move backwards.");
|
||||
|
||||
var transition = new ModelFeedbackTransition(State, next, reasonCode, evidenceHash, actorId, occurredAt);
|
||||
transitions.Add(transition);
|
||||
State = next;
|
||||
Revision++;
|
||||
LastOccurredAt = occurredAt;
|
||||
if (next == ModelFeedbackCycleState.Closed) ClosedAt = occurredAt;
|
||||
return transition;
|
||||
}
|
||||
|
||||
private static IReadOnlySet<ModelFeedbackCycleState> Set(params ModelFeedbackCycleState[] states)
|
||||
=> new HashSet<ModelFeedbackCycleState>(states);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace KArtSell.Modules.ModelOperations.FeedbackLoop;
|
||||
|
||||
public sealed record ModelFeedbackStage(
|
||||
int Order,
|
||||
string StageCode,
|
||||
string OperationCode,
|
||||
string Name,
|
||||
string AutomationBoundary,
|
||||
string RequiredEvidence,
|
||||
string HumanDecision,
|
||||
string FailureAction);
|
||||
|
||||
public static class ModelFeedbackPlan
|
||||
{
|
||||
public const string Boundary = "EVALUATION_AND_PROPOSAL_ONLY; NO_AUTOMATIC_MODEL_ACTIVATION; NO_AUTOMATIC_ORDER_SUBMISSION";
|
||||
|
||||
public static IReadOnlyList<ModelFeedbackStage> Stages { get; } =
|
||||
[
|
||||
new(1, "FREEZE", "J31", "PredictionFreezeRun", "EVALUATION_ONLY", "PIT cutoff, Dataset/Model/Config/Code SHA", "None", "BUSINESS_HOLD"),
|
||||
new(2, "MATURE", "J32", "OutcomeMaturitySweep", "EVALUATION_ONLY", "1/5/20/63/126/252 trading-session windows", "None", "BUSINESS_HOLD"),
|
||||
new(3, "SCORE", "J10/J11", "OutcomeEvaluationAndScorecard", "EVALUATION_ONLY", "versioned outcomes, metric definition and cohort", "None", "BUSINESS_HOLD"),
|
||||
new(4, "DIAGNOSE", "J17/J33/J34", "DriftAttributionCalibrationReview", "EVALUATION_ONLY", "drift, false-exit, gain-capture, reentry and calibration", "None", "BUSINESS_HOLD"),
|
||||
new(5, "HYPOTHESIS", "J35", "ImprovementHypothesisBuild", "PROPOSAL_ONLY", "supporting and counter evidence, falsification test", "Quant/Risk review", "CLOSE_OR_HOLD"),
|
||||
new(6, "CHALLENGER", "J36", "ChallengerExperimentPlan", "PROPOSAL_ONLY", "frozen registry, OOS plan, cost stress and stop criteria", "Independent validation approval", "CLOSE_OR_HOLD"),
|
||||
new(7, "VALIDATE", "J19/J20/J37", "FrozenOosAndIndependentValidation", "EVALUATION_AND_PROPOSAL_ONLY", "reproducibility, PBO, DSR, stability, double-cost", "Independent validator decision", "REJECT_OR_HOLD"),
|
||||
new(8, "REVIEW", "J22", "PromotionEvidenceReviewBuild", "PROPOSAL_ONLY", "complete gate pack, rollback and operational evidence", "Maker-checker investment/risk/compliance approval", "REJECT_OR_EXPIRE"),
|
||||
new(9, "ACTIVATE", "MANUAL_ONLY", "ControlledModelActivation", "AUTOMATION_FORBIDDEN", "approved effective_at, capability gate and rollback", "Separate human change approval", "KEEP_CURRENT_CHAMPION")
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace KArtSell.Modules.ModelOperations.FeedbackLoop;
|
||||
|
||||
public enum EvidenceClassification
|
||||
{
|
||||
Source,
|
||||
Assumption,
|
||||
Unknown,
|
||||
DecisionRequired
|
||||
}
|
||||
|
||||
public sealed record HypothesisEvidence(
|
||||
EvidenceClassification Classification,
|
||||
string Reference,
|
||||
string Statement,
|
||||
string ContentHash);
|
||||
|
||||
public sealed record ModelImprovementHypothesis(
|
||||
Guid HypothesisId,
|
||||
string ScopeKey,
|
||||
string BaseModelVersion,
|
||||
string ProblemStatement,
|
||||
string ProposedChange,
|
||||
string FalsificationCriterion,
|
||||
IReadOnlyList<HypothesisEvidence> SupportingEvidence,
|
||||
IReadOnlyList<HypothesisEvidence> CounterEvidence,
|
||||
string Owner,
|
||||
DateTimeOffset ExpiresAt)
|
||||
{
|
||||
public IReadOnlyList<string> Validate(DateTimeOffset now)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
if (HypothesisId == Guid.Empty) errors.Add("HYPOTHESIS_ID_REQUIRED");
|
||||
if (string.IsNullOrWhiteSpace(ScopeKey)) errors.Add("SCOPE_REQUIRED");
|
||||
if (string.IsNullOrWhiteSpace(BaseModelVersion)) errors.Add("BASE_MODEL_REQUIRED");
|
||||
if (string.IsNullOrWhiteSpace(ProblemStatement)) errors.Add("PROBLEM_REQUIRED");
|
||||
if (string.IsNullOrWhiteSpace(ProposedChange)) errors.Add("CHANGE_REQUIRED");
|
||||
if (string.IsNullOrWhiteSpace(FalsificationCriterion)) errors.Add("FALSIFICATION_TEST_REQUIRED");
|
||||
if (SupportingEvidence.Count == 0) errors.Add("SUPPORTING_EVIDENCE_REQUIRED");
|
||||
if (CounterEvidence.Count == 0) errors.Add("COUNTER_EVIDENCE_REQUIRED");
|
||||
if (SupportingEvidence.Concat(CounterEvidence).Any(x => x.Classification == EvidenceClassification.Unknown))
|
||||
errors.Add("UNKNOWN_EVIDENCE_MUST_BE_RESOLVED_OR_DOWNGRADED");
|
||||
if (SupportingEvidence.Concat(CounterEvidence).Any(x => x.Classification == EvidenceClassification.DecisionRequired))
|
||||
errors.Add("DECISION_REQUIRED_BLOCKS_EXPERIMENT");
|
||||
if (ExpiresAt <= now) errors.Add("HYPOTHESIS_EXPIRED");
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user