API was returning a flat array but frontend expects CompanyMacroImpacts
wrapper with exposure_profile and impacts fields. Also queries the
exposure_profiles table for the company's active profile.
- Trading page: added conservative/moderate/aggressive selector that
updates the trading engine config via PUT /api/trading/config
- Recommendations page: added risk tier dropdown that defaults to the
engine's current tier and filters recs by the tier's min_confidence
- Backend: added min_confidence query param to GET /api/recommendations
- Risk tier thresholds: conservative ≥0.75, moderate ≥0.55, aggressive ≥0.40
- Removed PUT /api/trading/capital (set capital) — only touched in-memory state
- Removed POST /api/trading/capital/adjust (add/withdraw) — same problem
- Reset endpoint now: liquidates Alpaca positions, cancels orders, clears DB,
then queries Alpaca for real portfolio_value to set engine capital
- Frontend: replaced CapitalCard with simple ResetCard (one button)
- Removed useSetTradingCapital and useAdjustCapital hooks
- Added cancel_all_orders() and close_all_positions() to AlpacaBrokerAdapter
- Reset endpoint creates a temporary adapter to call Alpaca DELETE /v2/orders
and DELETE /v2/positions before clearing DB and engine state
- Also clears positions table and processed_recommendation_ids on reset
- Broker reset is best-effort — DB/engine reset proceeds even if Alpaca fails
Agreement of 1-2 signals was inflating confidence to paper-eligible
levels (0.575) even with low credibility sources. Added log2-based
dampener that scales agreement contribution by unique source count,
saturating at n=7. Single signals now cap at 0.39 confidence,
2 signals at 0.49 — both correctly below paper threshold (0.50).
- Recommendation worker now resolves thesis-rewriter config from DB
and passes ollama_config to generate_recommendation. Thesis rewriting
is now active when the thesis-rewriter agent exists in ai_agents.
Refreshes config every 50 jobs.
- Event classifier now resolves its own config separately from the
document extractor via 'event-classifier' slug. Uses a separate
OllamaClient when the model differs from the extractor. Refreshes
alongside the extractor every 100 jobs.
- Document extractor was already wired (existing code).
- Added 8 unit tests for AgentConfigResolver covering: DB resolution,
variant override, not-found, DB errors, TTL caching, cache refresh,
and invalidation.
- Migration 026 and OllamaConfig now default to qwen3.5:9b instead of
llama3.1:8b. Existing deployments keep their current model (qwen3.5:9b-fast)
since the migration uses WHERE NOT EXISTS on slug.
- Event classifier system prompt expanded with macro-vs-company filtering:
explicitly instructs the model to NOT classify single-company news
(lawsuits, earnings, management changes, debt crises) as macro events.
Sets severity=low and confidence<0.3 for company-specific articles.
Reserves 'critical' severity for multi-country/global market events.
Prevents over-tagging event_types by requiring direct description.
- Updated test_system_prompt_is_concise threshold to accommodate the
expanded prompt (300 → 1000 chars).
Three distinct capital operations on the Trading Controls page:
- Set Capital: overwrites pool balances to a new amount (existing)
- Add/Withdraw: adjusts active pool by a delta without touching
positions, orders, or history. Validates sufficient balance for
withdrawals. Logged to reserve_pool_ledger as manual_adjustment.
- Reset Everything: nuclear option — deletes all positions, orders,
trading decisions, stop levels, snapshots, backtests, notifications,
and circuit breaker events, then resets capital fresh. Red button
with double-confirmation dialog.
Backend: POST /api/trading/capital/adjust and POST /api/trading/reset
Frontend: CapitalCard rebuilt with three sections and confirmation UIs
New Agents tab in the sidebar (Ops group) for viewing, editing, and
creating AI agent configurations:
Database (migration 026):
- ai_agents table: editable configs for each LLM agent (model, prompts,
temperature, tokens, retries). source='system' for built-in,
source='user' for custom. Seeds 3 system agents (Document Extractor,
Event Classifier, Thesis Rewriter) using WHERE NOT EXISTS to never
overwrite user edits across reinstalls.
- agent_performance_log table: per-invocation metrics (duration,
confidence, retries, tokens, errors) linked to agent config.
API endpoints:
- GET/POST /api/agents — list and create agents
- GET/PUT/DELETE /api/agents/{id} — view, edit, delete (system agents
can be edited but not deleted)
- GET /api/agents/{id}/performance — aggregated metrics (success rate,
avg/p95 latency, confidence, token usage)
- GET /api/agents/{id}/performance/history — hourly time series
Frontend:
- AgentsPage with sidebar list + detail panel
- Agent detail: config display, system prompt viewer, performance
dashboard with metrics cards and time-series chart
- Edit form: all config fields editable including system prompt,
model, temperature, tokens, retries
- Create form: new user-defined agents with auto-slug generation
- System agents show blue badge, user agents show green badge
Migration 023 was deleting all but the latest trend_windows row per
entity before 024 could save them to trend_history. On reinstall,
this wiped the entire history every time.
Fixed by restructuring:
- 023 now creates trend_history FIRST and copies all trend_windows
rows into it before deduplicating trend_windows down to latest-only.
Uses NOT EXISTS to avoid duplicating rows on re-runs.
- 024 is now idempotent: ensures table/indexes exist and backfills
from recommendations (last 7 days, 1 point per ticker/window/hour)
to reconstruct approximate history even if trend_windows was sparse.
Both migrations are safe to re-run on existing databases.
- New 'intraday_bars' endpoint in PolygonMarketAdapter: fetches hourly
bars for today using range_bars URL with timespan=hour, sort=asc
- Scheduler expands intraday_bars global source into per-ticker jobs
for all active companies (every 15 minutes via polling_interval)
- Migration 025 inserts the intraday source with 900s cadence
- Frontend price matching uses closest-timestamp instead of date-string
matching, with 2h tolerance for intraday and 36h for daily windows
- Bumped market price fetch limit to 200 for intraday granularity
- New GET /api/market/prices/{ticker} endpoint serving OHLCV data from
market_snapshots, deduped by bar_timestamp
- New useMarketPrices hook in frontend
- Trend chart now shows price (purple line) on a right Y axis ($)
alongside trend metrics (%) on the left Y axis
- Custom tooltip formats price as dollars, metrics as percentages
- Price line uses connectNulls for days with missing bar data
Replaced Recharts default Tooltip with formatter prop (broken in
Recharts v3 with explicit type annotations) with a custom
TrendTooltip component matching the SQL Explorer pattern. Shows
each series name, value, and color on hover.
/api/patterns/{ticker} returns {ticker, patterns, count} but
useHistoricalPatterns typed its response as HistoricalPattern[].
The .map() call on the object caused 'e.map is not a function'.
Fixed by unwrapping resp.patterns in the hook's queryFn.
Trend charts blank:
- trend_windows uses upsert (1 row per ticker/window), so charts had
at most 1 data point. Added trend_history table (migration 024) that
appends every snapshot. New /api/trends/history endpoint serves the
time series. Frontend now uses useTrendHistory for charts and
useTrends for the latest summary card.
Competitor GUIDs:
- list_competitors query returned raw company_b_id UUIDs without
joining companies table. Added LEFT JOIN with CASE to resolve the
other company's ticker and legal_name. Updated Pydantic model to
include enriched fields. Frontend fallback changed from truncated
UUID to ticker/legal_name/Unknown.
- ID mismatch: API generated a throwaway UUID while BacktestReplay
generated its own internally. Frontend polled with wrong ID and
never found the DB row. Now pre-generate ID in endpoint and pass
it to BacktestReplay.
- Field name: API returned 'backtest_id' but frontend read 'data.id'.
Unified to 'id' everywhere.
- No polling: useBacktestResult fired once and never refreshed.
Added refetchInterval that polls every 2s while status is running.
- Response shape: GET endpoint nested results under 'result' object
but frontend expected flat fields. Flattened response to match
BacktestResult type.
- Added running/failed/completed status indicators in BacktestPanel.
- Add dedup check in recommendation worker: skip generation when latest
rec for same ticker+window has identical action/mode/confidence
- Widen position sizing range (1-10% portfolio, 0.3-2% max loss) and
factor in trend strength + evidence count for differentiated sizing
- API returns only latest recommendation per ticker by default (DISTINCT ON)
to eliminate duplicate rows in the frontend list view
- Sell path: looks up existing position, sells full quantity, returns proceeds to pool
- Correlation matrix: computed from 30-day market_snapshots on startup + every 5min
- Holidays: 10 major US market holidays for 2026 checked in trading window functions
All 152 tasks across both phases are now marked complete:
- Phase 1 (1-26): pure computation modules, property tests, API, frontend, infra
- Phase 2 (27-37): live decision loop, stop-loss monitor, performance metrics,
risk tier scheduler, rebalancer, notification dispatch, backtest replay,
real DB connections, paper trading config, integration tests
Hover over any bar, line point, or scatter dot to see every column
value for that data point. The Y-axis column is highlighted in
brand color, X-axis in white, and other columns in gray. Works
for all chart types (bar, line, scatter, auto).
Adds an '✨ Auto' button that analyzes query results and picks the
best chart type and column mapping:
- Date/time column + numeric → line chart (time series)
- Categorical + numeric → bar chart (categories)
- Two numeric columns → scatter plot
- Shows detected type and column names as a label
Click Auto, run any query, and it figures out the rest.
Deploy scripts live on gremlin-1 at ~/sources/kube/stonks-oracle/,
not in the git repo. They reference local secret files and should
not be version controlled.
The pg-query API returns all values as strings. The chart builder
was using Number() which returns NaN for non-numeric strings.
Now uses parseFloat with NaN fallback to 0.
- Strip SQL comments (-- and /* */) before checking for SELECT,
so queries with leading comments don't get rejected
- Show the actual error detail from the API response instead of
generic 'API error 400' in the SQL Explorer UI
The migration ran on every deploy, inserting duplicate queries each
time (96 instead of 12). Added UNIQUE constraint on name and changed
ON CONFLICT to reference it. Cleaned up 84 duplicates in DB.
The trading engine network policy only allowed egress on ports 443
(HTTPS) and 53 (DNS). Gmail SMTP uses port 587 (STARTTLS), causing
'Network is unreachable' when sending notifications.
Replaced the Gmail API (OAuth2) notification delivery with plain
SMTP using a Gmail app password. Much simpler setup — no Google
Cloud project, no OAuth2 flow, no extra dependencies.
- Rewrote _send_gmail() to use smtplib with smtp.gmail.com:587 TLS
- Added stonks-gmail-secrets to Helm chart (GMAIL_SENDER,
GMAIL_RECIPIENT, GMAIL_APP_PASSWORD)
- Added gmail secret to trading-engine deployment
- Updated runmefirst.sh to read gmail.app from kube dir
- Sender/recipient: celes@celestium.life
The API returns macro_enabled/competitive_enabled but the TypeScript
interfaces expected 'enabled'. The toggles always showed disabled.
Now handles both field names with fallback.
Alpaca returns 404 when you don't hold a position in a ticker.
The ingestion worker was logging this as an error and incrementing
the failure count. Now returns an empty items list instead, since
'no position' is a valid state, not an error.
The ingestion worker creates an AlpacaBrokerAdapter but the pod
didn't have BROKER_API_KEY/BROKER_API_SECRET env vars, causing
401 Unauthorized on every broker source fetch. Added
stonks-broker-secrets to the ingestion service's secrets list.
The SQL Explorer was querying Trino which has zero tables. Rewrote to
use PostgreSQL directly:
Backend:
- GET /api/analytics/pg-schema: returns all public tables with column
names, types, and nullability from information_schema
- POST /api/analytics/pg-query: read-only SQL execution against
PostgreSQL with SELECT-only enforcement, auto LIMIT, and descriptive
error messages for syntax/table/query errors
Frontend:
- Schema browser shows all PostgreSQL tables with columns and types
- Click a table name → generates SELECT * FROM table LIMIT 100
- Pre-built Queries section with 12 seeded queries covering companies,
recommendations, trends, market prices, documents, global events,
trading decisions, ingestion health, reserve pool, sector exposure
- User-saved queries shown separately with delete buttons
- Chart builder, Monaco editor, and save functionality preserved
Migration 021: seeds 12 pre-built saved queries
The Trino/Iceberg lakehouse has zero tables, so all Trino-backed
dashboards showed 'No data available'. Rewrote all four to use
existing PostgreSQL-backed API endpoints:
- Sentiment Heatmap: useTrends + useCompanies → sector and ticker
trend strength bar charts (30k trend_windows in DB)
- Prediction Accuracy: useRecommendations → confidence distribution
and action distribution charts (30k recommendations in DB)
- Paper PnL: useTradingMetrics + useTradingMetricsHistory → equity
curve, daily returns, win/loss stats from trading engine
- Model Quality: useModelPerformance + useModelFailures → success
rate, latency, retries, and failure table from ops API
Removed unused Trino query function and ScatterChart imports.
The throughput API returns one row per source_type per time bucket,
but the chart was mapping each row as a separate bar. With 5 source
types × 24 hours, the bars were tiny and overlapping. Now aggregates
completed/failed/items across source types per time bucket so the
chart shows meaningful totals.
The alpaca.url config file contains https://paper-api.alpaca.markets/v2
but the adapter code also prepends /v2/ to all paths, resulting in
/v2/v2/positions which returns 404. Now strips trailing /v2 or /v1
from the configured base URL since the adapter manages API versioning.
This was causing 1,017 consecutive broker sync failures.
When multiple recommendations for the same ticker produce 'act'
decisions, the second one would overwrite the first in
simulated_positions, losing the first position's value and causing
incorrect portfolio value calculations. Now skips if already holding.
- Map DB 'id' field to 'recommendation_id' for evaluate_recommendation()
- Ensure confidence is cast to float (asyncpg may return Decimal)
- Add per-day logging showing rec count, act/skip, positions, pool balance
- Helps diagnose why backtests produce 0 trades
Two tiers of market data:
1. Per-ticker prev bars (existing 50 sources, 15-min cadence) for
watchlist detail — trading decisions, stop-loss, position sizing
2. Grouped daily (new single source, once per day) for broad market
context — correlation analysis, sector rotation, competitive intel
Changes:
- Add grouped_daily endpoint to PolygonMarketAdapter with auto date
calculation (previous trading day, skip weekends)
- Add fetch_global_market_sources() to scheduler for sources without
company_id, scheduled once daily (86400s cadence)
- Update _persist_market_items to use item-level ticker from T field
and look up company_id dynamically for grouped daily bars
- Migration 020: make company_id nullable on sources and
market_snapshots tables, add grouped daily source row
- Fix backtest replay to query market_snapshots data->>'c' for prices
- Increase market_api polling cadence from 60s to 900s (15 min).
The prev-day bar endpoint returns the same data all day, so polling
every minute wastes API quota. 50 tickers at 15-min cadence = ~3.3
req/min, well within the 5/min rate limit.
- Reduce market_api rate limit from 30/min to 5/min to match.
- Fix backtest replay to query market_snapshots with data->>'c' for
close prices instead of nonexistent market_data.close_price column.
- Enrich backtest recommendations with prices from market_snapshots
and sectors from companies table.
The simulated timestamp was 10:00 UTC (6:00 AM ET) which is outside
the trading window. Changed to 11:00 AM ET so backtested decisions
actually pass the trading window check.
Phase 2 of the autonomous trading engine:
- Replace start()/stop() stubs with real async implementations
- Decision loop: polls recommendations from PostgreSQL, deduplicates
via Redis, evaluates through the full pipeline, submits orders to
stonks:queue:broker_orders
- Stop-loss monitor: fetches prices from Polygon API, checks crossings,
submits immediate sell orders, safety sell after 15 min without data
- Performance loop: computes metrics every 5 min during market hours,
persists daily snapshots at market close
- Risk tier scheduler: evaluates daily at 16:00 ET, persists tier changes
- Rebalance scheduler: evaluates Monday 09:45 ET, respects circuit breaker
- Notification dispatch: SNS + Gmail with rate limiting and retry
- Backtest replay: fetches historical data, simulates decisions, persists
- Real asyncpg/redis connections in FastAPI lifespan (graceful degradation)
- Migration 019: enable paper trading with conservative tier, 5 cap
- Added max_open_positions to TradingConfig with env var loading
- Phase 2 tasks added to autonomous-trading-engine spec
When on /trading/engine, the /trading nav item also matched via
startsWith. Now checks if a more specific child route matches
first and uses exact match in that case.
When the LLM returns empty summary and no key facts, raise ValueError
so the retry logic kicks in instead of persisting an empty event.
Also strip whitespace from summary and filter empty key_facts entries.
Cleaned up 17 empty events from the database.
Macro news documents have no ticker, causing upload_normalized_text
and upload_parser_output to produce paths like parsed//2026/...
which MinIO rejects as XMinioInvalidObjectName. Use '_global' as
the path segment when ticker is empty, matching the existing
macro prefix pattern in upload_raw_document.
TypeScript strict mode in CI rejects explicit parameter types on
Recharts formatter/tickFormatter callbacks. Use inference with
'as number' casts on the value instead. Also fix unsafe cast in
PortfolioComposition and handle possibly-undefined percent.