132 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|