feat(dotnet): migrate core formulas, deploy tools, and blazor admin web app to .NET 10
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Has been cancelled
Quant Engine CI/CD Pipeline / validate-core (pull_request) Has been cancelled
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been cancelled
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Has been cancelled

This commit is contained in:
2026-06-25 15:52:10 +09:00
parent 9abb8d3bc3
commit 2ba8def9bb
232 changed files with 10825 additions and 65 deletions
@@ -0,0 +1,43 @@
using System;
namespace QuantEngine.Core.Domain
{
public class AntiChasingResult
{
public string AntiChasingVerdict { get; set; } = "CLEAR";
public string AntiChasingVelocityStatus { get; set; } = "PASS";
public double Velocity1dInput { get; set; }
}
public static class AntiChasingCalculator
{
public static AntiChasingResult ComputeAntiChasing(double velocity1d)
{
string verdict;
string status;
if (velocity1d >= 3.0)
{
verdict = "BLOCK_CHASE";
status = "BLOCKED";
}
else if (velocity1d >= 1.5)
{
verdict = "PULLBACK_WAIT";
status = "WAIT";
}
else
{
verdict = "CLEAR";
status = "PASS";
}
return new AntiChasingResult
{
AntiChasingVerdict = verdict,
AntiChasingVelocityStatus = status,
Velocity1dInput = Math.Round(velocity1d, 4)
};
}
}
}
@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
namespace QuantEngine.Core.Domain
{
public class StopPriceResult
{
public double? StopPrice { get; set; }
public string StopPriceStatus { get; set; } = "PASS";
public double? Atr20Pct { get; set; }
public double? AtrMultiplier { get; set; }
public List<string> DataMissing { get; set; } = new List<string>();
}
public class StopActionLadderResult
{
public string Action { get; set; } = "REVIEW_HUMAN";
public string Reason { get; set; } = "NO_FORCED_EXIT";
public double QuantityPct { get; set; }
public double Priority { get; set; } = 7;
}
public class HeatThresholdResult
{
public double HardBlock { get; set; }
public double Halve { get; set; }
}
public static class ExitDecisions
{
public static StopPriceResult ComputeStopPriceCore(
double? entryPrice,
double? atr20,
double? currentPrice = null,
double? atrMultiplier = null)
{
var result = new StopPriceResult();
if (!entryPrice.HasValue)
{
result.StopPrice = null;
result.StopPriceStatus = "NO_STOP_PRICE";
result.DataMissing.Add("entry_price");
return result;
}
if (!atr20.HasValue && !atrMultiplier.HasValue)
{
result.StopPrice = entryPrice.Value * 0.92;
result.StopPriceStatus = "DATA_MISSING — 하네스 업데이트 필요";
result.DataMissing.Add("atr20");
return result;
}
if (!atrMultiplier.HasValue && (!currentPrice.HasValue || currentPrice.Value == 0))
{
result.StopPrice = entryPrice.Value * 0.92;
result.StopPriceStatus = "DATA_MISSING — 하네스 업데이트 필요";
if (!atr20.HasValue) result.DataMissing.Add("atr20");
if (!currentPrice.HasValue || currentPrice.Value == 0) result.DataMissing.Add("current_price");
return result;
}
if (!atrMultiplier.HasValue)
{
double atr20Pct = (atr20!.Value / currentPrice!.Value) * 100;
atrMultiplier = atr20Pct >= 8 ? 2.0 : 1.5;
result.Atr20Pct = atr20Pct;
}
else
{
result.Atr20Pct = (currentPrice.HasValue && currentPrice.Value != 0)
? (atr20!.Value / currentPrice.Value) * 100
: (double?)null;
}
result.AtrMultiplier = atrMultiplier;
result.StopPrice = Math.Max(entryPrice.Value * 0.92, entryPrice.Value - atr20!.Value * atrMultiplier.Value);
result.StopPriceStatus = "PASS";
return result;
}
public static StopActionLadderResult ComputeStopActionLadder(Dictionary<string, object> context)
{
string GetString(string key) =>
context.TryGetValue(key, out var val) ? val?.ToString()?.ToUpper() ?? "" : "";
double GetDouble(string key) =>
context.TryGetValue(key, out var val) && double.TryParse(val?.ToString(), out var d) ? d : 0.0;
int GetInt(string key, int def = 0) =>
context.TryGetValue(key, out var val) && int.TryParse(val?.ToString(), out var i) ? i : def;
bool GetBool(string key) =>
context.TryGetValue(key, out var val) && bool.TryParse(val?.ToString(), out var b) && b;
string timingAction = GetString("timingAction");
if (string.IsNullOrEmpty(timingAction)) timingAction = GetString("timing_action");
int rwPartial = GetInt("rw_partial");
int rwPartialExcludingRw2b = GetInt("rw_partial_excluding_rw2b");
string regimePrelim = GetString("REGIME_PRELIM");
if (string.IsNullOrEmpty(regimePrelim)) regimePrelim = GetString("regime_prelim");
double timingExitScore = GetDouble("timingExitScore");
if (timingExitScore == 0) timingExitScore = GetDouble("timing_exit_score");
double profitPct = GetDouble("profitPct");
if (profitPct == 0) profitPct = GetDouble("profit_pct");
int daysToTimeStop = GetInt("daysToTimeStop", 9999);
if (daysToTimeStop == 9999) daysToTimeStop = GetInt("days_to_time_stop", 9999);
bool trailingStopBreach = GetBool("trailingStopBreach") || GetBool("trailing_stop_breach");
bool rw2bFastTrack = GetBool("RW2b_5d_rapid_weakness") || GetBool("rw2b_5d_rapid_weakness");
if (timingAction == "STOP_OR_TIME_EXIT_READY" || rwPartial >= 4)
{
return new StopActionLadderResult
{
Action = "EXIT_100",
Reason = timingAction == "STOP_OR_TIME_EXIT_READY" ? "STOP_OR_TIME_EXIT_READY" : "RW_EXIT_STRONG",
QuantityPct = 100,
Priority = 1
};
}
if (regimePrelim == "RISK_OFF" || regimePrelim == "RISK_OFF_CANDIDATE")
{
return new StopActionLadderResult
{
Action = "REGIME_TRIM_50",
Reason = regimePrelim == "RISK_OFF" ? "REGIME_RISK_OFF" : "REGIME_RISK_OFF_CANDIDATE",
QuantityPct = 50,
Priority = 2
};
}
if (rw2bFastTrack && rwPartialExcludingRw2b >= 1)
{
return new StopActionLadderResult
{
Action = "TRIM_50",
Reason = "RW2B_FAST_TRACK",
QuantityPct = 50,
Priority = 2.5
};
}
if (rwPartial >= 3 || timingExitScore >= 75)
{
return new StopActionLadderResult
{
Action = "TRIM_70",
Reason = rwPartial >= 3 ? "RW_EXIT" : "TIMING_EXIT_SCORE",
QuantityPct = 70,
Priority = 3
};
}
if (trailingStopBreach || rwPartial >= 2 || (rwPartial >= 1 && timingExitScore >= 50))
{
return new StopActionLadderResult
{
Action = "TRIM_50",
Reason = trailingStopBreach ? "TRAILING_STOP_BREACH" : "RW_OR_TIMING_EXIT",
QuantityPct = 50,
Priority = 4
};
}
if (profitPct >= 10)
{
return new StopActionLadderResult
{
Action = "TAKE_PROFIT_TIER1",
Reason = "PROFIT_PCT_THRESHOLD",
QuantityPct = 25,
Priority = 5
};
}
if (daysToTimeStop <= 0)
{
return new StopActionLadderResult
{
Action = "TIME_EXIT_100",
Reason = "TIME_STOP_EXPIRED",
QuantityPct = 100,
Priority = 6
};
}
return new StopActionLadderResult();
}
public static HeatThresholdResult ComputeDynamicHeatThresholds(string regime)
{
string r = regime?.ToUpper() ?? "";
if (r.Contains("EVENT_SHOCK"))
{
return new HeatThresholdResult { HardBlock = 5.0, Halve = 3.5 };
}
if (r.Contains("RISK_OFF"))
{
return new HeatThresholdResult { HardBlock = 7.0, Halve = 5.0 };
}
if (r.Contains("SECULAR_LEADER") && r.Contains("RISK_ON"))
{
return new HeatThresholdResult { HardBlock = 13.0, Halve = 9.0 };
}
if (r.Contains("RISK_ON"))
{
return new HeatThresholdResult { HardBlock = 12.0, Halve = 8.5 };
}
return new HeatThresholdResult { HardBlock = 10.0, Halve = 7.0 };
}
}
}
@@ -0,0 +1,49 @@
using System;
namespace QuantEngine.Core.Domain
{
public static class KrxTickNormalizer
{
private static readonly (double Limit, int Tick)[] KrxTickTable = new (double, int)[]
{
(2000, 1),
(5000, 5),
(20000, 10),
(50000, 50),
(200000, 100),
(500000, 500),
(double.PositiveInfinity, 1000)
};
public static int GetTickUnit(double price)
{
foreach (var (limit, tick) in KrxTickTable)
{
if (price < limit)
{
return tick;
}
}
return 1000;
}
public static double NormalizeTick(double price)
{
if (price <= 0) return 0;
int tick = GetTickUnit(price);
// Round to nearest tick unit
double remainder = price % tick;
if (remainder == 0) return price;
if (remainder >= tick / 2.0)
{
return price + (tick - remainder);
}
else
{
return price - remainder;
}
}
}
}
@@ -0,0 +1,66 @@
using System;
namespace QuantEngine.Core.Domain
{
public class ProfitLockResult
{
public string RatchetStage { get; set; } = "NORMAL";
public double? AutoTrailingStop { get; set; }
public string TpLadderAction { get; set; } = "적용 안함";
public bool ApexSuperActive { get; set; }
}
public static class ProfitLockCalculator
{
public static string ClassifyProfitLockStage(double profitPct)
{
if (profitPct >= 60) return "APEX_SUPER";
if (profitPct >= 40) return "APEX_TRAILING";
if (profitPct >= 30) return "PROFIT_LOCK_30";
if (profitPct >= 20) return "PROFIT_LOCK_20";
if (profitPct >= 10) return "PROFIT_LOCK_10";
if (profitPct >= 0) return "BREAKEVEN_RATCHET";
return "NORMAL";
}
public static ProfitLockResult ComputeTrailingStop(
double profitPct,
double highestClose,
double atr20,
double? ratchetStop,
double averageCost)
{
string stage = ClassifyProfitLockStage(profitPct);
double baseStop = ratchetStop ?? averageCost;
double? trailingStop = null;
string tpAction = "적용 안함";
if (stage == "APEX_SUPER")
{
double raw = highestClose - 1.2 * atr20;
trailingStop = KrxTickNormalizer.NormalizeTick(Math.Max(baseStop, raw));
tpAction = "강제 10% 익절 권고";
}
else if (stage == "APEX_TRAILING")
{
double raw = highestClose - 1.5 * atr20;
trailingStop = KrxTickNormalizer.NormalizeTick(Math.Max(baseStop, raw));
tpAction = "부분익절 검토";
}
else if (stage == "PROFIT_LOCK_30" || stage == "PROFIT_LOCK_20")
{
double raw = highestClose - 2.0 * atr20;
trailingStop = KrxTickNormalizer.NormalizeTick(Math.Max(baseStop, raw));
tpAction = "래칫 유지";
}
return new ProfitLockResult
{
RatchetStage = stage,
AutoTrailingStop = trailingStop,
TpLadderAction = tpAction,
ApexSuperActive = stage == "APEX_SUPER"
};
}
}
}
@@ -0,0 +1,42 @@
using System;
namespace QuantEngine.Core.Domain
{
public class PullbackTriggerResult
{
public string PullbackEntryVerdict { get; set; } = "ABOVE_PULLBACK_ZONE";
public string PullbackState { get; set; } = "BLOCKED";
public double PullbackEntryTriggerPrice { get; set; }
public double PullbackUpperBand { get; set; }
}
public static class PullbackTriggerCalculator
{
public static PullbackTriggerResult ComputePullbackTrigger(double close, double ma20, double atr20)
{
double triggerPrice = KrxTickNormalizer.NormalizeTick(ma20 - 0.5 * atr20);
double upperBand = ma20 * 1.03;
string verdict;
string state;
if (close <= upperBand)
{
verdict = "PULLBACK_ZONE";
state = "PASS";
}
else
{
verdict = "ABOVE_PULLBACK_ZONE";
state = "BLOCKED";
}
return new PullbackTriggerResult
{
PullbackEntryVerdict = verdict,
PullbackState = state,
PullbackEntryTriggerPrice = triggerPrice,
PullbackUpperBand = KrxTickNormalizer.NormalizeTick(upperBand)
};
}
}
}
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
namespace QuantEngine.Core.Domain
{
public class SellPriceSanityResult
{
public string SellPriceSanityStatus { get; set; } = "PASS";
public List<string> SellPriceSanityIssues { get; set; } = new List<string>();
public bool HtsAllowed { get; set; } = true;
public bool ShadowLedger { get; set; }
public string Ticker { get; set; } = string.Empty;
}
public static class SellPriceSanityChecker
{
public static SellPriceSanityResult CheckSellPriceSanity(
double sellLimitPrice,
double? stopLossPrice,
double prevClose,
string ticker = "")
{
var issues = new List<string>();
string status = "PASS";
if (stopLossPrice.HasValue && sellLimitPrice < stopLossPrice.Value)
{
issues.Add($"INVALID_PRICE_INVERSION: sell={sellLimitPrice:N0} < stop={stopLossPrice.Value:N0}");
status = "INVALID_PRICE_INVERSION";
}
double upperLimit = prevClose * 1.30;
if (sellLimitPrice > upperLimit)
{
issues.Add($"INVALID_UNREALISTIC_PRICE: sell={sellLimitPrice:N0} > prev_close*1.30={upperLimit:N0}");
if (status == "PASS")
{
status = "INVALID_UNREALISTIC_PRICE";
}
}
int tickUnit = KrxTickNormalizer.GetTickUnit(sellLimitPrice);
if (sellLimitPrice % tickUnit != 0)
{
double corrected = KrxTickNormalizer.NormalizeTick(sellLimitPrice);
issues.Add($"INVALID_TICK: sell={sellLimitPrice:N0} 호가단위={tickUnit}원 → 정규화={corrected:N0}");
if (status == "PASS")
{
status = "INVALID_TICK";
}
}
return new SellPriceSanityResult
{
SellPriceSanityStatus = status,
SellPriceSanityIssues = issues,
HtsAllowed = status == "PASS",
ShadowLedger = status != "PASS",
Ticker = ticker
};
}
}
}