5 Commits

Author SHA1 Message Date
tinkle-community
39eac5aca7 config: stop the churn — hold for big moves, wide TP/SL, low leverage
Live decomposition of the losing streak: 23% win rate with avg win +$1.23 /
avg loss -$1.04 on ~0.3-0.5% price moves, where the ~0.14% round-trip fee ate
30-50% of every tiny winner. Death by small-move grinding. The AI was closing
positions on ±0.5% noise after the 60m min-hold, capping winners at ~0.86%.

Redesign to 'few big positions, held for big moves':
- Throttle: min hold 60m->4h, noise-close window 90m->8h, reentry 30m->3h,
  opens/hour 30->3, opens/cycle 6->2. Noise band widened -1%..+2% -> -4%..+6%
  so small moves can no longer trigger a close.
- Exits: stop bypass -2.5% -> -5%, take-profit bypass +5% -> +12% (wide,
  asymmetric — let winners run, cut losers only on a real move).
- Leverage 20x -> 5x: a -5% stop at 20x is instant liquidation; at 5x it is
  -25% of margin, survivable. 2 positions x 2.5x = 5x total (full margin,
  ~20% cushion) instead of 4x5x=20x.
- Prompt now instructs the AI to set wide stops (~-5%) and distant targets
  (~+10-12%), hold multi-hour, and never scalp 0.5% moves.

Live strategy updated (maxPos=2, lev=5, ratio=2.5).
2026-07-21 15:02:23 +09:00
tinkle-community
0f3e71560c fix: fetch all Hyperliquid fills — UserFillsByTime is capped at 100
Root cause of the dashboard over-reporting profit while the account lost
money: the fill sync used UserFillsByTime, which hard-caps at 100 fills per
response. At 20x/4-position frequency the account does >100 fills/24h, so
~20% of fills were silently dropped — and the dropped ones skewed toward
losers, inflating recorded PnL and under-counting fees. Verified: over a 46h
window the DB showed net +$39 while Hyperliquid official was -$18.6, and the
equity drop ($176 -> $156) confirmed the loss.

Switch GetTrades to UserFills (returns up to 2000 recent fills), filtering to
startTime client-side, and widen the sync lookback 24h -> 7d so gaps
backfill. Verified live: a sync now pulls 675 fills where it previously
always received exactly 100.

Note: this stops future drift; already-corrupted historical position rows are
not retroactively rebuilt (dedup blocks re-processing). Account equity remains
the authoritative scoreboard.
2026-07-16 11:11:56 +09:00
tinkle-community
eabd279d10 feat: show hold duration in the recent trades panel
Adds a HOLD column (45m / 2h10 / 1d3h) plus a header row to the dashboard's
recent-closes table. Makes the hold-time-vs-PnL pattern (short holds bleed,
1h+ holds carry the edge) visible per trade instead of only in the aggregate
Edge Profile panel.
2026-07-15 19:36:58 +09:00
tinkle-community
09b7ac9e92 config: autopilot to 4x 5x notional at 20x leverage; short floor 0.75 -> 0.4
Two independent changes bundled:
- Position sizing back to 4 positions x 5x equity notional at 20x leverage
  (20x total account notional, ~5% liquidation cushion) — aggressive book
  size by operator choice, for bigger single positions on a small account.
- Forced short-coverage signal floor lowered 0.75 -> 0.4. At 0.75, a
  long-leaning board left every bearish candidate below the bar, so no short
  ever opened and the book became a one-directional long bet. 0.4 keeps
  genuine directional signals so the book actually hedges — which matters
  more, not less, at 20x.
2026-07-14 12:11:29 +09:00
tinkle-community
dc68884559 feat: autopilot book to 4 positions x 5x notional at 20x leverage
Was 2 positions x ~equity×1.2 — too few holdings and no room for shorts
(the 2 slots filled with the strongest signals, which were long-leaning, so
the balanced candidate pool never got expressed as short trades).

Now: max 4 positions, each sized at equity×5 notional, 20x leverage. Four
positions × 5x = 20x total account notional = full margin at 20x — the
operator's requested ceiling. This gives room for ~2 long + 2 short and
bigger single positions; the existing direction-balanced candidate selection
plus long/short coverage fills both sides when strong bearish signals exist.

Applied across all three config sources (default template, quick-create
preset, studio unified) with matching test assertions and 20x prompt copy.
2026-07-11 11:20:33 +09:00
12 changed files with 112 additions and 69 deletions

View File

@@ -263,12 +263,13 @@ func (s *Server) createDefaultStrategies(userID string, lang string) error {
c.CoinSource.VergexMarketType = "all"
c.CoinSource.VergexChain = "hyperliquid"
c.RiskControl.MaxPositions = 2
c.RiskControl.BTCETHMaxLeverage = 10
c.RiskControl.AltcoinMaxLeverage = 10
// 4× equity notional per position: at 10x leverage two full positions
// use ~80% of margin — concentrated but solvent.
c.RiskControl.BTCETHMaxPositionValueRatio = 4.0
c.RiskControl.AltcoinMaxPositionValueRatio = 4.0
c.RiskControl.BTCETHMaxLeverage = 5
c.RiskControl.AltcoinMaxLeverage = 5
// Few, concentrated positions held for big moves. 5x leverage so a
// wide (-5%) stop is survivable rather than an instant liquidation;
// 2 positions × 2.5x = 5x total notional (full margin, ~20% cushion).
c.RiskControl.BTCETHMaxPositionValueRatio = 2.5
c.RiskControl.AltcoinMaxPositionValueRatio = 2.5
c.RiskControl.MaxMarginUsage = 1.0
c.RiskControl.MinConfidence = 78
c.RiskControl.MinRiskRewardRatio = 3.0

View File

@@ -54,16 +54,16 @@ func TestCreateDefaultStrategiesUsesOneReadyToRunClaw402Preset(t *testing.T) {
if trendCfg.CoinSource.SourceType != "vergex_signal" || trendCfg.CoinSource.VergexLimit != 10 || trendCfg.CoinSource.VergexMarketType != "all" {
t.Fatalf("default strategy should use the Claw402/Vergex all-market signal ranking, got %+v", trendCfg.CoinSource)
}
if trendCfg.CoinSource.UseAI500 || trendCfg.RiskControl.MaxPositions > 2 {
t.Fatalf("default strategy should be Claw402/Vergex native with at most two positions, got coin=%+v risk=%+v", trendCfg.CoinSource, trendCfg.RiskControl)
if trendCfg.CoinSource.UseAI500 || trendCfg.RiskControl.MaxPositions != 2 {
t.Fatalf("default strategy should be Claw402/Vergex native with a 2-position concentrated book, got coin=%+v risk=%+v", trendCfg.CoinSource, trendCfg.RiskControl)
}
if trendCfg.RiskControl.BTCETHMaxLeverage != 10 || trendCfg.RiskControl.AltcoinMaxLeverage != 10 {
t.Fatalf("default strategy should use 10x leverage for all Claw402 opens, got risk=%+v", trendCfg.RiskControl)
if trendCfg.RiskControl.BTCETHMaxLeverage != 5 || trendCfg.RiskControl.AltcoinMaxLeverage != 5 {
t.Fatalf("default strategy should use 5x leverage for all Claw402 opens, got risk=%+v", trendCfg.RiskControl)
}
if trendCfg.RiskControl.BTCETHMaxPositionValueRatio != 4 ||
trendCfg.RiskControl.AltcoinMaxPositionValueRatio != 4 ||
if trendCfg.RiskControl.BTCETHMaxPositionValueRatio != 2.5 ||
trendCfg.RiskControl.AltcoinMaxPositionValueRatio != 2.5 ||
trendCfg.RiskControl.MaxMarginUsage != 1.0 {
t.Fatalf("default strategy should size Claw402 opens at 4x equity notional (two positions ≈ 80%% margin at 10x), got risk=%+v", trendCfg.RiskControl)
t.Fatalf("default strategy should size Claw402 opens at 2.5x equity notional (2 positions = 5x total at 5x), got risk=%+v", trendCfg.RiskControl)
}
}

View File

@@ -215,9 +215,9 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant
sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n")
sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n")
sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n")
sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 60 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n")
sb.WriteString("- Fees are the main edge killer: a round trip costs roughly 0.1%% of notional (about 1%% of margin at 10x). Only take setups whose expected move to target is at least 3x that cost; fewer, higher-conviction, longer-hold trades beat frequent scalps.\n")
sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n")
sb.WriteString("- Hold for BIG moves, do not churn: hold new positions for at least 4 hours; never close inside the -4%% to +6%% noise band before ~8 hours; after closing a symbol wait 3 hours before re-entry; open at most 1-2 new positions per hour. Small in-and-out trades bled this account to death on fees.\n")
sb.WriteString("- Fees are the main edge killer: a round trip costs ~0.1%% of notional. Only enter setups where the realistic target is a LARGE move: stop-loss around -5%% and take-profit around +10-12%% (roughly 2:1 or better). Do not aim for 0.5%% scalps — they cannot cover fees. Fewer, high-conviction, wide-target, multi-hour holds only.\n")
sb.WriteString("- Set WIDE stops and targets: place the stop well beyond short-term noise (around -5%%) and the target at a distant heatmap resistance/liquidation zone (around +10-12%%). Give the position room to develop; do not exit on small green or small red.\n\n")
} else {
sb.WriteString("# You are the NOFX Claw402 auto-trader\n\n")
sb.WriteString("Trade only Hyperliquid instruments returned by this cycle's Claw402.ai/Vergex board. You may trade only the current candidate symbols and existing positions; never invent tickers or rotate outside the provided universe.\n\n")
@@ -232,9 +232,9 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant
sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n")
sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n")
sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n")
sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 60 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n")
sb.WriteString("- Fees are the main edge killer: a round trip costs roughly 0.1%% of notional (about 1%% of margin at 10x). Only take setups whose expected move to target is at least 3x that cost; fewer, higher-conviction, longer-hold trades beat frequent scalps.\n")
sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n")
sb.WriteString("- Hold for BIG moves, do not churn: hold new positions for at least 4 hours; never close inside the -4%% to +6%% noise band before ~8 hours; after closing a symbol wait 3 hours before re-entry; open at most 1-2 new positions per hour. Small in-and-out trades bled this account to death on fees.\n")
sb.WriteString("- Fees are the main edge killer: a round trip costs ~0.1%% of notional. Only enter setups where the realistic target is a LARGE move: stop-loss around -5%% and take-profit around +10-12%% (roughly 2:1 or better). Do not aim for 0.5%% scalps — they cannot cover fees. Fewer, high-conviction, wide-target, multi-hour holds only.\n")
sb.WriteString("- Set WIDE stops and targets: place the stop well beyond short-term noise (around -5%%) and the target at a distant heatmap resistance/liquidation zone (around +10-12%%). Give the position room to develop; do not exit on small green or small red.\n\n")
}
writeModeVariant(&sb, variant, zh)

View File

@@ -29,8 +29,8 @@ func TestBuildSystemPromptUsesVergexClaw402Prompt(t *testing.T) {
if !strings.Contains(prompt, "Direction must be data-driven") {
t.Fatalf("prompt should explain that direction is data-driven, not long-only:\n%s", prompt)
}
if !strings.Contains(prompt, "every open position must use exactly 10x") {
t.Fatalf("prompt should force 10x leverage for Claw402 opens:\n%s", prompt)
if !strings.Contains(prompt, "every open position must use exactly 5x") {
t.Fatalf("prompt should force 5x leverage for Claw402 opens:\n%s", prompt)
}
if !strings.Contains(prompt, "use the full max notional per position") {
t.Fatalf("prompt should force full-size Claw402 opens:\n%s", prompt)

View File

@@ -1014,11 +1014,11 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
PriceRankingLimit: 10,
},
RiskControl: RiskControlConfig{
MaxPositions: 2, // Concentrated book: two full-size positions (CODE ENFORCED)
BTCETHMaxLeverage: 10, // BTC/ETH exchange leverage (AI guided)
AltcoinMaxLeverage: 10, // TradeFi exchange leverage (AI guided)
BTCETHMaxPositionValueRatio: 4.0, // Per-position notional = equity × 4; at 10x two positions ≈ 80% margin
AltcoinMaxPositionValueRatio: 4.0, // Per-position notional = equity × 4; at 10x two positions ≈ 80% margin
MaxPositions: 2, // Few, concentrated positions held for big moves (CODE ENFORCED)
BTCETHMaxLeverage: 5, // Low leverage so a big (-5%) stop is survivable, not an instant liquidation
AltcoinMaxLeverage: 5, // Low leverage so a big (-5%) stop is survivable, not an instant liquidation
BTCETHMaxPositionValueRatio: 2.5, // Per-position notional = equity × 2.5; 2 positions = 5x total (full margin at 5x, ~20% liquidation cushion)
AltcoinMaxPositionValueRatio: 2.5, // Per-position notional = equity × 2.5; 2 positions = 5x total (full margin at 5x, ~20% liquidation cushion)
MaxMarginUsage: 1.0, // Claw402 Autopilot intentionally uses full margin when opening
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)

View File

@@ -8,11 +8,13 @@ import (
)
// forcedCoverageMinScore is the minimum absolute board z-score a candidate
// needs before the engine will force-open it for book balance. Live trade
// history showed forced entries on near-neutral signals (|z| < 0.3) were a
// systematic money loser — especially shorts — while trades on strong signals
// carried the edge. Below this bar the book is simply left unbalanced.
const forcedCoverageMinScore = 0.75
// needs before the engine will force-open it for book balance. Near-neutral
// signals (|z| < ~0.3) proved a systematic loser, but a 0.75 floor was too
// strict: in a long-leaning tape every bearish candidate scored below it, so
// no short ever opened and the book became a one-directional long bet that
// drew down hard. 0.4 keeps genuine directional signals while still filtering
// pure noise, so the book can actually hedge.
const forcedCoverageMinScore = 0.4
// ensureLongShortCoverage tops the book up toward roughly half the
// MaxPositions slots long and half short — but only with candidates whose

View File

@@ -10,23 +10,25 @@ import (
)
const (
// Live history: trades held under an hour were net-negative after fees
// (the 15-60m bucket bled), while the edge concentrated in 1h+ holds.
autopilotMinHoldDuration = 60 * time.Minute
autopilotNoiseCloseHoldDuration = 90 * time.Minute
autopilotReentryCooldown = 30 * time.Minute
// Allow one long + one short per cycle. The real exposure/churn limits are
// MaxPositions (concurrent) + the 45m min-hold + the 90m per-symbol reentry
// cooldown, so the per-hour cap only needs to be high enough not to block the
// directional pair from re-establishing after positions close. A tight value
// here (e.g. 2) starves the strategy: once a couple opens fire, every later
// cycle is blocked and the book drains to flat. Keep it generous.
autopilotMaxOpensPerHour = 30
autopilotMaxOpensPerCycle = 6
earlyCloseStopLossBypassPct = -2.5
earlyCloseTakeProfitBypassPct = 5.0
noiseCloseLossFloorPct = -1.0
noiseCloseProfitCeilingPct = 2.0
// "Hold for big moves, don't churn" regime. Live history showed the
// account bleeding to death by fees: 0.3-0.5% in/out moves where a ~0.14%
// round-trip fee ate 30-50% of every small winner. These values force
// positions to be held for hours and to develop meaningful moves before
// closing, and cut the trade frequency hard.
autopilotMinHoldDuration = 4 * time.Hour
autopilotNoiseCloseHoldDuration = 8 * time.Hour
autopilotReentryCooldown = 3 * time.Hour
// Drastically cut churn: at most a couple of new positions per hour/cycle.
autopilotMaxOpensPerHour = 3
autopilotMaxOpensPerCycle = 2
// Wide, asymmetric exits. Cut a loser only at a real -5% (at 5x leverage
// that is -25% of margin — survivable), let a winner run to +12% before
// any early take-profit. The noise band (-4%..+6%) blocks closing on the
// small moves that were grinding the account to nothing.
earlyCloseStopLossBypassPct = -5.0
earlyCloseTakeProfitBypassPct = 12.0
noiseCloseLossFloorPct = -4.0
noiseCloseProfitCeilingPct = 6.0
)
func isOpenAction(action string) bool {

View File

@@ -32,7 +32,8 @@ func TestTradeThrottleBlocksEarlyNoiseClose(t *testing.T) {
func TestTradeThrottleAllowsEarlyHardStop(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -3.0)
// Only a real -5% stop bypasses the min hold now.
ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -6.0)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
if reason != "" {
@@ -42,7 +43,8 @@ func TestTradeThrottleAllowsEarlyHardStop(t *testing.T) {
func TestTradeThrottleBlocksFlatCloseInsideNoiseWindow(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, 0.4)
// Held past the 4h min hold but still inside the wide -4%..+6% noise band.
ctx := throttleContext("xyz:INTC", "long", 5*time.Hour, 0.4)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
if !strings.Contains(reason, "noise band") {
@@ -52,7 +54,8 @@ func TestTradeThrottleBlocksFlatCloseInsideNoiseWindow(t *testing.T) {
func TestTradeThrottleAllowsConfirmedLossAfterMinimumHold(t *testing.T) {
at := &AutoTrader{}
ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, -1.2)
// Past the 4h min hold, loss beyond the -4% noise floor → close allowed.
ctx := throttleContext("xyz:INTC", "long", 5*time.Hour, -4.5)
reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0)
if reason != "" {
@@ -76,13 +79,13 @@ 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)
// under the 2-per-cycle cap, a further open is allowed
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 1); reason != "" {
t.Fatalf("expected open within the 2-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)
if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 2); !strings.Contains(reason, "2 new position") {
t.Fatalf("expected open beyond the 2-per-cycle cap to be blocked, got %q", reason)
}
}

View File

@@ -20,13 +20,16 @@ func (t *HyperliquidTrader) SyncOrdersFromHyperliquid(traderID string, exchangeI
return fmt.Errorf("store is nil")
}
// Get recent trades (last 24 hours)
startTime := time.Now().Add(-24 * time.Hour)
// Look back 7 days. GetTrades now pulls up to 2000 recent fills (UserFills)
// and filters to this window, so a wide lookback backfills any fills missed
// during past outages/gaps without dropping recent ones. Dedup by trade ID
// keeps re-processing idempotent.
startTime := time.Now().Add(-7 * 24 * time.Hour)
logger.Infof("🔄 Syncing Hyperliquid trades from: %s", startTime.Format(time.RFC3339))
// Use GetTrades method to fetch trade records
trades, err := t.GetTrades(startTime, 1000)
trades, err := t.GetTrades(startTime, 2000)
if err != nil {
return fmt.Errorf("failed to get trades: %w", err)
}

View File

@@ -11,6 +11,8 @@ import (
"strconv"
"strings"
"time"
hl "github.com/sonirico/go-hyperliquid"
)
// GetBalance gets account balance
@@ -548,15 +550,23 @@ func (t *HyperliquidTrader) GetClosedPnL(startTime time.Time, limit int) ([]type
// GetTrades retrieves trade history from Hyperliquid
func (t *HyperliquidTrader) GetTrades(startTime time.Time, limit int) ([]types.TradeRecord, error) {
// Use UserFillsByTime API
// Use UserFills (returns up to 2000 recent fills) rather than
// UserFillsByTime, which is hard-capped at 100 fills per response. At
// this trading frequency the account exceeds 100 fills/24h, so
// UserFillsByTime silently dropped ~20% of fills — skewing recorded PnL
// and fees away from the exchange truth. 2000 recent fills covers many
// days of history; we filter to startTime client-side.
startTimeMs := startTime.UnixMilli()
fills, err := t.exchange.Info().UserFillsByTime(t.ctx, t.walletAddr, startTimeMs, nil, nil)
fills, err := t.exchange.Info().UserFills(t.ctx, hl.UserFillsParams{Address: t.walletAddr})
if err != nil {
return nil, fmt.Errorf("failed to get user fills: %w", err)
}
var trades []types.TradeRecord
for _, fill := range fills {
if fill.Time < startTimeMs {
continue
}
price, _ := strconv.ParseFloat(fill.Price, 64)
qty, _ := strconv.ParseFloat(fill.Size, 64)
fee, _ := strconv.ParseFloat(fill.Fee, 64)

View File

@@ -83,6 +83,18 @@ function fmtTime(raw?: string | number): string {
return Number.isNaN(d.getTime()) ? '' : d.toLocaleString('en-GB', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false })
}
/** Hold duration from entry/exit epoch-ms as a compact 45m / 2h10 / 1d3h. */
function fmtHold(entry?: number, exit?: number): string {
if (!entry || !exit || exit <= entry) return '—'
const mins = Math.round((exit - entry) / 60000)
if (mins < 60) return `${mins}m`
const h = Math.floor(mins / 60)
const m = mins % 60
if (h < 24) return m ? `${h}h${m}` : `${h}h`
const d = Math.floor(h / 24)
return `${d}d${h % 24}h`
}
function useTick(ms = 1000) {
const [, set] = useState(0)
useEffect(() => {
@@ -537,10 +549,19 @@ export function TerminalDashboard({
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 6 }}>
<span className="tm-px" style={{ fontSize: 11 }}>Recent trades</span>
<span className="tm-sc">Recent closes · symbol/side/time/pnl</span>
<span className="tm-sc">Recent closes · symbol/side/hold/pnl</span>
</div>
{recentTrades.length > 0 ? (
<table className="tm-mono" style={{ width: '100%', borderCollapse: 'collapse', fontSize: 11 }}>
<thead>
<tr className="tm-sc" style={{ fontSize: 9 }}>
<td style={{ padding: '0 0 3px' }}>symbol</td>
<td style={{ padding: '0 0 3px' }}>side</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>hold</td>
<td style={{ padding: '0 0 3px' }}> closed</td>
<td style={{ padding: '0 0 3px', textAlign: 'right' }}>PnL</td>
</tr>
</thead>
<tbody>
{recentTrades.map((p) => {
const win = p.realized_pnl >= 0
@@ -548,7 +569,8 @@ export function TerminalDashboard({
<tr key={p.id} style={{ borderTop: '1px solid var(--tm-hair)' }}>
<td style={{ padding: '5px 0', fontWeight: 500 }}>{baseLabel(p.symbol)}</td>
<td style={{ padding: '5px 0' }} className={p.side === 'long' || p.side === 'LONG' ? 'tm-up' : 'tm-dn'}>{p.side.toLowerCase()}</td>
<td style={{ padding: '5px 0', color: 'var(--tm-muted)' }}>{fmtTime(p.exit_time)}</td>
<td style={{ padding: '5px 0', textAlign: 'right', color: 'var(--tm-ink-2)' }}>{fmtHold(p.entry_time, p.exit_time)}</td>
<td style={{ padding: '5px 0 5px 6px', color: 'var(--tm-muted)' }}>{fmtTime(p.exit_time)}</td>
<td style={{ padding: '5px 0', textAlign: 'right' }} className={win ? 'tm-up' : 'tm-dn'}>{fmtUsd(p.realized_pnl, true)}</td>
</tr>
)

View File

@@ -1389,12 +1389,12 @@ export function StrategyStudioPage() {
risk_control: defaultRisk({
...base.ai_config?.risk_control,
max_positions: 2,
btc_eth_max_leverage: 10,
altcoin_max_leverage: 10,
// 4× equity notional per position — at 10x leverage two full
// positions use ~80% of margin (concentrated but solvent)
btc_eth_max_position_value_ratio: 4,
altcoin_max_position_value_ratio: 4,
btc_eth_max_leverage: 5,
altcoin_max_leverage: 5,
// Few, concentrated positions held for big moves. 5x leverage so a
// wide (-5%) stop is survivable; 2 positions × 2.5x = 5x total.
btc_eth_max_position_value_ratio: 2.5,
altcoin_max_position_value_ratio: 2.5,
max_margin_usage: 1.0,
min_confidence: 78,
min_risk_reward_ratio: 3,