- Database migration 018 with 13 tables for trading engine state - Trading engine service (services/trading/) with 12 pure computation modules: position sizer, stop-loss manager, reserve pool, circuit breaker, risk tier controller, correlation matrix, tax lots, trading window, gradual entry, notifications, micro-trading, backtester - Core TradingEngine with pre-trade evaluation pipeline and integration wiring - FastAPI HTTP service with 14 endpoints (health, config, decisions, metrics, backtest) - Performance tracker with Sharpe ratio, drawdown, profit factor computation - 194 Python tests (165 property-based + 29 integration) - Frontend: 13 TanStack Query hooks, 7 dashboard panels, tabbed Trading Engine page - Helm chart entry, network policy, nginx proxy, ingress for trading-engine - Shared infrastructure: enums, Redis keys, TradingConfig in AppConfig
57 KiB
Autonomous Trading Engine — Design
Overview
This design adds a fully autonomous trading engine to the Stonks Oracle platform. The engine is a new service (services/trading/) that consumes actionable recommendations from the existing three-layer signal aggregation pipeline, applies confidence-based position sizing with reserve pool management, enforces dynamic stop-loss/take-profit levels, manages circuit breakers, and submits orders through the existing Broker Service queue.
The engine is designed for a developer allocating ~$500 in paper trading capital. All buy/sell decisions are made autonomously based on signal confidence, risk tier configuration, portfolio state, and market conditions. The architecture supports switching from paper to live trading by changing a single configuration flag.
Design Rationale
- Extend, don't replace: The trading engine sits on top of the existing recommendation engine, broker service, and risk engine. It consumes recommendations via PostgreSQL polling and submits orders to the existing
stonks:queue:broker_ordersRedis queue. - Fail-safe by default: Circuit breakers halt trading on abnormal losses. The reserve pool is untouchable by normal trading. Stop-losses are mandatory on every position. The engine fails closed — if it can't determine portfolio state, it stops trading.
- Full audit trail: Every decision (act or skip) is persisted with the complete reasoning chain — recommendation ID, position sizing inputs, risk tier, portfolio heat, correlation checks, sector exposure, and circuit breaker status.
- Incremental entry: Positions above a threshold are built gradually via tranches, allowing the engine to abort if conditions change between fills.
- Adaptive risk: Risk tiers auto-adjust based on trailing performance metrics and reserve pool health, shifting conservative after losses and aggressive after sustained wins.
- Notification-driven awareness: Critical events (circuit breaker triggers, risk tier changes, large trades) push SMS via AWS SNS and email via Gmail API so the operator doesn't need to watch the dashboard.
Architecture
The trading engine introduces one new Kubernetes deployment (trading-engine) and extends the existing Query API and Dashboard. It communicates with existing services through PostgreSQL, Redis queues, and the Alpaca broker adapter.
flowchart TD
subgraph Existing["Existing Services"]
REC[Recommendation Engine]
BS[Broker Service]
BA[Broker Adapter / Alpaca]
RE[Risk Engine]
AGG[Aggregation Engine]
end
subgraph TradingEngine["Trading Engine Service (new)"]
DL[Decision Loop]
PS[Position Sizer]
SLM[Stop-Loss Manager]
CB[Circuit Breaker]
RPC[Reserve Pool Controller]
RTC[Risk Tier Controller]
PR[Portfolio Rebalancer]
PT[Performance Tracker]
BT[Backtester]
NS[Notification Service]
MT[Micro-Trading Module]
end
subgraph Storage["PostgreSQL"]
TEC[trading_engine_config]
RPL[reserve_pool_ledger]
RTH[risk_tier_history]
CBE[circuit_breaker_events]
TD[trading_decisions]
PSL[position_stop_levels]
PSN[portfolio_snapshots]
BTR[backtest_runs / backtest_trades]
TL[tax_lots]
EC[earnings_calendar]
CMC[correlation_matrix_cache]
NTF[notifications]
end
subgraph External["External Services"]
SNS[AWS SNS]
GMAIL[Gmail API]
end
subgraph Dashboard["Dashboard (extended)"]
TCP[Trading Control Panel]
PCP[Portfolio Composition]
THP[Trade History]
PFP[Performance Charts]
BTP[Backtesting Panel]
MTP[Micro-Trading Panel]
NTP[Notification Preferences]
end
REC -->|recommendations table| DL
AGG -->|macro events| DL
DL -->|order jobs| BS
BS -->|orders| BA
DL --> PS
DL --> SLM
DL --> CB
DL --> RPC
DL --> RTC
DL --> PR
DL --> PT
DL --> NS
DL --> MT
NS --> SNS
NS --> GMAIL
PT --> PSN
BT --> BTR
DL --> TD
SLM --> PSL
RPC --> RPL
RTC --> RTH
CB --> CBE
Data Flow
-
Recommendation Polling: The Decision Loop polls the
recommendationstable for new actionable recommendations (action= buy/sell,mode= paper_eligible/live_eligible). Each recommendation ID is checked against a Redis deduplication set (TTL 24h) to ensure at-most-once processing. -
Pre-Trade Evaluation: For each new recommendation, the engine evaluates: circuit breaker status, trading window constraints, risk tier confidence threshold, portfolio heat capacity, sector exposure limits, correlation constraints, and earnings calendar proximity.
-
Position Sizing: The Position Sizer computes the dollar allocation using
base_allocation_pct * (confidence / confidence_threshold) * risk_tier_multiplier, clamped to the risk tier's max_position_pct of the Active Pool. Correlation and sector adjustments reduce the allocation further if needed. -
Order Submission: The engine generates an order job payload (matching the existing broker queue schema) and pushes it to
stonks:queue:broker_orders. The existing Broker Service handles risk evaluation, Alpaca submission, and persistence. -
Stop-Loss/Take-Profit Management: The Stop-Loss Manager computes initial levels on position open (ATR-based) and re-evaluates every 5 minutes during market hours. Price crossings trigger immediate sell orders.
-
Reserve Pool Siphoning: When the Broker Service reports a filled sell order that closes a position at a profit, the engine transfers a configurable percentage of the realized profit into the Reserve Pool.
-
Performance Tracking: The Performance Tracker computes portfolio metrics every 5 minutes during market hours and persists daily snapshots at market close.
-
Adaptive Adjustment: The Risk Tier Controller evaluates performance daily at market close and adjusts the active tier based on win rate, Sharpe ratio, drawdown, and reserve pool health.
-
Notifications: Critical events (circuit breaker triggers, tier changes, large trades) are pushed via AWS SNS (SMS) and Gmail API (email). Daily and weekly summaries are sent on schedule.
Components and Interfaces
Decision Loop
Location: services/trading/engine.py
The core autonomous loop that drives all trading activity. Runs as an asyncio task within the trading engine service.
class TradingEngine:
"""Main autonomous trading engine.
Manages the decision loop, coordinates all sub-components,
and maintains runtime state.
"""
def __init__(
self,
pool: asyncpg.Pool,
redis: aioredis.Redis,
config: TradingEngineConfig,
) -> None: ...
async def start(self) -> None:
"""Load portfolio state, enter decision loop."""
async def stop(self) -> None:
"""Graceful shutdown — cancel pending tranches, persist state."""
async def decision_loop(self) -> None:
"""Main loop: poll recommendations, evaluate, size, submit."""
async def poll_recommendations(self) -> list[dict]:
"""Fetch new actionable recommendations not yet processed."""
async def evaluate_recommendation(self, rec: dict) -> TradingDecision:
"""Run all pre-trade checks and produce a decision record."""
async def execute_decision(self, decision: TradingDecision) -> None:
"""Submit order(s) to broker queue, handle gradual entry."""
Polling Query: Fetches from recommendations where action IN ('buy', 'sell') AND mode IN ('paper_eligible', 'live_eligible') AND generated_at > last_poll_timestamp, ordered by confidence DESC.
Deduplication: Each recommendation ID is checked against Redis key stonks:dedupe:trading:{recommendation_id} with a 24-hour TTL. Processed recommendations are marked immediately before evaluation to prevent double-processing on restart.
Startup Sequence:
- Load
trading_engine_configfrom PostgreSQL - Load active risk tier parameters
- Sync portfolio state from Broker Service (positions, account balance)
- Load reserve pool balance from
reserve_pool_ledger - Load circuit breaker status from
circuit_breaker_events - Load open stop-loss/take-profit levels from
position_stop_levels - Enter decision loop
Position Sizer
Location: services/trading/position_sizer.py
Computes the dollar allocation and share quantity for a trade.
@dataclass
class PositionSizeResult:
dollar_amount: float
share_quantity: int # whole shares, rounded down
allocation_pct: float # percentage of Active Pool
adjustments: list[str] # reasons for any reductions
rejected: bool # True if allocation is zero
rejection_reason: str # why rejected, if applicable
class PositionSizer:
def compute(
self,
confidence: float,
ticker: str,
sector: str,
current_price: float,
active_pool: float,
risk_tier: RiskTierConfig,
portfolio_state: PortfolioState,
correlation_matrix: dict[tuple[str, str], float],
earnings_calendar: dict[str, datetime],
) -> PositionSizeResult: ...
Sizing Formula:
raw_pct = base_allocation_pct * (confidence / risk_tier.min_confidence) * risk_tier.multiplier
clamped_pct = min(raw_pct, risk_tier.max_position_pct)
dollar_amount = active_pool * clamped_pct
dollar_amount = min(dollar_amount, absolute_position_cap)
Adjustment Pipeline (applied sequentially):
- Confidence gate: If
confidence < risk_tier.min_confidence, reject with zero allocation. - Correlation reduction: Compute weighted average correlation with existing positions. If avg > 0.5, reduce proportionally. If avg > 0.8, reject entirely.
- Sector exposure reduction: If adding this position would push sector exposure above
risk_tier.max_sector_pct, reduce to fit within limit. - Diversification bonus: If portfolio holds < 3 sectors, apply 1.2x multiplier for under-represented sectors.
- Earnings proximity: If earnings within 3 trading days, reduce by 50%. If within 1 day, reject new entries.
- Portfolio heat check: If current heat + new position heat would exceed
risk_tier.max_portfolio_heat, reject. - Active pool minimum: If Active Pool < $100, reject new entries (exits only).
- Absolute cap: Enforce configurable absolute dollar cap (default $50 for $500 portfolio, scales linearly).
- Share rounding: Round down to whole shares. If quantity = 0, reject.
Stop-Loss Manager
Location: services/trading/stop_loss_manager.py
Computes and maintains dynamic stop-loss and take-profit levels for every open position.
@dataclass
class StopLevels:
stop_loss_price: float
take_profit_price: float
trailing_stop_active: bool
atr_value: float
atr_multiplier: float
reward_risk_ratio: float
last_updated: datetime
class StopLossManager:
async def compute_initial_levels(
self, entry_price: float, atr: float, risk_tier: RiskTierConfig,
) -> StopLevels: ...
async def re_evaluate_levels(
self, position: OpenPosition, current_price: float, atr: float,
risk_tier: RiskTierConfig,
) -> StopLevels: ...
async def check_price_crossings(
self, positions: list[OpenPosition], prices: dict[str, float],
) -> list[StopTrigger]: ...
Initial Stop-Loss: entry_price - (ATR_14 * risk_tier.stop_loss_atr_multiplier)
Initial Take-Profit: entry_price + (stop_distance * risk_tier.reward_risk_ratio)
Trailing Stop: When price moves favorably by > 50% of take-profit distance, move stop-loss to entry price (breakeven).
Re-evaluation: Every 5 minutes during market hours (configurable). Adjusts if ATR has changed materially (> 10% shift) or if signal conditions have changed.
Price Data Unavailability: If price data is unavailable for > 15 minutes during market hours, close the position as a safety measure.
Circuit Breaker
Location: services/trading/circuit_breaker.py
Safety mechanism that halts trading under abnormal conditions.
@dataclass
class CircuitBreakerState:
active: bool
trigger_type: str | None # daily_loss | single_position | volatility
triggered_at: datetime | None
cooldown_expires: datetime | None
ticker_cooldowns: dict[str, datetime] # per-ticker re-entry cooldowns
class CircuitBreaker:
def check_daily_loss(self, daily_pnl: float, portfolio_value: float) -> bool: ...
def check_single_position(self, position_loss_pct: float, ticker: str) -> bool: ...
def check_volatility(self, stop_loss_hits: list[datetime]) -> bool: ...
def is_ticker_cooled_down(self, ticker: str) -> bool: ...
Triggers:
- Daily loss: Portfolio drops > 5% (configurable) in a single day → halt all new orders.
- Single position loss: Position loses > 15% (configurable) of entry value → immediate close + 48h ticker cooldown.
- Volatility spike: 3+ positions hit stop-losses within 30 minutes → pause all trading for 2 hours (configurable).
Cooldown: Circuit breakers auto-expire after their configured duration. Resumption is logged and notified.
Reserve Pool Controller
Location: services/trading/reserve_pool.py
Manages the untouchable cash reserve that grows from realized profits.
@dataclass
class ReservePoolState:
balance: float
total_deposits: float
total_withdrawals: float
last_updated: datetime
class ReservePoolController:
async def siphon_profit(self, realized_profit: float) -> float:
"""Transfer configured percentage of profit to reserve. Returns amount transferred."""
async def emergency_liquidate(self, reason: str) -> float:
"""Release reserve into active pool during emergency. Returns amount released."""
def compute_active_pool(self, total_portfolio_value: float) -> float:
"""Active Pool = total_portfolio_value - reserve_pool_balance."""
Siphon Rate: Default 20% of realized profit on each profitable position close. High-Water Mark: When reserve exceeds 30% of total portfolio, signal Risk Tier Controller to consider upgrading. Emergency Liquidation: When drawdown exceeds 40% of initial capital, liquidate reserve into active pool, log event, shift to conservative tier.
Risk Tier Controller
Location: services/trading/risk_tier_controller.py
Evaluates performance metrics and auto-adjusts the active risk tier.
@dataclass
class RiskTierConfig:
name: str # conservative | moderate | aggressive
min_confidence: float
max_position_pct: float
stop_loss_atr_multiplier: float
reward_risk_ratio: float
max_sector_pct: float
max_portfolio_heat: float
class RiskTierController:
async def evaluate(self, metrics: PerformanceMetrics, reserve_pct: float) -> RiskTierConfig | None:
"""Evaluate whether tier should change. Returns new tier or None."""
Downgrade Conditions (any triggers downgrade):
- Trailing 30-day win rate < 40%
- Current drawdown > 15%
Upgrade Conditions (all must be true):
- Trailing 30-day win rate > 55%
- Reserve pool > 20% of total portfolio
- Current drawdown < 5%
Evaluation Frequency: Daily at market close (configurable).
Portfolio Rebalancer
Location: services/trading/rebalancer.py
Periodically evaluates portfolio concentration and generates rebalancing orders.
class PortfolioRebalancer:
async def evaluate(
self, positions: list[OpenPosition], risk_tier: RiskTierConfig,
active_pool: float,
) -> list[RebalanceOrder]: ...
Schedule: Weekly at market open on Monday (configurable). Actions: Generate partial sell orders for over-concentrated positions (single stock > max_position_pct, sector > max_sector_pct). Sells lowest-confidence positions first within over-exposed sectors. Constraints: Respects trading window and circuit breaker status. Maximum 10 open positions (configurable).
Performance Tracker
Location: services/trading/performance_tracker.py
Computes and persists real-time portfolio metrics.
@dataclass
class PerformanceMetrics:
total_portfolio_value: float
active_pool: float
reserve_pool: float
unrealized_pnl: float
realized_pnl: float
daily_pnl: float
win_count: int
loss_count: int
win_rate: float
avg_win: float
avg_loss: float
profit_factor: float
sharpe_ratio: float # annualized, trailing 30d daily returns
max_drawdown: float
current_drawdown_pct: float
portfolio_heat: float
computed_at: datetime
class PerformanceTracker:
async def compute_metrics(self) -> PerformanceMetrics: ...
async def record_trade(self, trade: ClosedTrade) -> None: ...
async def persist_daily_snapshot(self) -> None: ...
Computation Interval: Every 5 minutes during market hours.
Sharpe Ratio: Annualized using daily returns over trailing 30 days: (mean_daily_return / std_daily_return) * sqrt(252).
Per-Trade Metrics: Entry price, exit price, hold duration, P&L amount/percentage, recommendation ID.
Backtester
Location: services/trading/backtester.py
Replays historical recommendations to simulate trading strategy performance.
@dataclass
class BacktestConfig:
start_date: date
end_date: date
initial_capital: float
risk_tier: str # conservative | moderate | aggressive
@dataclass
class BacktestResult:
backtest_id: str
config: BacktestConfig
total_return: float
sharpe_ratio: float
max_drawdown: float
win_rate: float
profit_factor: float
trade_count: int
trade_log: list[dict]
equity_curve: list[dict] # [{date, portfolio_value}]
class Backtester:
async def run(self, config: BacktestConfig) -> BacktestResult: ...
Simulation: Replays the full decision logic (position sizing, stop-loss/take-profit, circuit breakers, reserve pool, rebalancing) using historical recommendations and price data from existing tables.
API: POST /api/trading/backtest to launch, returns backtest_id. Results retrievable via GET /api/trading/backtest/{backtest_id}.
Notification Service
Location: services/trading/notifications.py
Sends alerts via SMS (AWS SNS) and email (Gmail API).
class NotificationService:
async def send_alert(self, event_type: str, message: str) -> None:
"""Send via all enabled channels."""
async def send_daily_summary(self, metrics: PerformanceMetrics) -> None:
async def send_weekly_digest(self, weekly_metrics: dict) -> None:
Channels:
- SMS: AWS SNS with configurable topic ARN and phone number. Auth via
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYenv vars. - Email: Gmail API with OAuth2 (client ID, client secret, refresh token). Configurable sender and recipient.
Rate Limiting: Max 10 SMS/hour, 20 emails/hour (configurable). Prevents notification storms during volatile markets.
Retry: Up to 3 retries with exponential backoff on delivery failure. Failures are logged but never block trading operations.
Event Types: circuit_breaker_triggered, circuit_breaker_resumed, risk_tier_changed, emergency_liquidation, large_trade_pnl, daily_summary, weekly_digest.
Micro-Trading Module
Location: services/trading/micro_trading.py
Optional fast-cycle trading that evaluates intraday signals.
class MicroTradingModule:
async def poll_intraday_signals(self) -> list[dict]:
"""Fetch intraday and 1d trend window signals."""
async def evaluate_micro_trade(self, signal: dict) -> TradingDecision: ...
Polling: Every 5 minutes during market hours when enabled (configurable). Allocation Cap: 3% of Active Pool per micro-trade (lower than standard). Daily Limit: Max 10 micro-trades per day (configurable). Stop-Loss: 1.0x ATR (tighter than standard tier multiplier). Max Hold: 2 hours — auto-close at market price after expiry. Metrics: Tracked separately from standard trades.
Trading Engine HTTP Service
Location: services/trading/app.py
FastAPI application exposing health, status, configuration, and trading API endpoints.
# Health and readiness
GET /health # liveness probe
GET /ready # readiness (portfolio loaded, loop active)
# Engine control
GET /api/trading/status # current engine state
PUT /api/trading/config # update configuration
POST /api/trading/pause # pause trading
POST /api/trading/resume # resume trading
# Decision audit
GET /api/trading/decisions # recent decisions (paginated, filterable)
# Performance
GET /api/trading/metrics # current performance metrics
GET /api/trading/metrics/history # historical daily snapshots
# Backtesting
POST /api/trading/backtest # launch backtest
GET /api/trading/backtest/{id} # retrieve results
# Notifications
GET /api/trading/notifications/config # notification preferences
PUT /api/trading/notifications/config # update preferences
GET /api/trading/notifications/history # recent notifications
Correlation Matrix
Location: services/trading/correlation.py
Computes and caches price correlation coefficients between tracked companies.
class CorrelationMatrix:
async def refresh(self, pool: asyncpg.Pool) -> None:
"""Recompute from trailing 90-day price history. Run daily."""
def get_correlation(self, ticker_a: str, ticker_b: str) -> float:
"""Return correlation coefficient, 0.0 if unknown."""
def get_portfolio_correlation(
self, candidate: str, positions: list[str],
) -> float:
"""Weighted average correlation with existing portfolio."""
Data Source: Market data tables (daily close prices) for all tracked companies.
Refresh: Daily, cached in correlation_matrix_cache table and in-memory.
Tax-Loss Harvesting Tracker
Location: services/trading/tax_lots.py
Tracks cost basis and flags wash sale risks.
class TaxLotTracker:
async def record_entry(self, ticker: str, quantity: int, price: float, date: date) -> str:
"""Create a tax lot record. Returns tax_lot_id."""
async def close_lots_fifo(self, ticker: str, quantity: int, exit_price: float, date: date) -> list[ClosedLot]:
"""Close lots using FIFO method. Returns closed lot details with P&L."""
async def check_wash_sale(self, ticker: str, loss_date: date) -> bool:
"""Check 30-day window before and after for same-ticker purchases."""
Data Models
New PostgreSQL Tables (Migration 018)
trading_engine_config
CREATE TABLE trading_engine_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
enabled BOOLEAN NOT NULL DEFAULT FALSE,
paused BOOLEAN NOT NULL DEFAULT FALSE,
risk_tier VARCHAR(20) NOT NULL DEFAULT 'moderate',
reserve_siphon_pct FLOAT NOT NULL DEFAULT 0.20,
polling_interval_seconds INTEGER NOT NULL DEFAULT 60,
gradual_entry_tranches INTEGER NOT NULL DEFAULT 3,
gradual_entry_threshold_dollars FLOAT NOT NULL DEFAULT 30.0,
gradual_entry_interval_minutes INTEGER NOT NULL DEFAULT 15,
trading_window_start_minutes INTEGER NOT NULL DEFAULT 15,
trading_window_end_minutes INTEGER NOT NULL DEFAULT 15,
max_open_positions INTEGER NOT NULL DEFAULT 10,
circuit_breaker_daily_loss_pct FLOAT NOT NULL DEFAULT 0.05,
circuit_breaker_single_position_loss_pct FLOAT NOT NULL DEFAULT 0.15,
circuit_breaker_ticker_cooldown_hours INTEGER NOT NULL DEFAULT 48,
circuit_breaker_volatility_pause_hours INTEGER NOT NULL DEFAULT 2,
circuit_breaker_stop_loss_hits_threshold INTEGER NOT NULL DEFAULT 3,
circuit_breaker_stop_loss_window_minutes INTEGER NOT NULL DEFAULT 30,
active_pool_minimum FLOAT NOT NULL DEFAULT 100.0,
emergency_drawdown_threshold_pct FLOAT NOT NULL DEFAULT 0.40,
reserve_high_water_pct FLOAT NOT NULL DEFAULT 0.30,
absolute_position_cap FLOAT NOT NULL DEFAULT 50.0,
correlation_reduction_threshold FLOAT NOT NULL DEFAULT 0.5,
correlation_rejection_threshold FLOAT NOT NULL DEFAULT 0.8,
earnings_pre_window_days INTEGER NOT NULL DEFAULT 3,
earnings_post_cooldown_days INTEGER NOT NULL DEFAULT 1,
micro_trading_enabled BOOLEAN NOT NULL DEFAULT FALSE,
micro_trading_interval_seconds INTEGER NOT NULL DEFAULT 300,
micro_trading_allocation_cap_pct FLOAT NOT NULL DEFAULT 0.03,
micro_trading_max_daily INTEGER NOT NULL DEFAULT 10,
micro_trading_max_hold_minutes INTEGER NOT NULL DEFAULT 120,
notification_sms_enabled BOOLEAN NOT NULL DEFAULT FALSE,
notification_email_enabled BOOLEAN NOT NULL DEFAULT FALSE,
notification_phone_number VARCHAR(20),
notification_email_recipient VARCHAR(200),
notification_sns_topic_arn VARCHAR(300),
notification_rate_limit_sms_per_hour INTEGER NOT NULL DEFAULT 10,
notification_rate_limit_email_per_hour INTEGER NOT NULL DEFAULT 20,
notification_daily_summary_time TIME NOT NULL DEFAULT '16:30',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
reserve_pool_ledger
CREATE TABLE reserve_pool_ledger (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
amount FLOAT NOT NULL,
balance_after FLOAT NOT NULL,
trigger_type VARCHAR(30) NOT NULL,
reference_id UUID,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_trigger_type CHECK (
trigger_type IN ('profit_siphon', 'emergency_liquidation', 'manual_adjustment', 'initial')
)
);
CREATE INDEX idx_reserve_pool_ledger_created ON reserve_pool_ledger(created_at DESC);
risk_tier_history
CREATE TABLE risk_tier_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
previous_tier VARCHAR(20) NOT NULL,
new_tier VARCHAR(20) NOT NULL,
trigger_source VARCHAR(30) NOT NULL,
trigger_metrics JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_risk_tier_history_created ON risk_tier_history(created_at DESC);
circuit_breaker_events
CREATE TABLE circuit_breaker_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
trigger_type VARCHAR(30) NOT NULL,
threshold_value FLOAT,
actual_value FLOAT,
ticker VARCHAR(20),
cooldown_expires TIMESTAMPTZ,
resolved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_cb_trigger_type CHECK (
trigger_type IN ('daily_loss', 'single_position', 'volatility', 'manual')
)
);
CREATE INDEX idx_circuit_breaker_events_active ON circuit_breaker_events(created_at DESC)
WHERE resolved_at IS NULL;
trading_decisions
CREATE TABLE trading_decisions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
recommendation_id UUID REFERENCES recommendations(id),
decision VARCHAR(10) NOT NULL,
skip_reason TEXT,
ticker VARCHAR(20) NOT NULL,
computed_position_size FLOAT,
computed_share_quantity INTEGER,
risk_tier_at_decision VARCHAR(20) NOT NULL,
portfolio_heat_at_decision FLOAT,
active_pool_at_decision FLOAT,
reserve_pool_at_decision FLOAT,
circuit_breaker_status VARCHAR(20) NOT NULL DEFAULT 'inactive',
correlation_check_result JSONB DEFAULT '{}',
sector_exposure_check_result JSONB DEFAULT '{}',
earnings_proximity_flag BOOLEAN NOT NULL DEFAULT FALSE,
is_micro_trade BOOLEAN NOT NULL DEFAULT FALSE,
decision_trace JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_trading_decisions_ticker ON trading_decisions(ticker, created_at DESC);
CREATE INDEX idx_trading_decisions_rec ON trading_decisions(recommendation_id);
CREATE INDEX idx_trading_decisions_decision ON trading_decisions(decision, created_at DESC);
position_stop_levels
CREATE TABLE position_stop_levels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ticker VARCHAR(20) NOT NULL,
entry_price FLOAT NOT NULL,
stop_loss_price FLOAT NOT NULL,
take_profit_price FLOAT NOT NULL,
trailing_stop_active BOOLEAN NOT NULL DEFAULT FALSE,
atr_value FLOAT NOT NULL,
atr_multiplier FLOAT NOT NULL,
reward_risk_ratio FLOAT NOT NULL,
signal_confidence FLOAT,
is_micro_trade BOOLEAN NOT NULL DEFAULT FALSE,
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_position_stop_levels_ticker ON position_stop_levels(ticker) WHERE active = TRUE;
portfolio_snapshots
CREATE TABLE portfolio_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
snapshot_date DATE NOT NULL UNIQUE,
portfolio_value FLOAT NOT NULL,
active_pool FLOAT NOT NULL,
reserve_pool FLOAT NOT NULL,
daily_return FLOAT,
cumulative_return FLOAT,
unrealized_pnl FLOAT,
realized_pnl FLOAT,
win_count INTEGER NOT NULL DEFAULT 0,
loss_count INTEGER NOT NULL DEFAULT 0,
win_rate FLOAT,
sharpe_ratio FLOAT,
max_drawdown FLOAT,
current_drawdown_pct FLOAT,
portfolio_heat FLOAT,
risk_tier VARCHAR(20),
positions JSONB NOT NULL DEFAULT '[]',
metrics JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_portfolio_snapshots_date ON portfolio_snapshots(snapshot_date DESC);
backtest_runs
CREATE TABLE backtest_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
start_date DATE NOT NULL,
end_date DATE NOT NULL,
initial_capital FLOAT NOT NULL,
risk_tier VARCHAR(20) NOT NULL,
config JSONB NOT NULL DEFAULT '{}',
total_return FLOAT,
sharpe_ratio FLOAT,
max_drawdown FLOAT,
win_rate FLOAT,
profit_factor FLOAT,
trade_count INTEGER,
equity_curve JSONB DEFAULT '[]',
status VARCHAR(20) NOT NULL DEFAULT 'running',
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
backtest_trades
CREATE TABLE backtest_trades (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
backtest_id UUID NOT NULL REFERENCES backtest_runs(id) ON DELETE CASCADE,
ticker VARCHAR(20) NOT NULL,
side VARCHAR(10) NOT NULL,
entry_price FLOAT NOT NULL,
exit_price FLOAT,
quantity INTEGER NOT NULL,
pnl FLOAT,
pnl_pct FLOAT,
entry_date DATE NOT NULL,
exit_date DATE,
hold_duration_days INTEGER,
recommendation_id UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_backtest_trades_run ON backtest_trades(backtest_id);
tax_lots
CREATE TABLE tax_lots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ticker VARCHAR(20) NOT NULL,
quantity INTEGER NOT NULL,
cost_basis_per_share FLOAT NOT NULL,
acquisition_date DATE NOT NULL,
status VARCHAR(10) NOT NULL DEFAULT 'open',
closed_date DATE,
exit_price FLOAT,
realized_pnl FLOAT,
wash_sale_flag BOOLEAN NOT NULL DEFAULT FALSE,
wash_sale_details TEXT,
order_id UUID REFERENCES orders(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_tax_lot_status CHECK (status IN ('open', 'closed', 'washed'))
);
CREATE INDEX idx_tax_lots_ticker ON tax_lots(ticker, acquisition_date DESC);
CREATE INDEX idx_tax_lots_status ON tax_lots(status) WHERE status = 'open';
earnings_calendar
CREATE TABLE earnings_calendar (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ticker VARCHAR(20) NOT NULL,
earnings_date DATE NOT NULL,
source VARCHAR(30) NOT NULL DEFAULT 'manual',
confirmed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(ticker, earnings_date)
);
CREATE INDEX idx_earnings_calendar_date ON earnings_calendar(earnings_date);
CREATE INDEX idx_earnings_calendar_ticker ON earnings_calendar(ticker);
correlation_matrix_cache
CREATE TABLE correlation_matrix_cache (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ticker_a VARCHAR(20) NOT NULL,
ticker_b VARCHAR(20) NOT NULL,
correlation_coefficient FLOAT NOT NULL,
lookback_days INTEGER NOT NULL DEFAULT 90,
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(ticker_a, ticker_b)
);
CREATE INDEX idx_correlation_cache_tickers ON correlation_matrix_cache(ticker_a, ticker_b);
notifications
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel VARCHAR(10) NOT NULL,
event_type VARCHAR(50) NOT NULL,
message TEXT NOT NULL,
delivery_status VARCHAR(20) NOT NULL DEFAULT 'pending',
retry_count INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
delivered_at TIMESTAMPTZ,
CONSTRAINT chk_channel CHECK (channel IN ('sms', 'email')),
CONSTRAINT chk_delivery_status CHECK (
delivery_status IN ('pending', 'delivered', 'failed', 'rate_limited')
)
);
CREATE INDEX idx_notifications_created ON notifications(created_at DESC);
CREATE INDEX idx_notifications_event ON notifications(event_type, created_at DESC);
New Pydantic Schemas
Added to services/shared/schemas.py:
class TradingDecisionType(str, Enum):
ACT = "act"
SKIP = "skip"
class CircuitBreakerTriggerType(str, Enum):
DAILY_LOSS = "daily_loss"
SINGLE_POSITION = "single_position"
VOLATILITY = "volatility"
MANUAL = "manual"
class ReservePoolTriggerType(str, Enum):
PROFIT_SIPHON = "profit_siphon"
EMERGENCY_LIQUIDATION = "emergency_liquidation"
MANUAL_ADJUSTMENT = "manual_adjustment"
INITIAL = "initial"
class NotificationChannel(str, Enum):
SMS = "sms"
EMAIL = "email"
class RiskTierName(str, Enum):
CONSERVATIVE = "conservative"
MODERATE = "moderate"
AGGRESSIVE = "aggressive"
New TradingConfig in services/shared/config.py
@dataclass
class TradingConfig:
enabled: bool = False
risk_tier: str = "moderate"
reserve_siphon_pct: float = 0.20
polling_interval_seconds: int = 60
stop_loss_check_interval_seconds: int = 300
fast_stop_loss_interval_seconds: int = 60
gradual_entry_tranches: int = 3
gradual_entry_threshold_dollars: float = 30.0
absolute_position_cap: float = 50.0
active_pool_minimum: float = 100.0
emergency_drawdown_threshold_pct: float = 0.40
reserve_high_water_pct: float = 0.30
micro_trading_enabled: bool = False
micro_trading_interval_seconds: int = 300
micro_trading_allocation_cap_pct: float = 0.03
micro_trading_max_daily: int = 10
micro_trading_max_hold_minutes: int = 120
sns_topic_arn: str = ""
sns_phone_number: str = ""
gmail_sender: str = ""
gmail_recipient: str = ""
New Redis Keys
Added to services/shared/redis_keys.py:
QUEUE_TRADING_DECISIONS = "trading_decisions"
# Trading engine deduplication prefix
TRADING_DEDUPE_PREFIX = f"{PREFIX}:dedupe:trading"
# Circuit breaker state
TRADING_CB_PREFIX = f"{PREFIX}:trading:circuit_breaker"
# Notification rate limiting
TRADING_NOTIFICATION_RATE = f"{PREFIX}:trading:notification_rate"
Kubernetes Deployment
New service entry in infra/helm/stonks-oracle/values.yaml:
trading-engine:
image: ghcr.io/celesrenata/stonks-oracle/trading-engine:latest
replicas: 1
port: 8000
env:
SERVICE_CMD: "services.trading.app"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
labels:
stonks-oracle/tier: trading
Network policy: Allow ingress from query-api and dashboard on port 8000. Allow egress to PostgreSQL, Redis, and external (SNS, Gmail API).
Dashboard proxy: Add /trading/ → trading-engine:8000 to the nginx frontend proxy config.
Risk Tier Default Parameters
| Parameter | Conservative | Moderate | Aggressive |
|---|---|---|---|
| min_confidence | 0.75 | 0.55 | 0.40 |
| max_position_pct | 0.05 | 0.10 | 0.15 |
| stop_loss_atr_multiplier | 1.5 | 2.0 | 2.5 |
| reward_risk_ratio | 2.0 | 1.5 | 1.2 |
| max_sector_pct | 0.20 | 0.30 | 0.40 |
| max_portfolio_heat | 0.10 | 0.20 | 0.30 |
Correctness Properties
A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.
Property 1: Position sizing formula and invariants
For any valid confidence value, Active Pool balance, current stock price, and Risk Tier configuration, the Position Sizer SHALL:
- Return zero allocation when confidence is below the Risk Tier's min_confidence threshold
- Compute the raw allocation as
base_allocation_pct * (confidence / min_confidence) * multiplier - Clamp the allocation percentage to never exceed the Risk Tier's max_position_pct
- Clamp the dollar amount to never exceed the absolute position cap
- Round the share quantity down to whole shares
- Reject the trade (zero allocation) when the rounded share quantity is zero
Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.7
Property 2: Correlation-based allocation adjustment
For any candidate stock, existing portfolio positions, and correlation matrix, the Position Sizer SHALL:
- Reduce the allocation proportionally when the weighted average correlation between the candidate and existing positions exceeds 0.5
- Reject the trade entirely when the weighted average correlation exceeds 0.8
- Leave the allocation unchanged when the weighted average correlation is at or below 0.5
Furthermore, increasing the weighted average correlation (while holding other inputs constant) SHALL produce an allocation that is less than or equal to the previous allocation (monotonic non-increase).
Validates: Requirements 2.5, 9.2, 9.3
Property 3: Sector exposure computation and enforcement
For any set of open positions with sector labels, the sector exposure for each sector SHALL equal the sum of market values of all positions in that sector. When adding a new position would cause a sector to exceed the Risk Tier's max_sector_pct of the Active Pool, the Position Sizer SHALL reduce the allocation to fit within the limit.
Validates: Requirements 2.6, 9.4
Property 4: Diversification bonus for under-represented sectors
For any portfolio holding positions in fewer than 3 sectors, the Position Sizer SHALL apply a 1.2x allocation multiplier to trades in sectors not currently represented in the portfolio. When the portfolio holds 3 or more sectors, no diversification bonus SHALL be applied.
Validates: Requirements 9.5
Property 5: Active Pool computation invariant
For any total portfolio value and reserve pool balance where reserve pool balance is non-negative and does not exceed total portfolio value, the Active Pool SHALL equal total_portfolio_value - reserve_pool_balance.
Validates: Requirements 3.3
Property 6: Reserve pool siphon computation
For any realized profit amount and siphon percentage, the amount transferred to the Reserve Pool SHALL equal realized_profit * siphon_pct, and the reserve pool balance after the transfer SHALL equal the previous balance plus the transferred amount.
Validates: Requirements 3.1, 3.2
Property 7: Active Pool minimum halts new entries but allows exits
For any portfolio state where the Active Pool is below the configured minimum threshold, the Trading Engine SHALL reject all new position entries (buy orders) but SHALL allow position exits (sell orders) to proceed.
Validates: Requirements 3.5
Property 8: Emergency drawdown triggers reserve liquidation
For any portfolio experiencing a drawdown exceeding the emergency threshold percentage of initial capital, the Trading Engine SHALL liquidate the entire Reserve Pool into the Active Pool, and the resulting risk tier SHALL be conservative.
Validates: Requirements 3.6
Property 9: Stop-loss and take-profit initial computation
For any entry price, ATR value, and Risk Tier configuration, the Stop Loss Manager SHALL compute:
- Stop-loss price =
entry_price - (ATR * stop_loss_atr_multiplier)(for long positions) - Take-profit price =
entry_price + (stop_distance * reward_risk_ratio)wherestop_distance = entry_price - stop_loss_price
The stop-loss price SHALL always be below the entry price, and the take-profit price SHALL always be above the entry price.
Validates: Requirements 4.1, 4.2
Property 10: Price crossing triggers immediate sell
For any open position with defined stop-loss and take-profit levels, when the current price is at or below the stop-loss price OR at or above the take-profit price, the Trading Engine SHALL generate a market sell order for that position.
Validates: Requirements 4.4, 4.5
Property 11: Trailing stop activation at 50% of take-profit distance
For any open position where the current price has moved favorably by more than 50% of the distance from entry price to take-profit price, the Stop Loss Manager SHALL move the stop-loss to the entry price (breakeven). The trailing stop SHALL NOT activate when the favorable move is 50% or less.
Validates: Requirements 4.6
Property 12: Risk tier auto-adjustment conditions
For any set of performance metrics (trailing 30-day win rate, current drawdown, reserve pool percentage):
- The Risk Tier Controller SHALL downgrade by one level when win rate < 40% OR drawdown > 15%
- The Risk Tier Controller SHALL upgrade by one level when win rate > 55% AND reserve pool > 20% of total portfolio AND drawdown < 5%
- The Risk Tier Controller SHALL make no change when neither condition is met
- The tier SHALL never go below conservative or above aggressive
Validates: Requirements 5.3, 5.4
Property 13: Circuit breaker activation
For any portfolio state:
- When daily portfolio loss exceeds the configured daily_loss_pct threshold, the circuit breaker SHALL activate with trigger_type = 'daily_loss'
- When a single position's loss exceeds the configured single_position_loss_pct of its entry value, the circuit breaker SHALL close that position and apply a ticker cooldown
- When 3 or more positions hit stop-losses within a 30-minute window, the circuit breaker SHALL activate with trigger_type = 'volatility'
When any circuit breaker is active, the Trading Engine SHALL reject all new order submissions.
Validates: Requirements 6.1, 6.2, 6.3
Property 14: Circuit breaker cooldown expiry
For any circuit breaker event with a configured cooldown duration, the circuit breaker SHALL transition from active to resolved when the current time exceeds triggered_at + cooldown_duration. Before expiry, the circuit breaker SHALL remain active.
Validates: Requirements 6.5
Property 15: Stop tightening during high-severity events
For any open position during an active high-severity macro event, the Stop Loss Manager SHALL compute tightened stop-loss levels using a reduced ATR multiplier (default: 0.5x the normal multiplier). The tightened stop-loss SHALL be closer to the current price than the normal stop-loss.
Validates: Requirements 7.2
Property 16: Multiple declining positions halts new entries
For any portfolio state where more than 50% of open positions have negative unrealized P&L exceeding 2% of their entry value, the Trading Engine SHALL reduce new position entries to zero while continuing to manage existing positions.
Validates: Requirements 7.5
Property 17: Portfolio rebalancing generates correct sell orders
For any portfolio where a single stock exceeds the Risk Tier's max_position_pct of the Active Pool, the Portfolio Rebalancer SHALL generate a partial sell order that, if executed, would bring the position within the limit. When a sector exceeds max_sector_pct, the Rebalancer SHALL generate sell orders targeting the lowest-confidence positions in that sector first.
Validates: Requirements 8.2, 8.3
Property 18: Maximum open positions enforcement
For any portfolio at the configured maximum number of open positions, the Trading Engine SHALL reject new position entries. The portfolio SHALL never hold more than the configured maximum number of simultaneous open positions.
Validates: Requirements 8.4
Property 19: Earnings proximity adjustments
For any stock with an earnings date within 3 trading days, the Position Sizer SHALL reduce the maximum position size by 50% and the Stop Loss Manager SHALL tighten stop-losses by the configured factor (default 0.7x ATR multiplier). When earnings are within 1 trading day, the Trading Engine SHALL reject all new position entries for that stock.
Validates: Requirements 10.2, 10.3
Property 20: Trading window determination
For any timestamp during US market hours, the Trading Engine SHALL classify it as within the Trading Window if and only if it falls between 9:45 AM ET (15 minutes after open) and 3:45 PM ET (15 minutes before close). Timestamps outside this range SHALL be classified as outside the Trading Window, and new orders SHALL not be submitted.
Validates: Requirements 11.1
Property 21: Gradual entry tranche splitting
For any position size exceeding the gradual entry threshold (default: min($30, 5% of Active Pool)), the Trading Engine SHALL split the order into the configured number of tranches (default: 3), each of approximately equal size, and all tranches SHALL reference the same parent trading decision ID.
Validates: Requirements 11.3, 11.5
Property 22: Tax lot FIFO ordering
For any sequence of buy and sell transactions for the same ticker, closing lots SHALL follow FIFO order — the earliest-acquired open lot is closed first. The realized P&L for each closed lot SHALL equal (exit_price - cost_basis_per_share) * quantity.
Validates: Requirements 12.4
Property 23: Wash sale detection within 30-day window
For any position closed at a loss, the Tax Lot Tracker SHALL flag it as a potential wash sale if the same ticker was purchased within 30 days before the loss date or if a pending recommendation suggests purchase within 30 days after the loss date.
Validates: Requirements 12.2, 12.3
Property 24: Portfolio heat computation and threshold enforcement
For any set of open positions with entry prices and stop-loss levels, Portfolio Heat SHALL equal the sum of position_value * (entry_price - stop_loss_price) / entry_price across all positions. When Portfolio Heat exceeds the Risk Tier's max_portfolio_heat percentage of the Active Pool, the Position Sizer SHALL reject new position entries.
Validates: Requirements 13.1, 13.2
Property 25: Proactive stop tightening at 80% heat threshold
For any portfolio state where Portfolio Heat exceeds 80% of the Risk Tier's max_portfolio_heat threshold, the Stop Loss Manager SHALL tighten stop-losses on the lowest-confidence positions first, reducing overall heat. Positions with lower confidence SHALL have their stops tightened before positions with higher confidence.
Validates: Requirements 13.3
Property 26: Performance metrics computation
For any set of closed trades with entry prices, exit prices, and hold durations, the Performance Tracker SHALL compute: win_rate = wins / total_trades, profit_factor = gross_profits / gross_losses (or infinity if no losses), and Sharpe ratio = (mean_daily_return / std_daily_return) * sqrt(252). All computed metrics SHALL be consistent with the underlying trade data.
Validates: Requirements 14.1, 14.2
Property 27: Recommendation deduplication (idempotence)
For any recommendation ID, processing it through the Trading Engine a second time SHALL be a no-op — no new trading decision record SHALL be created and no order SHALL be submitted. The deduplication check SHALL use the Redis key stonks:dedupe:trading:{recommendation_id}.
Validates: Requirements 1.5
Property 28: Trading decision record completeness and traceability
For any recommendation evaluated by the Trading Engine, a trading_decision record SHALL be persisted containing: recommendation_id, decision (act/skip), skip_reason (if skipped), computed_position_size, risk_tier_at_decision, portfolio_heat_at_decision, active_pool_at_decision, reserve_pool_at_decision, circuit_breaker_status, and timestamp. When the decision is "act", the resulting order job SHALL contain the trading_decision_id.
Validates: Requirements 1.4, 17.1, 17.2
Property 29: Persistence round-trip for trading engine state
For any valid trading engine configuration, reserve pool ledger entry, risk tier history record, circuit breaker event, portfolio snapshot, or backtest result, persisting it to PostgreSQL and reading it back SHALL produce an equivalent object with all fields preserved.
Validates: Requirements 3.2, 4.7, 5.5, 6.4, 14.3, 15.4, 16.1
Property 30: Notification rate limiting
For any sequence of notification requests within a one-hour window, the Notification Service SHALL deliver at most the configured maximum per channel (default: 10 SMS, 20 emails per hour). Notifications exceeding the rate limit SHALL be marked as 'rate_limited' and not delivered.
Validates: Requirements 19.7
Property 31: Micro-trade parameter constraints
For any micro-trade, the Position Sizer SHALL enforce: allocation does not exceed micro_trading_allocation_cap_pct of Active Pool (default 3%), stop-loss is computed at 1.0x ATR (not the standard tier multiplier), and take-profit is at 1.5x the stop distance. The daily micro-trade count SHALL not exceed the configured maximum (default 10).
Validates: Requirements 20.3, 20.4, 20.5
Property 32: Micro-trade auto-close after max hold duration
For any micro-trade position that has been open longer than the configured maximum hold duration (default: 2 hours), the Trading Engine SHALL close the position at market price regardless of current P&L.
Validates: Requirements 20.6
Property 33: Micro-trade metrics tracked separately
For any set of trades containing both standard and micro-trades, the Performance Tracker SHALL compute micro-trade metrics (win rate, average P&L, count, contribution to total P&L) independently from standard trade metrics. The two sets of metrics SHALL not contaminate each other.
Validates: Requirements 20.7
Property 34: Micro-trades respect all existing constraints
For any micro-trade evaluation, the Trading Engine SHALL enforce the same constraints as standard trades: Trading Window, Circuit Breakers, Portfolio Heat limits, correlation checks, sector exposure limits, and earnings proximity rules.
Validates: Requirements 20.10
Property 35: Configuration change audit trail
For any change to the trading engine configuration (whether via API, auto-adjustment, or manual override), the system SHALL persist an audit event containing the previous configuration, new configuration, and the source of the change.
Validates: Requirements 16.6
Property 36: Backtester produces equivalent metrics
For any set of historical trades, the Backtester's metric computation (total return, Sharpe ratio, max drawdown, win rate, profit factor) SHALL produce the same results as the Performance Tracker when given the same trade data.
Validates: Requirements 15.3
Error Handling
Decision Loop Failures
- If the recommendation polling query fails, the engine logs the error and retries on the next polling cycle. No decisions are made during the failed cycle.
- If portfolio state cannot be loaded from the Broker Service on startup, the engine enters a degraded state (readiness probe returns unhealthy) and retries every 30 seconds until state is available.
- If a single recommendation evaluation fails (exception in position sizing, correlation lookup, etc.), the engine logs the error, persists a skip decision with reason "evaluation_error", and continues to the next recommendation.
Position Sizing Failures
- If the correlation matrix is unavailable (cache miss, DB error), the Position Sizer treats all correlations as 0.0 (no reduction) and logs a warning. This is a fail-open decision for correlation only — all other checks still apply.
- If sector data is missing for a ticker, the sector exposure check is skipped with a warning. The position is still subject to all other constraints.
- If current price data is unavailable for share quantity computation, the trade is skipped with reason "price_unavailable".
Stop-Loss Manager Failures
- If ATR data cannot be fetched for a position, the Stop Loss Manager uses the last known ATR value and logs a warning.
- If price data is unavailable for more than 15 minutes during market hours, the position is closed as a safety measure (Req 4.8).
- If the stop-loss re-evaluation query fails, existing levels are preserved and the next re-evaluation cycle retries.
Circuit Breaker Failures
- Circuit breaker checks are fail-closed: if the daily P&L cannot be computed (DB error), the circuit breaker activates as a precaution.
- If circuit breaker event persistence fails, the in-memory state still prevents trading. The persistence is retried on the next cycle.
Reserve Pool Failures
- If the reserve pool ledger write fails during profit siphoning, the siphon is retried on the next profitable close. The trade itself is not affected.
- If the reserve pool balance cannot be loaded on startup, the engine treats the reserve as $0 (conservative — all capital is active pool) and logs a critical warning.
Notification Failures
- Notification delivery failures (SNS publish error, Gmail API error) trigger up to 3 retries with exponential backoff.
- Failed notifications are persisted with
delivery_status = 'failed'and the error message. - Notification failures never block or delay trading operations — they run in a separate asyncio task.
- Rate-limited notifications are persisted with
delivery_status = 'rate_limited'.
Backtester Failures
- If historical price data is missing for a date range, the backtester skips those dates and notes the gap in the result.
- If the backtester encounters an error mid-run, it persists partial results with
status = 'failed'and the error message.
Broker Queue Failures
- If pushing an order job to the Redis broker queue fails, the engine retries up to 3 times. If all retries fail, the decision is persisted as "act" with a note that queue submission failed, and the engine logs a critical error.
Graceful Degradation
- The trading engine is designed to degrade gracefully. Individual component failures (correlation matrix, earnings calendar, notification service) do not halt the core decision loop. Each component failure results in that specific check being skipped or defaulted, with appropriate logging.
Testing Strategy
Property-Based Testing
This feature is well-suited for property-based testing. The core logic — position sizing formulas, stop-loss/take-profit computation, circuit breaker threshold detection, portfolio heat calculation, risk tier evaluation, FIFO tax lot ordering, and trading window determination — consists of pure functions with clear input/output behavior and large input spaces.
Library: Hypothesis for Python property-based testing.
Configuration: Minimum 100 iterations per property test (@settings(max_examples=100)).
Tag format: Feature: autonomous-trading-engine, Property {number}: {property_text}
Each correctness property maps to one property-based test. Generators will produce:
- Random
RiskTierConfigobjects with valid parameter ranges - Random confidence values in [0, 1]
- Random Active Pool balances ($50 - $10,000)
- Random stock prices ($1 - $1,000)
- Random ATR values (proportional to price)
- Random correlation matrices with coefficients in [-1, 1]
- Random portfolio states with varying position counts, sectors, and P&L
- Random sequences of buy/sell transactions for FIFO testing
- Random timestamps across market hours for trading window testing
- Random performance metrics (win rates, drawdowns, Sharpe ratios)
- Random notification sequences for rate limiting testing
- Random earnings dates relative to current date
Unit Testing
Unit tests complement property tests for specific examples and edge cases:
- API endpoint response codes and error handling (status, config, pause/resume, decisions, backtest)
- Dashboard component rendering with mock data (trading control panel, portfolio composition, trade history, performance charts, backtesting panel, micro-trading panel, notification preferences)
- Health and readiness endpoint behavior in various engine states
- Risk tier default parameter values match specification
- Startup sequence with missing/partial state
- Gradual entry with exactly 1 tranche (below threshold)
- Circuit breaker with exactly 3 stop-losses at the 30-minute boundary
- Earnings calendar CRUD operations
- Notification daily summary and weekly digest content
Integration Testing
Integration tests verify end-to-end flows:
- Full decision cycle: recommendation → evaluation → position sizing → order submission to broker queue
- Stop-loss crossing detection → immediate sell order generation
- Reserve pool siphoning on profitable position close
- Risk tier auto-adjustment after simulated 30-day performance
- Circuit breaker trigger → trading halt → cooldown expiry → resume
- Portfolio rebalancing generating correct sell orders
- Backtester producing results from historical data
- Notification delivery via mocked SNS and Gmail clients
- Engine startup state reconstruction from PostgreSQL
- Micro-trading mode polling and evaluation cycle
- Gradual entry with re-evaluation between tranches