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
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:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user