Files
nofx/trader/auto_trader_throttle_test.go
2026-06-27 00:37:59 +08:00

82 lines
2.6 KiB
Go

package trader
import (
"nofx/kernel"
"strings"
"testing"
"time"
)
func throttleContext(symbol, side string, heldFor time.Duration, pnlPct float64) *kernel.Context {
return &kernel.Context{
Positions: []kernel.PositionInfo{
{
Symbol: symbol,
Side: side,
UnrealizedPnLPct: pnlPct,
UpdateTime: time.Now().Add(-heldFor).UnixMilli(),
},
},
}
}
func TestTradeThrottleBlocksEarlyNoiseClose(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -0.3)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
if !strings.Contains(reason, "min AI-managed hold") {
t.Fatalf("expected early close to be blocked by min hold, got %q", reason)
}
}
func TestTradeThrottleAllowsEarlyHardStop(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -3.0)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
if reason != "" {
t.Fatalf("expected hard stop close to pass, got %q", reason)
}
}
func TestTradeThrottleBlocksFlatCloseInsideNoiseWindow(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, 0.4)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
if !strings.Contains(reason, "noise band") {
t.Fatalf("expected flat close to be blocked inside noise window, got %q", reason)
}
}
func TestTradeThrottleAllowsConfirmedLossAfterMinimumHold(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, -1.2)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
if reason != "" {
t.Fatalf("expected confirmed loss after min hold to pass, got %q", reason)
}
}
func TestTradeThrottleBlocksSecondOpenInCycle(t *testing.T) {
at := &AutoTrader{}
ctx := &kernel.Context{}
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 1)
if !strings.Contains(reason, "only 1 new position") {
t.Fatalf("expected second open in cycle to be blocked, got %q", reason)
}
}
func TestTradeThrottleBlocksOpeningAgainstExistingPosition(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 2*time.Hour, 1.0)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_short"}, ctx, 0)
if !strings.Contains(reason, "already has an open") {
t.Fatalf("expected opposite open to be blocked when position exists, got %q", reason)
}
}