mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
82 lines
2.6 KiB
Go
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)
|
|
}
|
|
}
|