160 lines
6.8 KiB
C#
160 lines
6.8 KiB
C#
using QuantEngine.Application.Services;
|
|
using QuantEngine.Core.Interfaces;
|
|
using QuantEngine.Core.Models;
|
|
|
|
namespace QuantEngine.Core.Tests;
|
|
|
|
public class ApplicationServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task WorkspaceService_ForwardsSettingAndHistoryOperations()
|
|
{
|
|
var repo = new FakeWorkspaceRepository();
|
|
var history = new FakeHistoryStore();
|
|
var service = new WorkspaceService(repo, history);
|
|
|
|
var setting = new Setting { Ordinal = 1, Key = "risk_mode", ValueJson = "\"RISK_ON\"" };
|
|
Assert.True(await service.UpsertSettingAsync(setting));
|
|
Assert.Equal(setting, repo.LastSetting);
|
|
|
|
var payload = new Dictionary<string, object?> { ["foo"] = "bar" };
|
|
Assert.Equal(1, await service.AppendHistoryAsync("decision_result_history", payload));
|
|
Assert.Equal("decision_result_history", history.LastDomain);
|
|
Assert.Equal("bar", history.LastPayload?["foo"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ApprovalService_ForwardsApprovalAndLockOperations()
|
|
{
|
|
var repo = new FakeWorkspaceRepository();
|
|
var service = new ApprovalService(repo);
|
|
|
|
var approval = new WorkspaceApproval { Domain = "settings", TargetRef = "portfolio", Status = "APPROVED" };
|
|
Assert.True(await service.UpsertApprovalAsync(approval));
|
|
Assert.Equal(approval, repo.LastApproval);
|
|
|
|
var lockRow = new WorkspaceLock { Domain = "settings", TargetRef = "portfolio", LockedBy = "qa", Reason = "review" };
|
|
Assert.True(await service.AcquireLockAsync(lockRow));
|
|
Assert.Equal(lockRow, repo.LastLock);
|
|
Assert.True(await service.ReleaseLockAsync("settings", "portfolio"));
|
|
Assert.Equal(("settings", "portfolio"), repo.LastReleasedLock);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CollectionService_AppendsRunSnapshotAndErrorRecords()
|
|
{
|
|
var history = new FakeHistoryStore();
|
|
var service = new CollectionService(history);
|
|
|
|
await service.AppendRunAsync(new CollectionRun
|
|
{
|
|
RunId = "run-1",
|
|
CollectorName = "kis",
|
|
StartedAt = "2026-06-26T09:00:00+09:00",
|
|
Status = "PASS"
|
|
});
|
|
|
|
Assert.Equal("collection_run_history", history.LastDomain);
|
|
Assert.Equal("run-1", history.LastPayload?["run_id"]);
|
|
|
|
await service.AppendSnapshotAsync(new CollectionSnapshot
|
|
{
|
|
RunId = "run-1",
|
|
DatasetName = "decision_result_history",
|
|
Ticker = "005930",
|
|
SourcePriority = "KIS",
|
|
SourceStatus = "PASS",
|
|
PayloadJson = "{}",
|
|
ProvenanceJson = "{}"
|
|
});
|
|
|
|
Assert.Equal("collection_snapshot_history", history.LastDomain);
|
|
Assert.Equal("005930", history.LastPayload?["ticker"]);
|
|
|
|
await service.AppendSourceErrorAsync(new CollectionSourceError
|
|
{
|
|
RunId = "run-1",
|
|
SourceName = "naver",
|
|
ErrorKind = "TIMEOUT",
|
|
ErrorMessage = "timeout"
|
|
});
|
|
|
|
Assert.Equal("collection_source_error_history", history.LastDomain);
|
|
Assert.Equal("TIMEOUT", history.LastPayload?["error_kind"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FormulaService_ForwardsFormulaExecutionAndHistory()
|
|
{
|
|
var history = new FakeHistoryStore();
|
|
var service = new FormulaService(history);
|
|
|
|
var timing = service.ComputeTimingDecision(new Dictionary<string, object>
|
|
{
|
|
["entryModeGate"] = "PASS",
|
|
["entryMode"] = "BREAKOUT",
|
|
["leaderGate"] = "PASS",
|
|
["acGate"] = "CLEAR",
|
|
["priceStatus"] = "PRICE_OK",
|
|
["atr20"] = 1.0,
|
|
["leaderTotal"] = 4,
|
|
["flowCredit"] = 0.7,
|
|
["avgTradeValue5D"] = 100,
|
|
["spreadPct"] = 0.5
|
|
});
|
|
|
|
Assert.NotEqual(string.Empty, timing.Action);
|
|
|
|
await service.AppendFormulaRunAsync("timing", new Dictionary<string, object?>
|
|
{
|
|
["action"] = timing.Action,
|
|
["entry_score"] = timing.EntryScore
|
|
});
|
|
|
|
Assert.Equal("formula_timing_history", history.LastDomain);
|
|
Assert.Equal(timing.Action, history.LastPayload?["action"]);
|
|
}
|
|
|
|
private sealed class FakeWorkspaceRepository : IWorkspaceRepository
|
|
{
|
|
public Setting? LastSetting { get; private set; }
|
|
public WorkspaceApproval? LastApproval { get; private set; }
|
|
public WorkspaceLock? LastLock { get; private set; }
|
|
public (string Domain, string TargetRef)? LastReleasedLock { get; private set; }
|
|
|
|
public Task<IEnumerable<Setting>> GetSettingsAsync() => Task.FromResult(Enumerable.Empty<Setting>());
|
|
public Task<Setting?> GetSettingByKeyAsync(string key) => Task.FromResult<Setting?>(null);
|
|
public Task<bool> UpsertSettingAsync(Setting setting) { LastSetting = setting; return Task.FromResult(true); }
|
|
public Task<bool> DeleteSettingAsync(string key) => Task.FromResult(true);
|
|
|
|
public Task<IEnumerable<AccountSnapshot>> GetAccountSnapshotsAsync() => Task.FromResult(Enumerable.Empty<AccountSnapshot>());
|
|
public Task<bool> InsertAccountSnapshotsAsync(IEnumerable<AccountSnapshot> snapshots) => Task.FromResult(true);
|
|
public Task<bool> ClearAccountSnapshotsAsync() => Task.FromResult(true);
|
|
|
|
public Task<IEnumerable<WorkspaceApproval>> GetApprovalsAsync() => Task.FromResult(Enumerable.Empty<WorkspaceApproval>());
|
|
public Task<WorkspaceApproval?> GetApprovalAsync(string domain, string targetRef) => Task.FromResult<WorkspaceApproval?>(null);
|
|
public Task<bool> UpsertApprovalAsync(WorkspaceApproval approval) { LastApproval = approval; return Task.FromResult(true); }
|
|
|
|
public Task<IEnumerable<WorkspaceLock>> GetLocksAsync() => Task.FromResult(Enumerable.Empty<WorkspaceLock>());
|
|
public Task<WorkspaceLock?> GetLockAsync(string domain, string targetRef) => Task.FromResult<WorkspaceLock?>(null);
|
|
public Task<bool> AcquireLockAsync(WorkspaceLock @lock) { LastLock = @lock; return Task.FromResult(true); }
|
|
public Task<bool> ReleaseLockAsync(string domain, string targetRef) { LastReleasedLock = (domain, targetRef); return Task.FromResult(true); }
|
|
}
|
|
|
|
private sealed class FakeHistoryStore : IPostgresqlHistoryStore
|
|
{
|
|
public string? LastDomain { get; private set; }
|
|
public IDictionary<string, object?>? LastPayload { get; private set; }
|
|
|
|
public Task<int> AppendAsync(string domain, IDictionary<string, object?> payload)
|
|
{
|
|
LastDomain = domain;
|
|
LastPayload = new Dictionary<string, object?>(payload);
|
|
return Task.FromResult(1);
|
|
}
|
|
|
|
public Task<IReadOnlyList<IDictionary<string, object?>>> SnapshotAsync(string domain, int limit = 500)
|
|
=> Task.FromResult<IReadOnlyList<IDictionary<string, object?>>>(Array.Empty<IDictionary<string, object?>>());
|
|
}
|
|
}
|