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