153 lines
5.8 KiB
C#
153 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Xunit;
|
|
using QuantEngine.Core.Domain;
|
|
using QuantEngine.Core.Interfaces;
|
|
|
|
namespace QuantEngine.Core.Tests
|
|
{
|
|
public class FactorCalculatorTests
|
|
{
|
|
private List<PriceHistoryDailyRecord> CreateMockBars(string ticker, double startPrice, double trend, int count)
|
|
{
|
|
var list = new List<PriceHistoryDailyRecord>();
|
|
var startDate = new DateOnly(2026, 1, 1);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
double price = startPrice + (i * trend);
|
|
list.Add(new PriceHistoryDailyRecord(
|
|
ticker,
|
|
startDate.AddDays(i),
|
|
(decimal)price,
|
|
(decimal)(price + 2.0),
|
|
(decimal)(price - 2.0),
|
|
(decimal)price,
|
|
100000,
|
|
"TEST_SOURCE"
|
|
));
|
|
}
|
|
return list;
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateFactors_EmptyStockBars_ReturnsAllZeros()
|
|
{
|
|
var stock = new List<PriceHistoryDailyRecord>();
|
|
var index = new List<PriceHistoryDailyRecord>();
|
|
|
|
var outputs = FactorCalculator.CalculateFactors(stock, index);
|
|
|
|
Assert.Equal(0, outputs.Momentum20D);
|
|
Assert.Equal(0, outputs.Momentum60D);
|
|
Assert.Equal(0, outputs.Momentum120D);
|
|
Assert.Equal(0, outputs.Atr20Pct);
|
|
Assert.Equal(0, outputs.StDev20D);
|
|
Assert.Equal(1.0, outputs.Beta60D); // Beta defaults to 1.0 on short data
|
|
Assert.Equal(0, outputs.Rs20D);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateFactors_ConstantTrend_CalculatesCorrectMomentum()
|
|
{
|
|
// Stock starting at 100.0, rising 1.0 every day for 130 days.
|
|
// On day 130 (index 129), price = 100 + 129 = 229.
|
|
// Close[129] = 229.
|
|
// Close[129-20] = Close[109] = 100 + 109 = 209.
|
|
// Momentum 20D = ((229 - 209) / 209) * 100 = (20 / 209) * 100 = 9.5693%
|
|
var stock = CreateMockBars("005930", 100.0, 1.0, 130);
|
|
var index = CreateMockBars("KOSPI", 2000.0, 0.0, 130); // Constant index
|
|
|
|
var outputs = FactorCalculator.CalculateFactors(stock, index);
|
|
|
|
double expectedMom20 = (20.0 / 209.0) * 100.0;
|
|
Assert.Equal(expectedMom20, outputs.Momentum20D, 5); // 5 decimals precision
|
|
|
|
double expectedMom60 = (60.0 / 169.0) * 100.0;
|
|
Assert.Equal(expectedMom60, outputs.Momentum60D, 5);
|
|
|
|
double expectedMom120 = (120.0 / 109.0) * 100.0;
|
|
Assert.Equal(expectedMom120, outputs.Momentum120D, 5);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateAtr20Pct_ConstantHighLowDifference_CalculatesCorrectAtrPct()
|
|
{
|
|
// Create stock where High - Low = 4.0 consistently, and close doesn't gap.
|
|
// TR = High - Low = 4.0.
|
|
// ATR 20D = Average TR over last 20 days = 4.0.
|
|
// Final close price = 100 + 129 = 229.
|
|
// ATR% = (4.0 / 229.0) * 100 = 1.7467%
|
|
var stock = CreateMockBars("005930", 100.0, 1.0, 130);
|
|
var index = CreateMockBars("KOSPI", 2000.0, 0.0, 130);
|
|
|
|
var outputs = FactorCalculator.CalculateFactors(stock, index);
|
|
|
|
double expectedAtrPct = (4.0 / 229.0) * 100.0;
|
|
Assert.Equal(expectedAtrPct, outputs.Atr20Pct, 5);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculatePriceStDev20D_CalculatesCorrectStDev()
|
|
{
|
|
// Close values over last 20 days: 210, 211, ..., 229.
|
|
// Average = 219.5
|
|
// Variance = Sum(x_i - Avg)^2 / 19
|
|
var stock = CreateMockBars("005930", 100.0, 1.0, 130);
|
|
var index = CreateMockBars("KOSPI", 2000.0, 0.0, 130);
|
|
|
|
var outputs = FactorCalculator.CalculateFactors(stock, index);
|
|
|
|
// Manual stdev calculation for sequential 20 numbers: stdev = sqrt( (20^2 - 1) * d^2 / 12 * N / (N-1) )?
|
|
// stdev of 20 numbers with step 1: sqrt(35) * sqrt(20/19) ≈ 5.91608 * 1.02598 ≈ 6.0697
|
|
// Actual check using double math
|
|
double sum = 0;
|
|
for (int i = 110; i < 130; i++) sum += (100.0 + i);
|
|
double avg = sum / 20.0;
|
|
double sumSquares = 0;
|
|
for (int i = 110; i < 130; i++) sumSquares += Math.Pow((100.0 + i) - avg, 2);
|
|
double expectedStDev = Math.Sqrt(sumSquares / 19.0);
|
|
|
|
Assert.Equal(expectedStDev, outputs.StDev20D, 5);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateFactors_UnsortedInput_ProducesSameOutputs()
|
|
{
|
|
var stock = CreateMockBars("005930", 100.0, 1.0, 130);
|
|
var index = CreateMockBars("KOSPI", 2000.0, 0.0, 130);
|
|
|
|
stock.Reverse();
|
|
index.Reverse();
|
|
|
|
var outputs = FactorCalculator.CalculateFactors(stock, index);
|
|
|
|
double expectedMom20 = (20.0 / 209.0) * 100.0;
|
|
Assert.Equal(expectedMom20, outputs.Momentum20D, 5);
|
|
Assert.Equal(1.0, outputs.Beta60D);
|
|
}
|
|
|
|
[Fact]
|
|
public void CalculateFactors_DuplicateDates_UsesLastOccurrencePerDate()
|
|
{
|
|
var stock = CreateMockBars("005930", 100.0, 1.0, 130);
|
|
var index = CreateMockBars("KOSPI", 2000.0, 0.0, 130);
|
|
|
|
var duplicateDate = stock[129].TradeDate;
|
|
stock.Add(new PriceHistoryDailyRecord(
|
|
"005930",
|
|
duplicateDate,
|
|
1000m,
|
|
1002m,
|
|
998m,
|
|
1001m,
|
|
100000,
|
|
"TEST_SOURCE"));
|
|
|
|
var outputs = FactorCalculator.CalculateFactors(stock, index);
|
|
|
|
Assert.True(outputs.Momentum20D > 0);
|
|
Assert.True(outputs.Atr20Pct > 0);
|
|
}
|
|
}
|
|
}
|