Files
overnight-trading-bot/internal/reconciliation/engine_test.go
T
2026-06-07 21:01:40 +00:00

132 lines
3.6 KiB
Go

package reconciliation
import (
"context"
"testing"
"time"
"github.com/shopspring/decimal"
"overnight-trading-bot/internal/domain"
"overnight-trading-bot/internal/testutil"
"overnight-trading-bot/internal/tinvest"
)
func TestReconciliationFindsCriticalDiffs(t *testing.T) {
ctx := context.Background()
repo := testutil.NewMemoryRepository()
gateway := tinvest.NewFakeGateway()
now := time.Now().UTC()
if err := repo.UpsertOrder(ctx, domain.Order{
ClientOrderID: "local",
BrokerOrderID: "broker-missing",
AccountIDHash: "hash",
InstrumentUID: "uid-local",
TradeDate: now,
Side: domain.SideBuy,
OrderType: domain.OrderTypeLimit,
QuantityLots: 1,
Status: domain.OrderStatusSent,
}); err != nil {
t.Fatal(err)
}
gateway.Orders["broker-unknown"] = domain.Order{
ClientOrderID: "unknown",
BrokerOrderID: "broker-unknown",
AccountIDHash: "hash",
InstrumentUID: "uid-broker",
QuantityLots: 1,
Status: domain.OrderStatusSent,
}
if err := repo.UpsertPosition(ctx, domain.Position{
AccountIDHash: "hash",
InstrumentUID: "uid-local",
OpenTradeDate: now,
Lots: 2,
Status: domain.PositionHoldingOvernight,
}); err != nil {
t.Fatal(err)
}
gateway.Portfolio = domain.Portfolio{
Equity: decimal.NewFromInt(100000),
Cash: decimal.NewFromInt(90000),
Holdings: []domain.Holding{
{InstrumentUID: "uid-local", QuantityLots: 1},
{InstrumentUID: "uid-broker-only", QuantityLots: 3},
},
}
diffs, err := New(repo, gateway, "account", "hash").Run(ctx)
if err != nil {
t.Fatal(err)
}
wantKinds := map[string]bool{
"unknown_active_order": false,
"missing_local_order": false,
"position_lots_mismatch": false,
"unknown_broker_position": false,
}
for _, diff := range diffs {
if _, ok := wantKinds[diff.Kind]; ok {
wantKinds[diff.Kind] = true
}
}
for kind, seen := range wantKinds {
if !seen {
t.Fatalf("missing diff kind %s in %+v", kind, diffs)
}
}
if !HasCritical(diffs) {
t.Fatalf("expected critical diffs")
}
}
func TestCompareOperationsCommissionPerInstrument(t *testing.T) {
orders := []domain.Order{
{InstrumentUID: "TRUR", Status: domain.OrderStatusFilled, Commission: decimal.NewFromInt(2)},
{InstrumentUID: "TGLD", Status: domain.OrderStatusFilled, Commission: decimal.NewFromInt(1)},
}
operations := []domain.Operation{
{InstrumentUID: "TRUR", Type: "OPERATION_TYPE_BUY", Commission: decimal.NewFromInt(1)},
{InstrumentUID: "TGLD", Type: "OPERATION_TYPE_BUY", Commission: decimal.NewFromInt(2)},
}
diffs := compareOperations(orders, operations)
seen := map[string]bool{}
for _, diff := range diffs {
if diff.Kind == "commission_mismatch" {
seen[diff.InstrumentUID] = true
}
}
if !seen["TRUR"] || !seen["TGLD"] {
t.Fatalf("expected per-instrument commission diffs, got %+v", diffs)
}
}
func TestReconciliationSkipsFreshInFlightLocalOrders(t *testing.T) {
ctx := context.Background()
repo := testutil.NewMemoryRepository()
gateway := tinvest.NewFakeGateway()
now := time.Now().UTC()
if err := repo.UpsertOrder(ctx, domain.Order{
ClientOrderID: "fresh",
AccountIDHash: "hash",
InstrumentUID: "uid",
TradeDate: now,
Side: domain.SideBuy,
OrderType: domain.OrderTypeLimit,
QuantityLots: 1,
Status: domain.OrderStatusSent,
CreatedAt: now,
}); err != nil {
t.Fatal(err)
}
diffs, err := New(repo, gateway, "account", "hash").WithInFlightGrace(10 * time.Second).Run(ctx)
if err != nil {
t.Fatal(err)
}
for _, diff := range diffs {
if diff.Kind == "local_order_without_broker_id" || diff.Kind == "missing_local_order" {
t.Fatalf("fresh in-flight order produced diff: %+v", diffs)
}
}
}