diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts index 7ff5a7a..af7ae2d 100644 --- a/frontend/src/api/hooks.ts +++ b/frontend/src/api/hooks.ts @@ -397,6 +397,15 @@ export function useSetTradingMode() { }); } +export function useSetTradingCapital() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (initial_capital: number) => + apiPut<{ initial_capital: number; active_pool: number; reserve_pool: number }>('trading', '/api/trading/capital', { initial_capital }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['trading-config'] }), + }); +} + export function usePendingApprovals() { return useGet(['pending-approvals'], 'query', '/api/admin/trading/approvals'); } diff --git a/frontend/src/pages/Trading.tsx b/frontend/src/pages/Trading.tsx index c766614..00a9aa9 100644 --- a/frontend/src/pages/Trading.tsx +++ b/frontend/src/pages/Trading.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useTradingConfig, useSetTradingMode, + useSetTradingCapital, usePendingApprovals, useReviewApproval, useActiveLockouts, @@ -19,6 +20,7 @@ export function TradingPage() { const { data: macroStatus } = useMacroStatus(); const { data: competitiveStatus } = useCompetitiveStatus(); const setMode = useSetTradingMode(); + const setCapital = useSetTradingCapital(); const reviewApproval = useReviewApproval(); const toggleMacro = useToggleMacro(); const toggleCompetitive = useToggleCompetitive(); @@ -87,6 +89,9 @@ export function TradingPage() { )} + {/* Paper Trading Capital */} + setCapital.mutate(amount)} isPending={setCapital.isPending} /> + {/* Macro Signal Layer Toggle */}

Macro Signal Layer

@@ -292,3 +297,77 @@ function ApprovalRow({ approval, onReview }: { ); } + + +function CapitalCard({ onSetCapital, isPending }: { onSetCapital: (amount: number) => void; isPending: boolean }) { + const [amount, setAmount] = useState('100000'); + const [showConfirm, setShowConfirm] = useState(false); + + const presets = [10000, 50000, 100000, 500000]; + + return ( + +

Paper Trading Capital

+
+
+ + setAmount(e.target.value)} + className="w-40 rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none" + /> +
+
+ {presets.map((p) => ( + + ))} +
+ +
+ + {showConfirm && ( +
+

+ This will reset the paper trading pools to ${Number(amount).toLocaleString()}. + Any existing pool balances will be overwritten. +

+
+ + +
+
+ )} +
+ ); +} diff --git a/infra/migrations/018_autonomous_trading_engine.sql b/infra/migrations/018_autonomous_trading_engine.sql index d1d5dde..4e8dd2f 100644 --- a/infra/migrations/018_autonomous_trading_engine.sql +++ b/infra/migrations/018_autonomous_trading_engine.sql @@ -320,6 +320,7 @@ CREATE INDEX idx_notifications_event ON notifications(event_type, created_at DES -- ============================================================ -- Insert default trading engine configuration (moderate tier defaults) +-- Use a singleton pattern: only insert if no rows exist INSERT INTO trading_engine_config ( enabled, paused, risk_tier, reserve_siphon_pct, polling_interval_seconds, gradual_entry_tranches, @@ -339,7 +340,8 @@ INSERT INTO trading_engine_config ( notification_sms_enabled, notification_email_enabled, notification_rate_limit_sms_per_hour, notification_rate_limit_email_per_hour, notification_daily_summary_time -) VALUES ( +) +SELECT FALSE, FALSE, 'moderate', 0.20, 60, 3, 30.0, 15, @@ -358,8 +360,9 @@ INSERT INTO trading_engine_config ( FALSE, FALSE, 10, 20, '16:30' -); +WHERE NOT EXISTS (SELECT 1 FROM trading_engine_config); --- Insert initial reserve pool ledger entry with zero balance +-- Insert initial reserve pool ledger entry with zero balance (only if empty) INSERT INTO reserve_pool_ledger (amount, balance_after, trigger_type, notes) -VALUES (0.0, 0.0, 'initial', 'Initial reserve pool entry'); +SELECT 0.0, 0.0, 'initial', 'Initial reserve pool entry' +WHERE NOT EXISTS (SELECT 1 FROM reserve_pool_ledger); diff --git a/services/trading/app.py b/services/trading/app.py index ff6f234..518c3e0 100644 --- a/services/trading/app.py +++ b/services/trading/app.py @@ -50,6 +50,13 @@ class ConfigUpdateRequest(BaseModel): absolute_position_cap: Optional[float] = None active_pool_minimum: Optional[float] = None micro_trading_enabled: Optional[bool] = None + max_open_positions: Optional[int] = None + + +class CapitalRequest(BaseModel): + """Body for PUT /api/trading/capital.""" + + initial_capital: float class BacktestRequest(BaseModel): @@ -255,6 +262,60 @@ async def resume_engine() -> dict[str, bool]: return {"paused": False} +@app.put("/api/trading/capital") +async def set_capital(body: CapitalRequest) -> dict[str, Any]: + """Set or reset the paper trading capital. + + Allocates the given amount as initial capital, splitting between + active pool and reserve pool based on the current reserve_siphon_pct. + """ + if engine is None: + raise HTTPException(503, "Engine not initialised") + + capital = body.initial_capital + if capital <= 0: + raise HTTPException(400, "initial_capital must be positive") + + reserve_pct = engine.config.reserve_siphon_pct + reserve = capital * reserve_pct + active = capital - reserve + + ps = engine.portfolio_state + previous = { + "total_value": ps.total_value if ps else 0.0, + "active_pool": ps.active_pool if ps else 0.0, + "reserve_pool": ps.reserve_pool if ps else 0.0, + } + + from services.trading.models import PortfolioState + engine.portfolio_state = PortfolioState( + total_value=capital, + cash=capital, + active_pool=active, + reserve_pool=reserve, + ) + + # Record in reserve pool ledger + if engine.pool: + try: + await engine.pool.execute( + "INSERT INTO reserve_pool_ledger (amount, balance_after, trigger_type, notes) " + "VALUES ($1, $2, 'capital_reset', $3)", + reserve, reserve, + f"Capital set to ${capital:,.2f} (active=${active:,.2f}, reserve=${reserve:,.2f})", + ) + except Exception: + pass # Non-critical + + return { + "initial_capital": capital, + "active_pool": active, + "reserve_pool": reserve, + "reserve_siphon_pct": reserve_pct, + "previous": previous, + } + + # --------------------------------------------------------------------------- # Decision Audit Trail # ---------------------------------------------------------------------------