second version

This commit is contained in:
2026-06-07 21:51:20 +00:00
parent 8e2d7efc32
commit 282c841e11
23 changed files with 869 additions and 151 deletions
+53 -9
View File
@@ -16,16 +16,26 @@ import (
)
type Engine struct {
repo repository.Repository
gateway tinvest.Gateway
accountID string
accountIDHash string
window time.Duration
inFlightGrace time.Duration
repo repository.Repository
gateway tinvest.Gateway
accountID string
accountIDHash string
window time.Duration
inFlightGrace time.Duration
commissionTolerance decimal.Decimal
requireZeroCommission bool
quarantineOnNonZero bool
}
func New(repo repository.Repository, gateway tinvest.Gateway, accountID, accountIDHash string) Engine {
return Engine{repo: repo, gateway: gateway, accountID: accountID, accountIDHash: accountIDHash, window: 72 * time.Hour}
return Engine{
repo: repo,
gateway: gateway,
accountID: accountID,
accountIDHash: accountIDHash,
window: 72 * time.Hour,
commissionTolerance: decimal.NewFromFloat(0.01),
}
}
func (e Engine) WithWindow(window time.Duration) Engine {
@@ -42,6 +52,15 @@ func (e Engine) WithInFlightGrace(grace time.Duration) Engine {
return e
}
func (e Engine) WithCommissionPolicy(requireZero, quarantineOnNonZero bool, tolerance decimal.Decimal) Engine {
e.requireZeroCommission = requireZero
e.quarantineOnNonZero = quarantineOnNonZero
if !tolerance.IsNegative() {
e.commissionTolerance = tolerance
}
return e
}
func (e Engine) Run(ctx context.Context) ([]domain.ReconciliationDiff, error) {
localOrders, err := e.repo.ListActiveOrders(ctx, e.accountIDHash)
if err != nil {
@@ -138,7 +157,17 @@ func (e Engine) Run(ctx context.Context) ([]domain.ReconciliationDiff, error) {
if err != nil {
return nil, err
}
diffs = append(diffs, compareOperations(recentOrders, operations)...)
diffs = append(diffs, compareOperationsWithPolicy(recentOrders, operations, e.requireZeroCommission, e.commissionTolerance)...)
if e.requireZeroCommission && e.quarantineOnNonZero {
for _, diff := range diffs {
if diff.Kind != "actual_commission_nonzero" || diff.InstrumentUID == "" {
continue
}
if err := e.repo.QuarantineInstrument(ctx, diff.InstrumentUID, diff.Message); err != nil {
return nil, err
}
}
}
raw, _ := json.Marshal(diffs)
if err := e.repo.InsertReconciliation(ctx, now, string(raw), len(diffs) > 0); err != nil {
return nil, err
@@ -163,7 +192,14 @@ func HasCritical(diffs []domain.ReconciliationDiff) bool {
}
func compareOperations(orders []domain.Order, operations []domain.Operation) []domain.ReconciliationDiff {
return compareOperationsWithPolicy(orders, operations, false, decimal.NewFromFloat(0.01))
}
func compareOperationsWithPolicy(orders []domain.Order, operations []domain.Operation, requireZeroCommission bool, commissionTolerance decimal.Decimal) []domain.ReconciliationDiff {
var diffs []domain.ReconciliationDiff
if commissionTolerance.IsNegative() {
commissionTolerance = decimal.Zero
}
localCommissionByInstrument := make(map[string]decimal.Decimal)
localTraded := make(map[string]bool)
for _, order := range orders {
@@ -192,7 +228,15 @@ func compareOperations(orders []domain.Order, operations []domain.Operation) []d
for instrumentUID := range instruments {
localCommission := localCommissionByInstrument[instrumentUID]
brokerCommission := brokerCommissionByInstrument[instrumentUID]
if diff := money.Abs(localCommission.Sub(brokerCommission)); diff.GreaterThan(decimal.NewFromFloat(0.01)) {
if requireZeroCommission && brokerCommission.IsPositive() {
diffs = append(diffs, domain.ReconciliationDiff{
Kind: "actual_commission_nonzero",
InstrumentUID: instrumentUID,
Message: fmt.Sprintf("broker commission=%s", brokerCommission.StringFixed(2)),
Critical: true,
})
}
if diff := money.Abs(localCommission.Sub(brokerCommission)); diff.GreaterThan(commissionTolerance) {
diffs = append(diffs, domain.ReconciliationDiff{
Kind: "commission_mismatch",
InstrumentUID: instrumentUID,
+41
View File
@@ -101,6 +101,47 @@ func TestCompareOperationsCommissionPerInstrument(t *testing.T) {
}
}
func TestReconciliationQuarantinesOnNonZeroBrokerCommission(t *testing.T) {
ctx := context.Background()
repo := testutil.NewMemoryRepository()
gateway := tinvest.NewFakeGateway()
if err := repo.UpsertInstrument(ctx, domain.Instrument{
InstrumentUID: "uid",
Ticker: "TRUR",
Enabled: true,
}); err != nil {
t.Fatal(err)
}
gateway.Operations = []domain.Operation{{
InstrumentUID: "uid",
Type: "OPERATION_TYPE_BROKER_FEE",
Commission: decimal.NewFromFloat(0.01),
ExecutedAt: time.Now().UTC(),
}}
diffs, err := New(repo, gateway, "account", "hash").
WithCommissionPolicy(true, true, decimal.NewFromFloat(0.01)).
Run(ctx)
if err != nil {
t.Fatal(err)
}
found := false
for _, diff := range diffs {
if diff.Kind == "actual_commission_nonzero" && diff.Critical {
found = true
}
}
if !found {
t.Fatalf("expected actual_commission_nonzero diff, got %+v", diffs)
}
instruments, err := repo.ListInstruments(ctx, true)
if err != nil {
t.Fatal(err)
}
if len(instruments) != 1 || !instruments[0].Quarantine {
t.Fatalf("instrument not quarantined: %+v", instruments)
}
}
func TestReconciliationSkipsFreshInFlightLocalOrders(t *testing.T) {
ctx := context.Background()
repo := testutil.NewMemoryRepository()