mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 18:04:32 +08:00
feat: simplify Claw402 autopilot trading flow
This commit is contained in:
81
trader/auto_trader_throttle_test.go
Normal file
81
trader/auto_trader_throttle_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user