104 lines
4.6 KiB
C#
104 lines
4.6 KiB
C#
using System.Globalization;
|
|
|
|
namespace QuantEngine.Core.Domain;
|
|
|
|
/// <summary>
|
|
/// Canonical .NET implementations for formula IDs that were previously only
|
|
/// represented by legacy harness anchors. Inputs are supplied by the harness;
|
|
/// missing inputs produce DATA_MISSING rather than invented values.
|
|
/// </summary>
|
|
public static class FormulaCanonicalCoverage
|
|
{
|
|
public static Dictionary<string, object?> AntiChaseV1(IReadOnlyDictionary<string, object?> input)
|
|
{
|
|
var velocity = Number(input, "velocity_1d");
|
|
var threshold = Number(input, "velocity_threshold");
|
|
if (!velocity.HasValue || !threshold.HasValue)
|
|
return Missing("ANTI_CHASE_V1");
|
|
return Result("ANTI_CHASE_V1", velocity.Value > threshold.Value ? "BLOCK" : "PASS", velocity.Value);
|
|
}
|
|
|
|
public static Dictionary<string, object?> CashRecoveryV1(IReadOnlyDictionary<string, object?> input)
|
|
{
|
|
var shortfall = Number(input, "cash_shortfall_krw");
|
|
var recovered = Number(input, "recovered_krw");
|
|
if (!shortfall.HasValue || !recovered.HasValue)
|
|
return Missing("CASH_RECOVERY_V1");
|
|
return Result("CASH_RECOVERY_V1", recovered.Value >= shortfall.Value ? "PASS" : "LIMITED", recovered.Value);
|
|
}
|
|
|
|
public static Dictionary<string, object?> ComprehensiveProposalV1(IReadOnlyDictionary<string, object?> input)
|
|
=> GateFromInputs("COMPREHENSIVE_PROPOSAL_V1", input, "proposal_gate");
|
|
|
|
public static Dictionary<string, object?> DfgV1(IReadOnlyDictionary<string, object?> input)
|
|
=> GateFromInputs("DFG_V1", input, "cycle_detected", invert: true);
|
|
|
|
public static Dictionary<string, object?> IntradayV1(IReadOnlyDictionary<string, object?> input)
|
|
=> GateFromInputs("INTRADAY_V1", input, "intraday_restriction_gate");
|
|
|
|
public static Dictionary<string, object?> PortfolioHealthV1(IReadOnlyDictionary<string, object?> input)
|
|
=> GateFromInputs("PORTFOLIO_HEALTH_V1", input, "portfolio_health_label");
|
|
|
|
public static Dictionary<string, object?> RsV2Fusion(IReadOnlyDictionary<string, object?> input)
|
|
{
|
|
var rs = Number(input, "rs_v2_score");
|
|
var technical = Number(input, "technical_score");
|
|
if (!rs.HasValue || !technical.HasValue)
|
|
return Missing("RS_V2_FUSION");
|
|
var score = (rs.Value + technical.Value) / 2d;
|
|
return Result("RS_V2_FUSION", score >= 0 ? "PASS" : "BLOCK", score);
|
|
}
|
|
|
|
public static Dictionary<string, object?> StopBreachV1(IReadOnlyDictionary<string, object?> input)
|
|
{
|
|
var current = Number(input, "current_price");
|
|
var stop = Number(input, "stop_loss_price");
|
|
var gap = Number(input, "gap_threshold");
|
|
if (!current.HasValue || !stop.HasValue || !gap.HasValue || stop.Value == 0)
|
|
return Missing("STOP_BREACH_V1");
|
|
var gapPct = (stop.Value - current.Value) / stop.Value;
|
|
return Result("STOP_BREACH_V1", gapPct >= gap.Value ? "BREACH_IMMEDIATE_EXIT" : "PASS", gapPct);
|
|
}
|
|
|
|
public static Dictionary<string, object?> TickNormV1(IReadOnlyDictionary<string, object?> input)
|
|
{
|
|
var price = Number(input, "price");
|
|
if (!price.HasValue)
|
|
return Missing("TICK_NORM_V1");
|
|
return Result("TICK_NORM_V1", "PASS", KrxTickNormalizer.NormalizeTick(price.Value));
|
|
}
|
|
|
|
private static Dictionary<string, object?> GateFromInputs(string id, IReadOnlyDictionary<string, object?> input, string field, bool invert = false)
|
|
{
|
|
if (!input.TryGetValue(field, out var value) || value is null)
|
|
return Missing(id);
|
|
var blocked = string.Equals(value.ToString(), "BLOCK", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(value.ToString(), "true", StringComparison.OrdinalIgnoreCase);
|
|
if (invert) blocked = !blocked;
|
|
return Result(id, blocked ? "BLOCK" : "PASS", null);
|
|
}
|
|
|
|
private static Dictionary<string, object?> Missing(string id) => new()
|
|
{
|
|
["formula_id"] = id,
|
|
["gate"] = "DATA_MISSING — 하네스 업데이트 필요",
|
|
["value"] = null,
|
|
};
|
|
|
|
private static Dictionary<string, object?> Result(string id, string gate, double? value) => new()
|
|
{
|
|
["formula_id"] = id,
|
|
["gate"] = gate,
|
|
["value"] = value,
|
|
};
|
|
|
|
private static double? Number(IReadOnlyDictionary<string, object?> input, string key)
|
|
{
|
|
if (!input.TryGetValue(key, out var value) || value is null)
|
|
return null;
|
|
return double.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
|
|
? parsed
|
|
: null;
|
|
}
|
|
}
|