thirteenth version
This commit is contained in:
@@ -3,6 +3,7 @@ package risk
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
"overnight-trading-bot/internal/domain"
|
||||
)
|
||||
|
||||
var exitProcess = os.Exit
|
||||
|
||||
type EventSink interface {
|
||||
InsertRiskEvent(ctx context.Context, event domain.RiskEvent) error
|
||||
SaveSystemState(ctx context.Context, state domain.SystemState, mode domain.Mode, halted bool, reason string, contextJSON string) error
|
||||
@@ -63,6 +66,11 @@ func (m Manager) Halt(ctx context.Context, mode domain.Mode, eventType, reason s
|
||||
if m.sink == nil {
|
||||
return nil
|
||||
}
|
||||
if err := m.sink.SaveSystemState(ctx, domain.StateHalted, mode, true, reason, "{}"); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "fail-stop: persist halt state: %v\n", err)
|
||||
exitProcess(1)
|
||||
return fmt.Errorf("persist halt state: %w", err)
|
||||
}
|
||||
event := domain.RiskEvent{
|
||||
TS: time.Now().UTC(),
|
||||
Severity: domain.SeverityCritical,
|
||||
@@ -73,9 +81,6 @@ func (m Manager) Halt(ctx context.Context, mode domain.Mode, eventType, reason s
|
||||
if err := m.sink.InsertRiskEvent(ctx, event); err != nil {
|
||||
return fmt.Errorf("insert halt risk event: %w", err)
|
||||
}
|
||||
if err := m.sink.SaveSystemState(ctx, domain.StateHalted, mode, true, reason, "{}"); err != nil {
|
||||
return fmt.Errorf("persist halt state: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package risk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -9,6 +11,69 @@ import (
|
||||
"overnight-trading-bot/internal/domain"
|
||||
)
|
||||
|
||||
func TestHaltPersistsStateBeforeRiskEvent(t *testing.T) {
|
||||
sink := &recordingHaltSink{}
|
||||
manager := NewManager(sink, ManagerConfig{})
|
||||
if err := manager.Halt(context.Background(), domain.ModeLiveTrade, "risk", "stop", "uid"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sink.calls) != 2 || sink.calls[0] != "state" || sink.calls[1] != "event" {
|
||||
t.Fatalf("calls=%v, want state before event", sink.calls)
|
||||
}
|
||||
if sink.state != domain.StateHalted || !sink.halted || sink.reason != "stop" {
|
||||
t.Fatalf("state=%s halted=%v reason=%q", sink.state, sink.halted, sink.reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHaltFailStopsWhenStatePersistFails(t *testing.T) {
|
||||
oldExit := exitProcess
|
||||
defer func() { exitProcess = oldExit }()
|
||||
exitCode := -1
|
||||
exitProcess = func(code int) {
|
||||
exitCode = code
|
||||
panic("exit")
|
||||
}
|
||||
sink := &recordingHaltSink{saveErr: errors.New("db down")}
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatal("expected fail-stop panic from exit hook")
|
||||
}
|
||||
if exitCode != 1 {
|
||||
t.Fatalf("exit code=%d, want 1", exitCode)
|
||||
}
|
||||
if sink.eventInserted {
|
||||
t.Fatal("risk event inserted before failed halt state persist")
|
||||
}
|
||||
}()
|
||||
_ = NewManager(sink, ManagerConfig{}).Halt(context.Background(), domain.ModeLiveTrade, "risk", "stop", "")
|
||||
}
|
||||
|
||||
type recordingHaltSink struct {
|
||||
calls []string
|
||||
saveErr error
|
||||
eventInserted bool
|
||||
state domain.SystemState
|
||||
halted bool
|
||||
reason string
|
||||
}
|
||||
|
||||
func (s *recordingHaltSink) InsertRiskEvent(context.Context, domain.RiskEvent) error {
|
||||
s.calls = append(s.calls, "event")
|
||||
s.eventInserted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *recordingHaltSink) SaveSystemState(_ context.Context, state domain.SystemState, _ domain.Mode, halted bool, reason string, _ string) error {
|
||||
s.calls = append(s.calls, "state")
|
||||
if s.saveErr != nil {
|
||||
return s.saveErr
|
||||
}
|
||||
s.state = state
|
||||
s.halted = halted
|
||||
s.reason = reason
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPreTradeClosingPositionBypassesOpenPositionLimit(t *testing.T) {
|
||||
manager := NewManager(nil, ManagerConfig{MaxOpenPositions: 1})
|
||||
input := PreTradeInput{
|
||||
|
||||
Reference in New Issue
Block a user