d1278b26ee
Deploy to Production / Build Release Package (push) Failing after 17s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 5s
Deploy to Production / Deploy to Production Server (push) Has been skipped
Deploy to Production / Post-Deployment Checks (push) Has been skipped
Snapshot Admin Deployment / build-and-deploy (push) Failing after 38s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 2m19s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
102 lines
4.0 KiB
C#
102 lines
4.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Xunit;
|
|
using QuantEngine.Core.Domain;
|
|
|
|
namespace QuantEngine.Core.Tests
|
|
{
|
|
public class ExitDecisionsTests
|
|
{
|
|
[Fact]
|
|
public void ComputeStopPriceCore_AtrBased_ReturnsCorrectPrice()
|
|
{
|
|
// ATR 2.0배 기반 계산 검증
|
|
var res = ExitDecisions.ComputeStopPriceCore(
|
|
entryPrice: 100000,
|
|
atr20: 3000,
|
|
currentPrice: 100000,
|
|
atrMultiplier: 2.0
|
|
);
|
|
|
|
Assert.Equal("PASS", res.StopPriceStatus);
|
|
Assert.Equal(94000, res.StopPrice); // 100000 - 3000 * 2.0 = 94000
|
|
}
|
|
|
|
[Fact]
|
|
public void ComputeStopPriceCore_FallbackPrice_Returns8PercentDown()
|
|
{
|
|
// 결측인 경우 8% 하락 폴백 가격으로 설정 검증
|
|
var res = ExitDecisions.ComputeStopPriceCore(
|
|
entryPrice: 100000,
|
|
atr20: null,
|
|
currentPrice: null,
|
|
atrMultiplier: null
|
|
);
|
|
|
|
Assert.Contains("DATA_MISSING", res.StopPriceStatus);
|
|
Assert.Equal(92000, res.StopPrice); // 100000 * 0.92 = 92000
|
|
}
|
|
|
|
[Fact]
|
|
public void ComputeStopPriceCore_AtrPercentBased_SetsCorrectMultiplier()
|
|
{
|
|
// ATR 비율에 따른 동적 승수 선택 검증 (atr20=10000, current=100000 -> atr20Pct = 10% >= 8% -> multiplier = 2.0)
|
|
var res = ExitDecisions.ComputeStopPriceCore(
|
|
entryPrice: 100000,
|
|
atr20: 10000,
|
|
currentPrice: 100000,
|
|
atrMultiplier: null
|
|
);
|
|
|
|
Assert.Equal("PASS", res.StopPriceStatus);
|
|
Assert.Equal(2.0, res.AtrMultiplier);
|
|
Assert.Equal(92000, res.StopPrice); // Max(92000, 100000 - 10000 * 2.0) = Max(92000, 80000) = 92000
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("STOP_OR_TIME_EXIT_READY", 4, "EXIT_100", "STOP_OR_TIME_EXIT_READY")]
|
|
[InlineData("NORMAL_TRADING", 4, "EXIT_100", "RW_EXIT_STRONG")]
|
|
[InlineData("NORMAL_TRADING", 1, "REGIME_TRIM_50", "REGIME_RISK_OFF")] // REGIME_PRELIM="RISK_OFF"
|
|
[InlineData("NORMAL_TRADING", 1, "TRIM_70", "TIMING_EXIT_SCORE")] // timingExitScore = 75
|
|
[InlineData("NORMAL_TRADING", 1, "TRIM_50", "TRAILING_STOP_BREACH")] // trailingStopBreach = true
|
|
[InlineData("NORMAL_TRADING", 0, "TIME_EXIT_100", "TIME_STOP_EXPIRED")] // daysToTimeStop = 0
|
|
public void ComputeStopActionLadder_Scenarios_ReturnExpectedAction(
|
|
string timingAction,
|
|
int rwPartial,
|
|
string expectedAction,
|
|
string expectedReason)
|
|
{
|
|
var ctx = new Dictionary<string, object>
|
|
{
|
|
{ "timingAction", timingAction },
|
|
{ "rw_partial", rwPartial },
|
|
{ "REGIME_PRELIM", expectedReason == "REGIME_RISK_OFF" ? "RISK_OFF" : "RISK_ON" },
|
|
{ "timingExitScore", expectedReason == "TIMING_EXIT_SCORE" ? 75.0 : 0.0 },
|
|
{ "trailingStopBreach", expectedReason == "TRAILING_STOP_BREACH" },
|
|
{ "daysToTimeStop", expectedReason == "TIME_STOP_EXPIRED" ? 0 : 9999 }
|
|
};
|
|
|
|
var res = ExitDecisions.ComputeStopActionLadder(ctx);
|
|
|
|
Assert.Equal(expectedAction, res.Action);
|
|
Assert.Equal(expectedReason, res.Reason);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("EVENT_SHOCK", 5.0, 3.5)]
|
|
[InlineData("RISK_OFF", 7.0, 5.0)]
|
|
[InlineData("SECULAR_LEADER_RISK_ON", 13.0, 9.0)]
|
|
[InlineData("RISK_ON", 12.0, 8.5)]
|
|
[InlineData("NEUTRAL", 10.0, 7.0)]
|
|
public void ComputeDynamicHeatThresholds_Regimes_ReturnCorrectThresholds(
|
|
string regime,
|
|
double expectedHard,
|
|
double expectedHalve)
|
|
{
|
|
var res = ExitDecisions.ComputeDynamicHeatThresholds(regime);
|
|
Assert.Equal(expectedHard, res.HardBlock);
|
|
Assert.Equal(expectedHalve, res.Halve);
|
|
}
|
|
}
|
|
}
|