mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-06 20:41:14 +08:00
- Redesign dashboard into a cream-paper + vermilion IBM Plex Mono terminal (live L2 order book, cost/liq map, WS K-line, signal matrix, orchestration topology, risk radar, execution log, current positions, equity curve) - Convert all user-facing UI and backend strings/prompts from Chinese to English (multi-language retained, default English) - Add /api/statistics/full endpoint + full-stats frontend wiring - Fix Autopilot launch: reuse the existing trader instead of creating duplicates (eliminates repeat ~35s create cost and stale-trader 404s); launch sends 5m scan interval - Fix unreadable toasts: cream theme with high-contrast text + per-type accent - Silence background dashboard polls (getTraderConfig) to stop error-toast spam
98 lines
3.3 KiB
Go
98 lines
3.3 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 TestTradeThrottleAllowsLongShortPairInCycle(t *testing.T) {
|
|
at := &AutoTrader{}
|
|
ctx := &kernel.Context{}
|
|
|
|
// One open already queued this cycle (e.g. the long) — the second open
|
|
// (the short) must still be allowed so a directional pair can open.
|
|
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_short"}, ctx, 1)
|
|
if reason != "" {
|
|
t.Fatalf("expected the second (short) open in cycle to be allowed, got %q", reason)
|
|
}
|
|
}
|
|
|
|
func TestTradeThrottleBlocksOpensOverCycleCap(t *testing.T) {
|
|
at := &AutoTrader{}
|
|
ctx := &kernel.Context{}
|
|
|
|
// under the 6-per-cycle cap, a further open is allowed
|
|
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 5); reason != "" {
|
|
t.Fatalf("expected open within the 6-per-cycle cap to be allowed, got %q", reason)
|
|
}
|
|
// at the cap, the next open is blocked
|
|
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 6); !strings.Contains(reason, "6 new position") {
|
|
t.Fatalf("expected open beyond the 6-per-cycle cap 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)
|
|
}
|
|
}
|