Files
KArtSell.Aegis/tests/KArtSell.Integration.Tests/SignalGenerationTests.cs
T
kjh2064 220e646a4b feat: Implement EMA crossover signal generation for Phase 2 gates optimization
- Added CalculateEMA() method to ReplayEngine for 12/26-day exponential moving average
- Updated GenerateSignalsAsync() to emit Buy/Sell signals when EMA12 crosses EMA26
- Added 0.1% threshold to avoid noise and excessive trading
- Signal confidence set to 0.75m with clear rationale for traceability
- New SignalGenerationTests to verify signal generation on trending data
- Fixes: signals were empty (0 signals/orders/returns), now generates trade signals
- Result: Phase 2 metrics should now be non-zero (orders, returns, metrics)
- AGENTS.md v16.0: Necessity-driven (unblocks Phase 3), Simple logic, Reliability tested

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-12 15:50:23 +09:00

84 lines
2.9 KiB
C#

using Xunit;
using KArtSell.BuildingBlocks.Time;
using KArtSell.Modules.ModelOperations.ShadowRun;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace KArtSell.Integration.Tests;
public sealed class SignalGenerationTests
{
private readonly ILogger<ReplayEngine> _logger = new NoOpLogger<ReplayEngine>();
[Fact]
public async Task GenerateSignals_EMA_ProducesTradeSignals()
{
// Arrange
var replay = new ReplayEngine(_logger);
// Create OHLCV bars with clear trend
var bars = new List<DataBackfiller.OhlcvBar>();
// Generate uptrend (days 1-30): price goes from 2500 to 2600
for (int i = 1; i <= 30; i++)
{
var price = 2500m + (i * 3.33m); // Linear uptrend
bars.Add(new DataBackfiller.OhlcvBar(
new DateOnly(2026, 1, i),
"KOSPI",
price - 10, // open
price + 10, // high
price - 15, // low
price, // close
1_000_000L));
}
// Downtrend (days 31-45): price goes from 2600 down to 2500
for (int i = 31; i <= 45; i++)
{
var price = 2600m - ((i - 30) * 6.67m); // Linear downtrend
bars.Add(new DataBackfiller.OhlcvBar(
new DateOnly(2026, 2, i - 30),
"KOSPI",
price - 10,
price + 10,
price - 15,
price,
1_000_000L));
}
var fees = new List<DataBackfiller.FeeScheduleEntry>
{
new(new DateOnly(2026, 1, 1), 0.001m, 0.0005m),
};
var sessions = bars.Select(b => b.Date).Distinct().OrderBy(d => d).ToList();
// Act
var result = await replay.ReplayAsync(
Guid.NewGuid(), bars, fees, 10_000_000m, sessions, CancellationToken.None);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result.Signals); // Should have signals from trend
// Verify signal generation happened
var buySignals = result.Signals.Where(s => s.Action == ReplayEngine.SignalAction.Buy).ToList();
var sellSignals = result.Signals.Where(s => s.Action == ReplayEngine.SignalAction.Sell).ToList();
Assert.NotEmpty(buySignals); // Should have BUY signals during uptrend
Assert.NotEmpty(sellSignals); // Should have SELL signals during downtrend
}
private sealed class NoOpLogger<T> : ILogger<T>
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => false;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter) { }
}
}