Add recommendations folder so that the UI can display it 4

This commit is contained in:
Youssef Aitousarrah 2026-02-10 22:28:52 -08:00
parent 0bc7dda086
commit 8ebb42114d
13 changed files with 1885 additions and 1414 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,47 @@
{
"total_recommendations": 170,
"total_recommendations": 185,
"by_strategy": {
"momentum": {
"count": 35,
"wins_1d": 29,
"losses_1d": 6,
"wins_7d": 5,
"count": 92,
"wins_1d": 45,
"losses_1d": 33,
"wins_7d": 25,
"losses_7d": 23,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 1.0,
"avg_return_7d": 0.71,
"avg_return_30d": 0,
"win_rate_1d": 57.7,
"win_rate_7d": 52.1
},
"volume_accumulation": {
"count": 2,
"wins_1d": 1,
"losses_1d": 0,
"wins_7d": 1,
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": 19.7,
"avg_return_7d": 19.7,
"avg_return_30d": 0,
"win_rate_1d": 82.9,
"win_rate_1d": 100.0,
"win_rate_7d": 100.0
},
"insider_buying": {
"count": 8,
"wins_1d": 6,
"losses_1d": 2,
"wins_7d": 2,
"losses_7d": 0,
"count": 21,
"wins_1d": 15,
"losses_1d": 6,
"wins_7d": 10,
"losses_7d": 5,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": 0.84,
"avg_return_7d": 0.18,
"avg_return_30d": 0,
"win_rate_1d": 75.0,
"win_rate_7d": 100.0
"win_rate_1d": 71.4,
"win_rate_7d": 66.7
},
"options_flow": {
"count": 5,
@ -37,53 +51,26 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_1d": 3.09,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 80.0
},
"earnings_calendar": {
"count": 4,
"wins_1d": 1,
"losses_1d": 3,
"wins_7d": 0,
"losses_7d": 0,
"count": 17,
"wins_1d": 6,
"losses_1d": 11,
"wins_7d": 4,
"losses_7d": 8,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": -0.23,
"avg_return_7d": 0.36,
"avg_return_30d": 0,
"win_rate_1d": 25.0
"win_rate_1d": 35.3,
"win_rate_7d": 33.3
},
"Momentum": {
"count": 40,
"wins_1d": 18,
"losses_1d": 22,
"wins_7d": 18,
"losses_7d": 22,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 45.0,
"win_rate_7d": 45.0
},
"Insider Play": {
"count": 13,
"wins_1d": 7,
"losses_1d": 6,
"wins_7d": 7,
"losses_7d": 6,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 53.8,
"win_rate_7d": 53.8
},
"Contrarian Value": {
"contrarian_value": {
"count": 6,
"wins_1d": 3,
"losses_1d": 3,
@ -91,67 +78,39 @@
"losses_7d": 3,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": -4.91,
"avg_return_7d": -4.91,
"avg_return_30d": 0,
"win_rate_1d": 50.0,
"win_rate_7d": 50.0
},
"Earnings Play": {
"news_catalyst": {
"count": 3,
"wins_1d": 1,
"losses_1d": 2,
"wins_7d": 1,
"losses_7d": 2,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 33.3,
"win_rate_7d": 33.3
},
"News Catalyst": {
"count": 1,
"wins_1d": 0,
"losses_1d": 1,
"losses_1d": 3,
"wins_7d": 0,
"losses_7d": 1,
"losses_7d": 3,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": -13.55,
"avg_return_7d": -13.55,
"avg_return_30d": 0,
"win_rate_1d": 0.0,
"win_rate_7d": 0.0
},
"Volume Accumulation": {
"count": 1,
"wins_1d": 1,
"losses_1d": 0,
"wins_7d": 1,
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 100.0,
"win_rate_7d": 100.0
},
"short_squeeze": {
"count": 7,
"wins_1d": 4,
"losses_1d": 3,
"wins_7d": 2,
"losses_7d": 2,
"count": 10,
"wins_1d": 5,
"losses_1d": 5,
"wins_7d": 4,
"losses_7d": 3,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": 0.56,
"avg_return_7d": 0.85,
"avg_return_30d": 0,
"win_rate_1d": 57.1,
"win_rate_7d": 50.0
"win_rate_1d": 50.0,
"win_rate_7d": 57.1
},
"early_accumulation": {
"count": 1,
@ -161,8 +120,8 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": 20.41,
"avg_return_7d": 20.41,
"avg_return_30d": 0,
"win_rate_1d": 100.0,
"win_rate_7d": 100.0
@ -175,26 +134,12 @@
"losses_7d": 4,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": -2.0,
"avg_return_7d": -2.06,
"avg_return_30d": 0,
"win_rate_1d": 28.6,
"win_rate_7d": 33.3
},
"earnings_play": {
"count": 10,
"wins_1d": 4,
"losses_1d": 6,
"wins_7d": 3,
"losses_7d": 6,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 40.0,
"win_rate_7d": 33.3
},
"analyst_upgrade": {
"count": 8,
"wins_1d": 6,
@ -203,8 +148,8 @@
"losses_7d": 2,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": 1.32,
"avg_return_7d": -0.1,
"avg_return_30d": 0,
"win_rate_1d": 75.0,
"win_rate_7d": 66.7
@ -217,8 +162,8 @@
"losses_7d": 1,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": -19.2,
"avg_return_7d": -19.2,
"avg_return_30d": 0,
"win_rate_1d": 0.0,
"win_rate_7d": 0.0
@ -231,26 +176,12 @@
"losses_7d": 2,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": -8.9,
"avg_return_7d": -8.9,
"avg_return_30d": 0,
"win_rate_1d": 0.0,
"win_rate_7d": 0.0
},
"news_catalyst": {
"count": 2,
"wins_1d": 1,
"losses_1d": 1,
"wins_7d": 1,
"losses_7d": 1,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 50.0,
"win_rate_7d": 50.0
},
"undiscovered_dd": {
"count": 2,
"wins_1d": 2,
@ -259,36 +190,8 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 100.0,
"win_rate_7d": 100.0
},
"Momentum/Hype": {
"count": 3,
"wins_1d": 3,
"losses_1d": 0,
"wins_7d": 3,
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 100.0,
"win_rate_7d": 100.0
},
"Momentum/Hype / Short Squeeze": {
"count": 3,
"wins_1d": 3,
"losses_1d": 0,
"wins_7d": 3,
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_7d": 0,
"avg_return_1d": 6.44,
"avg_return_7d": 6.44,
"avg_return_30d": 0,
"win_rate_1d": 100.0,
"win_rate_7d": 100.0
@ -301,7 +204,7 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_1d": -3.38,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 50.0
@ -314,7 +217,7 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_1d": 0.93,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 50.0
@ -327,7 +230,7 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_1d": -5.11,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 0.0
@ -340,7 +243,7 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_1d": -1.47,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 50.0
@ -353,7 +256,7 @@
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_1d": 0,
"avg_return_1d": 1.36,
"avg_return_7d": 0,
"avg_return_30d": 0,
"win_rate_1d": 100.0
@ -361,15 +264,15 @@
},
"overall_1d": {
"count": 170,
"wins": 100,
"avg_return": 0.95,
"win_rate": 58.8
"wins": 94,
"avg_return": 0.26,
"win_rate": 55.3
},
"overall_7d": {
"count": 110,
"wins": 58,
"avg_return": 0.45,
"win_rate": 52.7
"wins": 56,
"avg_return": -0.18,
"win_rate": 50.9
},
"overall_30d": {
"count": 0,

View File

@ -27,11 +27,13 @@ logger = get_logger(__name__)
def main():
logger.info("""
logger.info(
"""
TradingAgents - Historical Memory Builder
""")
"""
)
# Configuration
tickers = [

View File

@ -89,7 +89,8 @@ def build_strategy_memories(strategy_name: str, config: dict):
strategy = STRATEGIES[strategy_name]
logger.info(f"""
logger.info(
f"""
Building Memories: {strategy_name.upper().replace('_', ' ')}
@ -98,7 +99,8 @@ Strategy: {strategy['description']}
Lookforward: {strategy['lookforward_days']} days
Sampling: Every {strategy['interval_days']} days
Tickers: {', '.join(strategy['tickers'])}
""")
"""
)
# Date range - last 2 years
end_date = datetime.now()
@ -157,7 +159,8 @@ Tickers: {', '.join(strategy['tickers'])}
def main():
logger.info("""
logger.info(
"""
TradingAgents - Strategy-Specific Memory Builder
@ -168,7 +171,8 @@ This script builds optimized memories for different trading styles:
2. Swing Trading - 7-day returns, weekly samples
3. Position Trading - 30-day returns, monthly samples
4. Long-term - 90-day returns, quarterly samples
""")
"""
)
logger.info("Available strategies:")
for i, (name, config) in enumerate(STRATEGIES.items(), 1):
@ -216,11 +220,13 @@ This script builds optimized memories for different trading styles:
logger.info("\n" + "=" * 70)
logger.info("\n💡 TIP: To use a specific strategy's memories, update your config:")
logger.info("""
logger.info(
"""
config = DEFAULT_CONFIG.copy()
config["memory_dir"] = "data/memories/swing_trading" # or your strategy
config["load_historical_memories"] = True
""")
"""
)
if __name__ == "__main__":

View File

@ -29,30 +29,73 @@ logger = get_logger(__name__)
def load_recommendations() -> List[Dict[str, Any]]:
"""Load all historical recommendations from the recommendations directory."""
"""Load all historical recommendations, preferring the performance database.
The performance database preserves accumulated return data (return_1d,
return_7d, win_1d, etc.) across runs. Raw date files are only used to
pick up new recommendations not yet in the database.
"""
recommendations_dir = "data/recommendations"
if not os.path.exists(recommendations_dir):
logger.warning(f"No recommendations directory found at {recommendations_dir}")
return []
all_recs = []
pattern = os.path.join(recommendations_dir, "*.json")
# Step 1: Load existing accumulated data from the performance database
existing: Dict[str, Dict[str, Any]] = {}
db_path = os.path.join(recommendations_dir, "performance_database.json")
if os.path.exists(db_path):
try:
with open(db_path, "r") as f:
db = json.load(f)
for recs in db.get("recommendations_by_date", {}).values():
if isinstance(recs, list):
for rec in recs:
key = f"{rec.get('ticker')}|{rec.get('discovery_date')}"
existing[key] = rec
logger.info(f"Loaded {len(existing)} records from performance database")
except Exception as e:
logger.error(f"Error loading performance database: {e}")
for filepath in glob.glob(pattern):
# Step 2: Scan raw date files for any new recommendations
new_count = 0
for filepath in glob.glob(os.path.join(recommendations_dir, "*.json")):
basename = os.path.basename(filepath)
if basename in ("performance_database.json", "statistics.json"):
continue
try:
with open(filepath, "r") as f:
data = json.load(f)
# Each file contains recommendations from one discovery run
recs = data.get("recommendations", [])
run_date = data.get("date", os.path.basename(filepath).replace(".json", ""))
for rec in recs:
rec["discovery_date"] = run_date
all_recs.append(rec)
recs = data.get("recommendations", [])
run_date = data.get("date", basename.replace(".json", ""))
for rec in recs:
rec["discovery_date"] = run_date
key = f"{rec.get('ticker')}|{run_date}"
if key not in existing:
existing[key] = rec
new_count += 1
except Exception as e:
logger.error(f"Error loading {filepath}: {e}")
return all_recs
if new_count:
logger.info(f"Merged {new_count} new recommendations from raw files")
return list(existing.values())
def _parse_price(raw) -> float | None:
"""Extract a numeric price from get_stock_price output.
The function may return a float directly or a markdown string like
"**Current Price**: $123.45". Handle both cases.
"""
if raw is None:
return None
if isinstance(raw, (int, float)):
return float(raw)
import re
m = re.search(r"\$([0-9,.]+)", str(raw))
return float(m.group(1).replace(",", "")) if m else None
def update_performance(recommendations: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@ -67,52 +110,37 @@ def update_performance(recommendations: List[Dict[str, Any]]) -> List[Dict[str,
if not all([ticker, discovery_date, entry_price]):
continue
# Skip if already marked as closed
if rec.get("status") == "closed":
continue
try:
# Get current price
current_price_data = get_stock_price(ticker, curr_date=today)
# Parse the price from the response (it returns a markdown report)
# Format is typically: "**Current Price**: $XXX.XX"
import re
price_match = re.search(r"\$([0-9,.]+)", current_price_data)
if price_match:
current_price = float(price_match.group(1).replace(",", ""))
else:
logger.warning(f"Could not parse price for {ticker}")
current_price = _parse_price(get_stock_price(ticker, curr_date=today))
if current_price is None:
logger.warning(f"Could not get price for {ticker}")
continue
# Calculate days since recommendation
rec_date = datetime.strptime(discovery_date, "%Y-%m-%d")
days_held = (datetime.now() - rec_date).days
# Calculate return
return_pct = ((current_price - entry_price) / entry_price) * 100
# Update metrics
rec["current_price"] = current_price
rec["return_pct"] = round(return_pct, 2)
rec["days_held"] = days_held
rec["last_updated"] = today
# Check specific time periods
# Capture milestone returns (only once per milestone)
if days_held >= 1 and "return_1d" not in rec:
rec["return_1d"] = round(return_pct, 2)
rec["win_1d"] = return_pct > 0
if days_held >= 7 and "return_7d" not in rec:
rec["return_7d"] = round(return_pct, 2)
rec["win_7d"] = return_pct > 0
if days_held >= 30 and "return_30d" not in rec:
rec["return_30d"] = round(return_pct, 2)
rec["status"] = "closed" # Mark as complete after 30 days
# Determine win/loss for completed periods
if "return_7d" in rec:
rec["win_7d"] = rec["return_7d"] > 0
if "return_30d" in rec:
rec["win_30d"] = rec["return_30d"] > 0
rec["win_30d"] = return_pct > 0
rec["status"] = "closed"
logger.info(
f"{ticker}: Entry ${entry_price:.2f} → Current ${current_price:.2f} ({return_pct:+.1f}%) [{days_held}d]"
@ -149,80 +177,15 @@ def save_performance_database(recommendations: List[Dict[str, Any]]):
def calculate_statistics(recommendations: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Calculate aggregate statistics from historical performance."""
stats = {
"total_recommendations": len(recommendations),
"by_strategy": {},
"overall_7d": {"count": 0, "wins": 0, "avg_return": 0},
"overall_30d": {"count": 0, "wins": 0, "avg_return": 0},
}
"""Calculate aggregate statistics from historical performance.
# Calculate by strategy
for rec in recommendations:
strategy = rec.get("strategy_match", "unknown")
Delegates to DiscoveryAnalytics.calculate_statistics so there is a single
source of truth for strategy normalization and metric calculation.
"""
from tradingagents.dataflows.discovery.analytics import DiscoveryAnalytics
if strategy not in stats["by_strategy"]:
stats["by_strategy"][strategy] = {
"count": 0,
"wins_7d": 0,
"losses_7d": 0,
"wins_30d": 0,
"losses_30d": 0,
"avg_return_7d": 0,
"avg_return_30d": 0,
}
stats["by_strategy"][strategy]["count"] += 1
# 7-day stats
if "return_7d" in rec:
stats["overall_7d"]["count"] += 1
if rec.get("win_7d"):
stats["overall_7d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_7d"] += 1
else:
stats["by_strategy"][strategy]["losses_7d"] += 1
stats["overall_7d"]["avg_return"] += rec["return_7d"]
# 30-day stats
if "return_30d" in rec:
stats["overall_30d"]["count"] += 1
if rec.get("win_30d"):
stats["overall_30d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_30d"] += 1
else:
stats["by_strategy"][strategy]["losses_30d"] += 1
stats["overall_30d"]["avg_return"] += rec["return_30d"]
# Calculate averages and win rates
if stats["overall_7d"]["count"] > 0:
stats["overall_7d"]["win_rate"] = round(
(stats["overall_7d"]["wins"] / stats["overall_7d"]["count"]) * 100, 1
)
stats["overall_7d"]["avg_return"] = round(
stats["overall_7d"]["avg_return"] / stats["overall_7d"]["count"], 2
)
if stats["overall_30d"]["count"] > 0:
stats["overall_30d"]["win_rate"] = round(
(stats["overall_30d"]["wins"] / stats["overall_30d"]["count"]) * 100, 1
)
stats["overall_30d"]["avg_return"] = round(
stats["overall_30d"]["avg_return"] / stats["overall_30d"]["count"], 2
)
# Calculate per-strategy stats
for strategy, data in stats["by_strategy"].items():
total_7d = data["wins_7d"] + data["losses_7d"]
total_30d = data["wins_30d"] + data["losses_30d"]
if total_7d > 0:
data["win_rate_7d"] = round((data["wins_7d"] / total_7d) * 100, 1)
if total_30d > 0:
data["win_rate_30d"] = round((data["wins_30d"] / total_30d) * 100, 1)
return stats
analytics = DiscoveryAnalytics()
return analytics.calculate_statistics(recommendations)
def print_statistics(stats: Dict[str, Any]):

View File

@ -129,10 +129,12 @@ def main():
6. Save updated positions
7. Print progress messages
"""
logger.info("""
logger.info(
"""
TradingAgents - Position Updater
""".strip())
""".strip()
)
# Initialize position tracker
tracker = PositionTracker(data_dir="data")

View File

@ -20,24 +20,40 @@ class DiscoveryAnalytics:
self.recommendations_dir = self.data_dir / "recommendations"
self.recommendations_dir.mkdir(parents=True, exist_ok=True)
def update_performance_tracking(self):
"""Update performance metrics for all open recommendations."""
logger.info("📊 Updating recommendation performance tracking...")
def _load_existing_database(self) -> Dict[str, Dict]:
"""Load existing performance database keyed by (ticker, discovery_date).
if not self.recommendations_dir.exists():
logger.info("No historical recommendations to track yet.")
return
Returns a dict mapping "TICKER|DATE" -> rec dict, preserving accumulated
return data (return_1d, return_7d, etc.) across runs.
"""
db_path = self.recommendations_dir / "performance_database.json"
if not db_path.exists():
return {}
# Load all recommendations
try:
with open(db_path, "r") as f:
data = json.load(f)
except Exception as e:
logger.warning(f"Error loading performance database: {e}")
return {}
existing = {}
by_date = data.get("recommendations_by_date", {})
for recs in by_date.values():
if isinstance(recs, list):
for rec in recs:
key = f"{rec.get('ticker')}|{rec.get('discovery_date')}"
existing[key] = rec
return existing
def _load_raw_recommendations(self) -> List[Dict]:
"""Load recommendations from raw date files."""
all_recs = []
# Use glob directly on the path object if python 3.10+ otherwise str()
pattern = str(self.recommendations_dir / "*.json")
for filepath in glob.glob(pattern):
# Skip the database and stats files
if "performance_database" in filepath or "statistics" in filepath:
continue
try:
with open(filepath, "r") as f:
data = json.load(f)
@ -49,16 +65,46 @@ class DiscoveryAnalytics:
all_recs.append(rec)
except Exception as e:
logger.warning(f"Error loading {filepath}: {e}")
return all_recs
if not all_recs:
def update_performance_tracking(self):
"""Update performance metrics for all recommendations.
Loads accumulated data from performance_database.json first, merges in
any new recs from raw date files, then updates prices for open positions.
This preserves return_1d/return_7d/return_30d across runs.
"""
logger.info("📊 Updating recommendation performance tracking...")
if not self.recommendations_dir.exists():
logger.info("No historical recommendations to track yet.")
return
# Step 1: Load existing database (preserves accumulated return data)
existing = self._load_existing_database()
logger.info(f"Loaded {len(existing)} existing records from performance database")
# Step 2: Load raw recommendation files and merge new ones
raw_recs = self._load_raw_recommendations()
new_count = 0
for rec in raw_recs:
key = f"{rec.get('ticker')}|{rec.get('discovery_date')}"
if key not in existing:
existing[key] = rec
new_count += 1
if not existing:
logger.info("No recommendations found to track.")
return
# Filter to only track open positions
if new_count > 0:
logger.info(f"Added {new_count} new recommendations")
all_recs = list(existing.values())
open_recs = [r for r in all_recs if r.get("status") != "closed"]
logger.info(f"Tracking {len(open_recs)} open positions (out of {len(all_recs)} total)...")
# Update performance
# Step 3: Update prices for open positions
today = datetime.now().strftime("%Y-%m-%d")
updated_count = 0
@ -67,13 +113,10 @@ class DiscoveryAnalytics:
discovery_date = rec.get("discovery_date")
entry_price = rec.get("entry_price")
# Skip if already closed or missing data
if rec.get("status") == "closed" or not all([ticker, discovery_date, entry_price]):
continue
try:
# Get current price
# We interpret this import here to avoid circular dependency if this class is imported early
from tradingagents.dataflows.y_finance import get_stock_price
current_price = get_stock_price(ticker, curr_date=today)
@ -81,18 +124,16 @@ class DiscoveryAnalytics:
if current_price is None:
continue
# Calculate metrics
rec_date = datetime.strptime(discovery_date, "%Y-%m-%d")
days_held = (datetime.now() - rec_date).days
return_pct = ((current_price - entry_price) / entry_price) * 100
# Update
rec["current_price"] = current_price
rec["return_pct"] = round(return_pct, 2)
rec["days_held"] = days_held
rec["last_updated"] = today
# Capture specific time periods (1d, 7d, 30d)
# Capture milestone returns (only once, at the first eligible run)
if days_held >= 1 and "return_1d" not in rec:
rec["return_1d"] = round(return_pct, 2)
rec["win_1d"] = return_pct > 0
@ -109,11 +150,11 @@ class DiscoveryAnalytics:
updated_count += 1
except Exception:
# Silently skip errors to not interrupt discovery
pass
if updated_count > 0:
logger.info(f"Updated {updated_count} positions")
# Step 4: Always save — even if no price updates, the merge may have added new recs
if updated_count > 0 or new_count > 0:
logger.info(f"Updated {updated_count} positions, {new_count} new recs")
self._save_performance_db(all_recs)
else:
logger.info("No updates needed")
@ -148,6 +189,35 @@ class DiscoveryAnalytics:
logger.info("💾 Updated performance database and statistics")
@staticmethod
def _normalize_strategy(name: str) -> str:
"""Normalize strategy names to snake_case canonical form.
Merges duplicates like 'Momentum' / 'momentum', 'Insider Play' / 'insider_buying'.
"""
import re
if not name:
return "unknown"
# Lowercase and replace separators with underscore
normalized = name.strip().lower()
normalized = re.sub(r"[\s/]+", "_", normalized)
# Collapse multiple underscores
normalized = re.sub(r"_+", "_", normalized).strip("_")
# Map known aliases to canonical names
aliases = {
"insider_play": "insider_buying",
"earnings_play": "earnings_calendar",
"contrarian_value": "contrarian_value",
"news_catalyst": "news_catalyst",
"volume_accumulation": "volume_accumulation",
"momentum_hype": "momentum",
"momentum_hype_short_squeeze": "short_squeeze",
}
return aliases.get(normalized, normalized)
def calculate_statistics(self, recommendations: list) -> dict:
"""Calculate aggregate statistics from historical performance."""
stats = {
@ -158,12 +228,9 @@ class DiscoveryAnalytics:
"overall_30d": {"count": 0, "wins": 0, "avg_return": 0},
}
# Calculate by strategy
for rec in recommendations:
strategy = rec.get("strategy_match", "unknown")
if strategy not in stats["by_strategy"]:
stats["by_strategy"][strategy] = {
def _get_strategy_bucket(strategy_name):
if strategy_name not in stats["by_strategy"]:
stats["by_strategy"][strategy_name] = {
"count": 0,
"wins_1d": 0,
"losses_1d": 0,
@ -175,45 +242,53 @@ class DiscoveryAnalytics:
"avg_return_7d": 0,
"avg_return_30d": 0,
}
return stats["by_strategy"][strategy_name]
stats["by_strategy"][strategy]["count"] += 1
# Calculate by strategy
for rec in recommendations:
strategy = self._normalize_strategy(rec.get("strategy_match", "unknown"))
bucket = _get_strategy_bucket(strategy)
bucket["count"] += 1
# 1-day stats
if "return_1d" in rec:
stats["overall_1d"]["count"] += 1
bucket["avg_return_1d"] += rec["return_1d"]
if rec.get("win_1d"):
stats["overall_1d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_1d"] += 1
bucket["wins_1d"] += 1
else:
stats["by_strategy"][strategy]["losses_1d"] += 1
bucket["losses_1d"] += 1
stats["overall_1d"]["avg_return"] += rec["return_1d"]
# 7-day stats
if "return_7d" in rec:
stats["overall_7d"]["count"] += 1
bucket["avg_return_7d"] += rec["return_7d"]
if rec.get("win_7d"):
stats["overall_7d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_7d"] += 1
bucket["wins_7d"] += 1
else:
stats["by_strategy"][strategy]["losses_7d"] += 1
bucket["losses_7d"] += 1
stats["overall_7d"]["avg_return"] += rec["return_7d"]
# 30-day stats
if "return_30d" in rec:
stats["overall_30d"]["count"] += 1
bucket["avg_return_30d"] += rec["return_30d"]
if rec.get("win_30d"):
stats["overall_30d"]["wins"] += 1
stats["by_strategy"][strategy]["wins_30d"] += 1
bucket["wins_30d"] += 1
else:
stats["by_strategy"][strategy]["losses_30d"] += 1
bucket["losses_30d"] += 1
stats["overall_30d"]["avg_return"] += rec["return_30d"]
# Calculate averages and win rates
# Calculate overall averages and win rates
self._calculate_metric_averages(stats["overall_1d"])
self._calculate_metric_averages(stats["overall_7d"])
self._calculate_metric_averages(stats["overall_30d"])
# Calculate per-strategy stats
# Calculate per-strategy win rates and avg returns
for strategy, data in stats["by_strategy"].items():
total_1d = data["wins_1d"] + data["losses_1d"]
total_7d = data["wins_7d"] + data["losses_7d"]
@ -221,12 +296,15 @@ class DiscoveryAnalytics:
if total_1d > 0:
data["win_rate_1d"] = round((data["wins_1d"] / total_1d) * 100, 1)
data["avg_return_1d"] = round(data["avg_return_1d"] / total_1d, 2)
if total_7d > 0:
data["win_rate_7d"] = round((data["wins_7d"] / total_7d) * 100, 1)
data["avg_return_7d"] = round(data["avg_return_7d"] / total_7d, 2)
if total_30d > 0:
data["win_rate_30d"] = round((data["wins_30d"] / total_30d) * 100, 1)
data["avg_return_30d"] = round(data["avg_return_30d"] / total_30d, 2)
return stats

View File

@ -1,21 +1,23 @@
"""
Main Streamlit app entry point for the Trading Agents Dashboard.
This module sets up the dashboard page configuration, sidebar navigation,
and routing to different pages based on user selection.
Dark terminal-inspired trading interface with sidebar navigation.
"""
from datetime import datetime
import streamlit as st
from tradingagents.ui import pages
from tradingagents.ui.theme import COLORS, GLOBAL_CSS
from tradingagents.ui.utils import load_quick_stats
def setup_page_config():
"""Configure the Streamlit page settings."""
st.set_page_config(
page_title="Trading Agents Dashboard",
page_icon="📊",
page_title="Trading Agents",
page_icon="",
layout="wide",
initial_sidebar_state="expanded",
)
@ -24,46 +26,101 @@ def setup_page_config():
def render_sidebar():
"""Render the sidebar with navigation and quick stats."""
with st.sidebar:
st.title("Trading Agents")
# Brand header
st.markdown(
f"""
<div style="padding:0.5rem 0 1.25rem 0;">
<div style="font-family:'JetBrains Mono',monospace;font-size:1.15rem;
font-weight:700;color:{COLORS['text_primary']};letter-spacing:-0.03em;">
TRADING<span style="color:{COLORS['green']};">AGENTS</span>
</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;
color:{COLORS['text_muted']};margin-top:0.15rem;">
v2.0 &mdash; {datetime.now().strftime('%b %d, %Y')}
</div>
</div>
""",
unsafe_allow_html=True,
)
st.markdown(
f"""<div style="height:1px;background:{COLORS['border']};
margin-bottom:1rem;"></div>""",
unsafe_allow_html=True,
)
# Navigation
st.markdown("### Navigation")
page = st.radio(
"Select a page:",
options=["Home", "Today's Picks", "Portfolio", "Performance", "Settings"],
"Navigation",
options=["Overview", "Signals", "Portfolio", "Performance", "Config"],
label_visibility="collapsed",
)
st.markdown("---")
st.markdown(
f"""<div style="height:1px;background:{COLORS['border']};
margin:1rem 0;"></div>""",
unsafe_allow_html=True,
)
# Quick stats section
st.markdown("### Quick Stats")
# Quick stats
try:
open_positions, win_rate = load_quick_stats()
col1, col2 = st.columns(2)
with col1:
st.metric("Open Positions", open_positions)
with col2:
st.metric("Win Rate", f"{win_rate:.1f}%")
except Exception as e:
st.warning(f"Could not load quick stats: {str(e)}")
st.markdown(
f"""
<div style="padding:0.75rem;background:{COLORS['bg_card']};
border:1px solid {COLORS['border']};border-radius:8px;">
<div style="font-family:'DM Sans',sans-serif;font-size:0.65rem;
font-weight:600;text-transform:uppercase;letter-spacing:0.06em;
color:{COLORS['text_muted']};margin-bottom:0.75rem;">
Quick Stats
</div>
<div style="display:flex;justify-content:space-between;
align-items:flex-end;">
<div>
<div style="font-family:'JetBrains Mono',monospace;
font-size:1.3rem;font-weight:700;
color:{COLORS['text_primary']};">
{open_positions}
</div>
<div style="font-family:'DM Sans',sans-serif;
font-size:0.65rem;color:{COLORS['text_muted']};">
Open
</div>
</div>
<div style="text-align:right;">
<div style="font-family:'JetBrains Mono',monospace;
font-size:1.3rem;font-weight:700;
color:{COLORS['green'] if win_rate >= 50 else COLORS['red']};">
{win_rate:.0f}%
</div>
<div style="font-family:'DM Sans',sans-serif;
font-size:0.65rem;color:{COLORS['text_muted']};">
Win Rate
</div>
</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
except Exception:
pass
return page
def route_page(page):
"""Route to the appropriate page based on selection."""
if page == "Home":
pages.home.render()
elif page == "Today's Picks":
pages.todays_picks.render()
elif page == "Portfolio":
pages.portfolio.render()
elif page == "Performance":
pages.performance.render()
elif page == "Settings":
pages.settings.render()
page_map = {
"Overview": pages.home,
"Signals": pages.todays_picks,
"Portfolio": pages.portfolio,
"Performance": pages.performance,
"Config": pages.settings,
}
module = page_map.get(page)
if module:
module.render()
else:
st.error(f"Unknown page: {page}")
@ -72,17 +129,8 @@ def main():
"""Main entry point for the Streamlit app."""
setup_page_config()
# Custom CSS for better styling
st.markdown(
"""
<style>
.main {
padding: 2rem;
}
</style>
""",
unsafe_allow_html=True,
)
# Inject global theme CSS
st.markdown(GLOBAL_CSS, unsafe_allow_html=True)
# Render sidebar and get selected page
selected_page = render_sidebar()

View File

@ -1,133 +1,180 @@
"""
Home page for the Trading Agents Dashboard.
Overview page trading terminal home screen.
This module displays the main dashboard with overview metrics and
pipeline performance visualization.
Shows KPI cards, strategy scatter plot, and recent signal summary.
"""
from datetime import datetime
import pandas as pd
import plotly.express as px
import streamlit as st
from tradingagents.ui.utils import load_open_positions, load_statistics, load_strategy_metrics
from tradingagents.ui.theme import COLORS, get_plotly_template, kpi_card, page_header
from tradingagents.ui.utils import (
load_open_positions,
load_recommendations,
load_statistics,
load_strategy_metrics,
)
def render() -> None:
"""
Render the home page with overview metrics and pipeline performance.
"""Render the overview page."""
Displays:
- Dashboard title
- Warning if no statistics available
- 4-column metric layout (Win Rate, Open Positions, Avg Return, Best Pipeline)
- Pipeline performance scatter plot with quadrant lines
"""
# Page title
st.title("🎯 Trading Discovery Dashboard")
st.markdown(
page_header("Overview", f"Market session {datetime.now().strftime('%A, %B %d %Y')}"),
unsafe_allow_html=True,
)
# Load data
stats = load_statistics()
positions = load_open_positions()
strategy_metrics = load_strategy_metrics()
# Check if statistics are available
if not stats or not stats.get("overall_7d"):
st.warning("No statistics data available. Run the discovery pipeline to generate data.")
return
overall = stats.get("overall_7d", {}) if stats else {}
win_rate_7d = overall.get("win_rate", 0)
avg_return_7d = overall.get("avg_return", 0)
total_recs = stats.get("total_recommendations", 0) if stats else 0
open_count = len(positions) if positions else 0
if not strategy_metrics:
st.warning("No strategy performance data available yet.")
return
best_strat_name = "N/A"
best_strat_wr = 0.0
for item in (strategy_metrics or []):
wr = item.get("Win Rate", 0) or 0
if wr > best_strat_wr:
best_strat_wr = wr
best_strat_name = item.get("Strategy", "unknown")
# Extract overall metrics from 7-day period
overall_metrics = stats.get("overall_7d", {})
win_rate_7d = overall_metrics.get("win_rate", 0)
avg_return_7d = overall_metrics.get("avg_return", 0)
open_positions_count = len(positions) if positions else 0
# ---- KPI Row ----
cols = st.columns(5)
kpis = [
("Win Rate 7d", f"{win_rate_7d:.0f}%", f"+{win_rate_7d - 50:.0f}pp vs 50%" if win_rate_7d >= 50 else f"{win_rate_7d - 50:.0f}pp vs 50%", "green" if win_rate_7d >= 50 else "red"),
("Avg Return 7d", f"{avg_return_7d:+.2f}%", "", "green" if avg_return_7d > 0 else "red"),
("Open Positions", str(open_count), "", "blue"),
("Total Signals", str(total_recs), "", "amber"),
("Top Strategy", best_strat_name.upper(), f"{best_strat_wr:.0f}% WR" if best_strat_wr else "", "green" if best_strat_wr >= 60 else "amber"),
]
for col, (label, value, delta, color) in zip(cols, kpis):
with col:
st.markdown(kpi_card(label, value, delta, color), unsafe_allow_html=True)
# Find best strategy
best_strategy = None
best_win_rate = 0.0
for item in strategy_metrics:
win_rate = item.get("Win Rate", 0) or 0
if win_rate > best_win_rate:
best_win_rate = win_rate
best_strategy = {"name": item.get("Strategy", "unknown"), "win_rate": win_rate}
st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
# Display 4-column metric layout
col1, col2, col3, col4 = st.columns(4)
# ---- Two-column: strategy chart + today's signals ----
left_col, right_col = st.columns([3, 2])
with col1:
st.metric(
label="Win Rate (7d)",
value=f"{win_rate_7d:.1f}%",
delta=f"{win_rate_7d - 50:.1f}%" if win_rate_7d >= 50 else None,
with left_col:
st.markdown(
'<div class="section-title">Strategy Performance <span class="accent">// scatter</span></div>',
unsafe_allow_html=True,
)
with col2:
st.metric(
label="Open Positions",
value=open_positions_count,
)
if strategy_metrics:
df = pd.DataFrame(strategy_metrics)
template = get_plotly_template()
with col3:
st.metric(
label="Avg Return (7d)",
value=f"{avg_return_7d:.2f}%",
delta=f"{avg_return_7d:.2f}%" if avg_return_7d > 0 else None,
)
with col4:
if best_strategy:
st.metric(
label="Best Strategy",
value=best_strategy["name"],
delta=f"{best_strategy['win_rate']:.1f}% WR",
fig = px.scatter(
df,
x="Win Rate",
y="Avg Return",
size="Count",
color="Strategy",
hover_name="Strategy",
hover_data={"Win Rate": ":.1f", "Avg Return": ":.2f", "Count": True, "Strategy": False},
labels={"Win Rate": "Win Rate (%)", "Avg Return": "Avg Return (%)"},
size_max=40,
)
fig.add_hline(y=0, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
fig.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
fig.add_annotation(x=75, y=5, text="WINNERS", showarrow=False, font=dict(size=10, color=COLORS["green"], family="JetBrains Mono"), opacity=0.3)
fig.add_annotation(x=25, y=-5, text="LOSERS", showarrow=False, font=dict(size=10, color=COLORS["red"], family="JetBrains Mono"), opacity=0.3)
fig.update_layout(
**template,
height=380,
showlegend=True,
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(size=10), orientation="h", yanchor="bottom", y=-0.25),
)
st.plotly_chart(fig, width="stretch")
else:
st.metric(
label="Best Strategy",
value="N/A",
)
st.info("Run the discovery pipeline to generate strategy data.")
# Strategy Performance scatter plot
st.subheader("Strategy Performance")
with right_col:
st.markdown(
'<div class="section-title">Today\'s Signals <span class="accent">// latest</span></div>',
unsafe_allow_html=True,
)
today = datetime.now().strftime("%Y-%m-%d")
recs = load_recommendations(today)
if recs:
for rec in recs[:6]:
ticker = rec.get("ticker", "???")
score = rec.get("final_score", 0)
conf = rec.get("confidence", 0)
strat = (rec.get("strategy_match") or "momentum").upper()
entry = rec.get("entry_price")
entry_str = f"${entry:.2f}" if entry else "N/A"
score_color = COLORS["green"] if score >= 35 else (COLORS["amber"] if score >= 20 else COLORS["text_muted"])
conf_bar_w = conf * 10
conf_color = COLORS["green"] if conf >= 8 else (COLORS["amber"] if conf >= 6 else COLORS["red"])
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-radius:8px;padding:0.65rem 0.85rem;margin-bottom:0.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;align-items:center;gap:0.6rem;">
<span style="font-family:'JetBrains Mono',monospace;
font-weight:700;font-size:0.95rem;
color:{COLORS['text_primary']};">{ticker}</span>
<span style="font-family:'DM Sans',sans-serif;font-size:0.6rem;
font-weight:600;text-transform:uppercase;
padding:0.15rem 0.4rem;border-radius:3px;
background:rgba(59,130,246,0.15);
color:{COLORS['blue']};letter-spacing:0.04em;">{strat}</span>
</div>
<span style="font-family:'JetBrains Mono',monospace;
font-size:0.8rem;color:{score_color};font-weight:600;">{score}</span>
</div>
<div style="display:flex;justify-content:space-between;
align-items:center;margin-top:0.35rem;">
<span style="font-family:'JetBrains Mono',monospace;
font-size:0.72rem;color:{COLORS['text_muted']};">{entry_str}</span>
<div style="width:50px;height:3px;background:{COLORS['border']};
border-radius:2px;overflow:hidden;">
<div style="height:100%;width:{conf_bar_w}%;
background:{conf_color};border-radius:2px;"></div>
</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
if len(recs) > 6:
st.caption(f"+{len(recs) - 6} more signals. Switch to Signals page for the full list.")
else:
st.info("No signals generated today.")
# ---- Strategy table ----
if strategy_metrics:
df = pd.DataFrame(strategy_metrics)
# Create scatter plot with plotly
fig = px.scatter(
df,
x="Win Rate",
y="Avg Return",
size="Count",
color="Strategy",
hover_name="Strategy",
hover_data={
"Win Rate": ":.1f",
"Avg Return": ":.2f",
"Count": True,
"Strategy": False,
},
title="Strategy Performance Analysis",
labels={
"Win Rate": "Win Rate (%)",
"Avg Return": "Avg Return (%)",
st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
st.markdown(
'<div class="section-title">Strategy Breakdown <span class="accent">// table</span></div>',
unsafe_allow_html=True,
)
df_table = pd.DataFrame(strategy_metrics).sort_values("Win Rate", ascending=False)
st.dataframe(
df_table,
width="stretch",
hide_index=True,
column_config={
"Win Rate": st.column_config.NumberColumn(format="%.1f%%"),
"Avg Return": st.column_config.NumberColumn(format="%+.2f%%"),
"Count": st.column_config.NumberColumn(format="%d"),
},
)
# Add quadrant lines at y=0 (breakeven) and x=50 (50% win rate)
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5)
fig.add_vline(x=50, line_dash="dash", line_color="gray", opacity=0.5)
# Update layout for better visibility
fig.update_layout(
height=400,
showlegend=True,
hovermode="closest",
)
st.plotly_chart(fig, use_container_width=True)
else:
st.info("No strategy data available for visualization.")

View File

@ -1,50 +1,73 @@
"""
Performance analytics page for the Trading Agents Dashboard.
Performance analytics page strategy comparison and win/loss analysis.
This module displays performance metrics and visualization for different scanners,
including win rates, average returns, and trading volume analysis.
Shows strategy scatter plot with themed Plotly charts, per-strategy
breakdown table, and win rate distribution.
"""
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from tradingagents.ui.utils import load_strategy_metrics
from tradingagents.ui.theme import COLORS, get_plotly_template, page_header
from tradingagents.ui.utils import load_performance_database, load_statistics, load_strategy_metrics
def render() -> None:
"""
Render the performance analytics page.
"""Render the performance analytics page."""
st.markdown(page_header("Performance", "Strategy analytics & win/loss breakdown"), unsafe_allow_html=True)
Displays:
- Page title
- Warning if no statistics available
- Scanner Performance heatmap with scatter plot showing:
- Win Rate (x-axis) vs Avg Return (y-axis)
- Bubble size = Trade count
- Color = Win Rate (RdYlGn colorscale)
- Quadrant lines at y=0 and x=50
"""
# Page title
st.title("📊 Performance Analytics")
# Load data
strategy_metrics = load_strategy_metrics()
stats = load_statistics()
# Check if data is available
if not strategy_metrics:
st.warning(
"No strategy performance data available. Run performance tracking to generate data."
)
st.warning("No performance data available yet. Run the discovery pipeline and track outcomes.")
return
# Strategy Performance section
st.subheader("Strategy Performance")
template = get_plotly_template()
df = pd.DataFrame(strategy_metrics)
if strategy_metrics:
df = pd.DataFrame(strategy_metrics)
# ---- Summary KPIs ----
total_trades = df["Count"].sum()
avg_wr = (df["Win Rate"] * df["Count"]).sum() / total_trades if total_trades > 0 else 0
avg_ret = (df["Avg Return"] * df["Count"]).sum() / total_trades if total_trades > 0 else 0
n_strategies = len(df)
cols = st.columns(4)
summaries = [
("Total Trades", str(int(total_trades))),
("Weighted Win Rate", f"{avg_wr:.1f}%"),
("Weighted Avg Return", f"{avg_ret:+.2f}%"),
("Active Strategies", str(n_strategies)),
]
for col, (label, val) in zip(cols, summaries):
with col:
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-radius:8px;padding:0.85rem 1rem;text-align:center;">
<div style="font-family:'DM Sans',sans-serif;font-size:0.65rem;
font-weight:600;text-transform:uppercase;letter-spacing:0.06em;
color:{COLORS['text_muted']};margin-bottom:0.3rem;">{label}</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:1.3rem;
font-weight:700;color:{COLORS['text_primary']};">{val}</div>
</div>
""",
unsafe_allow_html=True,
)
st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
# ---- Two-column: scatter + bar chart ----
left_col, right_col = st.columns(2)
with left_col:
st.markdown(
'<div class="section-title">Win Rate vs Return <span class="accent">// scatter</span></div>',
unsafe_allow_html=True,
)
# Create scatter plot with plotly
fig = px.scatter(
df,
x="Win Rate",
@ -52,31 +75,99 @@ def render() -> None:
size="Count",
color="Win Rate",
hover_name="Strategy",
hover_data={
"Win Rate": ":.1f",
"Avg Return": ":.2f",
"Count": True,
"Strategy": False,
},
title="Strategy Performance Analysis",
labels={
"Win Rate": "Win Rate (%)",
"Avg Return": "Avg Return (%)",
},
color_continuous_scale="RdYlGn",
hover_data={"Win Rate": ":.1f", "Avg Return": ":.2f", "Count": True},
labels={"Win Rate": "Win Rate (%)", "Avg Return": "Avg Return (%)"},
color_continuous_scale=[
[0, COLORS["red"]],
[0.5, COLORS["amber"]],
[1.0, COLORS["green"]],
],
size_max=45,
)
# Add quadrant lines at y=0 (breakeven) and x=50 (50% win rate)
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5)
fig.add_vline(x=50, line_dash="dash", line_color="gray", opacity=0.5)
fig.add_hline(y=0, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
fig.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
# Update layout for better visibility
fig.update_layout(
height=500,
showlegend=True,
hovermode="closest",
**template,
height=400,
showlegend=False,
coloraxis_showscale=False,
)
st.plotly_chart(fig, width="stretch")
with right_col:
st.markdown(
'<div class="section-title">Win Rate by Strategy <span class="accent">// bar</span></div>',
unsafe_allow_html=True,
)
st.plotly_chart(fig, use_container_width=True)
else:
st.info("No strategy data available for visualization.")
df_sorted = df.sort_values("Win Rate", ascending=True)
colors = [COLORS["green"] if wr >= 50 else COLORS["red"] for wr in df_sorted["Win Rate"]]
fig_bar = go.Figure(go.Bar(
x=df_sorted["Win Rate"],
y=df_sorted["Strategy"],
orientation="h",
marker_color=colors,
text=[f"{wr:.0f}%" for wr in df_sorted["Win Rate"]],
textposition="auto",
textfont=dict(family="JetBrains Mono", size=11, color=COLORS["text_primary"]),
))
fig_bar.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.5)
fig_bar.update_layout(
**template,
height=400,
xaxis_title="Win Rate (%)",
yaxis_title="",
)
fig_bar.update_yaxes(tickfont=dict(family="JetBrains Mono", size=11))
st.plotly_chart(fig_bar, width="stretch")
# ---- Strategy breakdown table ----
st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
st.markdown(
'<div class="section-title">Detailed Breakdown <span class="accent">// table</span></div>',
unsafe_allow_html=True,
)
display_df = df.copy()
display_df = display_df.sort_values("Win Rate", ascending=False)
display_df["Count"] = display_df["Count"].astype(int)
st.dataframe(
display_df,
width="stretch",
hide_index=True,
column_config={
"Win Rate": st.column_config.NumberColumn(format="%.1f%%"),
"Avg Return": st.column_config.NumberColumn(format="%+.2f%%"),
"Count": st.column_config.NumberColumn(format="%d"),
},
)
# ---- Per-strategy stats from statistics.json ----
if stats and stats.get("by_strategy"):
st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
st.markdown(
'<div class="section-title">Time-Period Breakdown <span class="accent">// 1d / 7d / 30d</span></div>',
unsafe_allow_html=True,
)
by_strat = stats["by_strategy"]
rows = []
for strat_name, data in by_strat.items():
rows.append({
"Strategy": strat_name,
"Count": data.get("count", 0),
"Win Rate 1d": f"{data.get('win_rate_1d', 0):.0f}%" if "win_rate_1d" in data else "N/A",
"Win Rate 7d": f"{data.get('win_rate_7d', 0):.0f}%" if "win_rate_7d" in data else "N/A",
"Wins 1d": data.get("wins_1d", 0),
"Losses 1d": data.get("losses_1d", 0),
"Wins 7d": data.get("wins_7d", 0),
"Losses 7d": data.get("losses_7d", 0),
})
if rows:
st.dataframe(pd.DataFrame(rows), width="stretch", hide_index=True)

View File

@ -1,90 +1,169 @@
"""Portfolio tracker."""
"""
Portfolio page position tracker with P/L visualization.
Shows portfolio summary KPIs and individual position rows
with color-coded P/L indicators.
"""
from datetime import datetime
import pandas as pd
import streamlit as st
from tradingagents.ui.theme import COLORS, kpi_card, page_header, pnl_color
from tradingagents.ui.utils import load_open_positions
def render():
st.title("💼 Portfolio Tracker")
st.markdown(page_header("Portfolio", "Open positions & P/L tracker"), unsafe_allow_html=True)
# Manual add form
with st.expander(" Add Position"):
# ---- Add position form ----
with st.expander("Add Position"):
col1, col2, col3, col4 = st.columns(4)
with col1:
ticker = st.text_input("Ticker")
ticker = st.text_input("Ticker", placeholder="AAPL")
with col2:
entry_price = st.number_input("Entry Price", min_value=0.0)
entry_price = st.number_input("Entry Price", min_value=0.0, format="%.2f")
with col3:
shares = st.number_input("Shares", min_value=0, step=1)
with col4:
st.write("") # Spacing
if st.button("Add"):
st.write("")
if st.button("Add Position"):
if ticker and entry_price > 0 and shares > 0:
from tradingagents.dataflows.discovery.performance.position_tracker import (
PositionTracker,
)
from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker
tracker = PositionTracker()
pos = tracker.create_position(
{
"ticker": ticker.upper(),
"entry_price": entry_price,
"shares": shares,
"recommendation_date": datetime.now().isoformat(),
"pipeline": "manual",
"scanner": "manual",
"strategy_match": "manual",
"confidence": 5,
}
)
pos = tracker.create_position({
"ticker": ticker.upper(),
"entry_price": entry_price,
"shares": shares,
"recommendation_date": datetime.now().isoformat(),
"pipeline": "manual",
"scanner": "manual",
"strategy_match": "manual",
"confidence": 5,
})
tracker.save_position(pos)
st.success(f"Added {ticker.upper()}")
st.rerun()
# Load positions
positions = load_open_positions()
if not positions:
st.info("No open positions")
st.markdown(
f"""
<div style="text-align:center;padding:3rem;color:{COLORS['text_muted']};
font-family:'DM Sans',sans-serif;">
<div style="font-size:2rem;margin-bottom:0.5rem;">No open positions</div>
<div style="font-size:0.85rem;">
Enter positions manually above or run the discovery pipeline.
</div>
</div>
""",
unsafe_allow_html=True,
)
return
# Summary
# ---- Portfolio summary ----
total_invested = sum(p["entry_price"] * p.get("shares", 0) for p in positions)
total_current = sum(p["metrics"]["current_price"] * p.get("shares", 0) for p in positions)
total_pnl = total_current - total_invested
total_pnl_pct = (total_pnl / total_invested * 100) if total_invested > 0 else 0
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Invested", f"${total_invested:,.2f}")
with col2:
st.metric("Current", f"${total_current:,.2f}")
with col3:
st.metric("P/L", f"${total_pnl:,.2f}", delta=f"{total_pnl_pct:+.1f}%")
with col4:
st.metric("Positions", len(positions))
cols = st.columns(4)
summary_kpis = [
("Invested", f"${total_invested:,.0f}", "", "blue"),
("Current Value", f"${total_current:,.0f}", "", "blue"),
("P/L", f"${total_pnl:+,.0f}", f"{total_pnl_pct:+.1f}%", "green" if total_pnl >= 0 else "red"),
("Positions", str(len(positions)), "", "amber"),
]
for col, (label, value, delta, color) in zip(cols, summary_kpis):
with col:
st.markdown(kpi_card(label, value, delta, color), unsafe_allow_html=True)
# Table
st.subheader("📊 Positions")
st.markdown("<div style='height:1rem;'></div>", unsafe_allow_html=True)
# ---- Position cards ----
st.markdown(
'<div class="section-title">Open Positions <span class="accent">// live</span></div>',
unsafe_allow_html=True,
)
data = []
for p in positions:
pnl = (p["metrics"]["current_price"] - p["entry_price"]) * p.get("shares", 0)
data.append(
{
"Ticker": p["ticker"],
"Entry": f"${p['entry_price']:.2f}",
"Current": f"${p['metrics']['current_price']:.2f}",
"Shares": p.get("shares", 0),
"P/L": f"${pnl:.2f}",
"P/L %": f"{p['metrics']['current_return']:+.1f}%",
"Days": p["metrics"]["days_held"],
}
ticker = p["ticker"]
entry = p["entry_price"]
current = p["metrics"]["current_price"]
shares = p.get("shares", 0)
pnl_dollar = (current - entry) * shares
pnl_pct = p["metrics"]["current_return"]
days = p["metrics"]["days_held"]
color = pnl_color(pnl_pct)
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-left:3px solid {color};border-radius:8px;
padding:0.85rem 1.1rem;margin-bottom:0.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;align-items:center;gap:1rem;">
<span style="font-family:'JetBrains Mono',monospace;
font-weight:700;font-size:1.05rem;
color:{COLORS['text_primary']};">{ticker}</span>
<span style="font-family:'JetBrains Mono',monospace;
font-size:0.72rem;color:{COLORS['text_muted']};">
{shares} shares &middot; {days}d
</span>
</div>
<div style="text-align:right;">
<span style="font-family:'JetBrains Mono',monospace;
font-size:1rem;font-weight:700;color:{color};">
{pnl_pct:+.1f}%
</span>
<span style="font-family:'JetBrains Mono',monospace;
font-size:0.75rem;color:{COLORS['text_muted']};margin-left:0.5rem;">
${pnl_dollar:+,.0f}
</span>
</div>
</div>
<div style="display:flex;gap:2rem;margin-top:0.4rem;">
<span style="font-family:'JetBrains Mono',monospace;
font-size:0.72rem;color:{COLORS['text_muted']};">
Entry <span style="color:{COLORS['text_secondary']};">${entry:.2f}</span>
</span>
<span style="font-family:'JetBrains Mono',monospace;
font-size:0.72rem;color:{COLORS['text_muted']};">
Current <span style="color:{COLORS['text_secondary']};">${current:.2f}</span>
</span>
</div>
</div>
""",
unsafe_allow_html=True,
)
df = pd.DataFrame(data)
st.dataframe(df, use_container_width=True)
# ---- Data table fallback ----
with st.expander("Detailed Table"):
data = []
for p in positions:
pnl = (p["metrics"]["current_price"] - p["entry_price"]) * p.get("shares", 0)
data.append({
"Ticker": p["ticker"],
"Entry": p["entry_price"],
"Current": p["metrics"]["current_price"],
"Shares": p.get("shares", 0),
"P/L": pnl,
"P/L %": p["metrics"]["current_return"],
"Days": p["metrics"]["days_held"],
})
st.dataframe(
pd.DataFrame(data),
width="stretch",
hide_index=True,
column_config={
"Entry": st.column_config.NumberColumn(format="$%.2f"),
"Current": st.column_config.NumberColumn(format="$%.2f"),
"Shares": st.column_config.NumberColumn(format="%d"),
"P/L": st.column_config.NumberColumn(format="$%+.2f"),
"P/L %": st.column_config.NumberColumn(format="%+.1f%%"),
"Days": st.column_config.NumberColumn(format="%d"),
},
)

View File

@ -1,147 +1,155 @@
"""
Settings page for the Trading Agents Dashboard.
Config page displays pipeline configuration in a terminal-style layout.
This module displays configuration settings and scanner/pipeline status information.
It provides a read-only view of current settings with expandable sections for detailed configuration.
Read-only view of scanners, pipelines, and data source configuration.
"""
import streamlit as st
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.ui.theme import COLORS, page_header
def render() -> None:
"""
Render the settings page.
"""Render the configuration page."""
st.markdown(page_header("Config", "Pipeline & scanner configuration (read-only)"), unsafe_allow_html=True)
Displays:
- Page title
- Configuration info message
- Discovery configuration settings
- Pipelines section with expandable cards showing:
- enabled status
- priority
- deep_dive_budget
- Scanners section with checkboxes showing:
- enabled status for each scanner
"""
# Page title
st.title("⚙️ Settings")
# Info message
st.info("Configuration UI - TODO: Implement save functionality")
# Get configuration
config = DEFAULT_CONFIG
discovery_config = config.get("discovery", {})
# Display current configuration section
st.subheader("📋 Configuration")
# ---- Top-level settings ----
st.markdown(
'<div class="section-title">Discovery Settings <span class="accent">// core</span></div>',
unsafe_allow_html=True,
)
# Show key discovery settings
col1, col2, col3 = st.columns(3)
settings_grid = [
("Discovery Mode", discovery_config.get("discovery_mode", "N/A")),
("Max Candidates", str(discovery_config.get("max_candidates_to_analyze", "N/A"))),
("Final Recommendations", str(discovery_config.get("final_recommendations", "N/A"))),
("Deep Dive Workers", str(discovery_config.get("deep_dive_max_workers", "N/A"))),
]
with col1:
st.metric(
label="Discovery Mode",
value=discovery_config.get("discovery_mode", "N/A"),
cols = st.columns(len(settings_grid))
for col, (label, val) in zip(cols, settings_grid):
with col:
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-radius:8px;padding:0.75rem 1rem;text-align:center;">
<div style="font-family:'DM Sans',sans-serif;font-size:0.6rem;
font-weight:600;text-transform:uppercase;letter-spacing:0.06em;
color:{COLORS['text_muted']};margin-bottom:0.3rem;">{label}</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:1.1rem;
font-weight:600;color:{COLORS['text_primary']};">{val}</div>
</div>
""",
unsafe_allow_html=True,
)
st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
# ---- Pipelines ----
left_col, right_col = st.columns(2)
with left_col:
st.markdown(
'<div class="section-title">Pipelines <span class="accent">// routing</span></div>',
unsafe_allow_html=True,
)
with col2:
st.metric(
label="Max Candidates",
value=discovery_config.get("max_candidates_to_analyze", "N/A"),
pipelines = discovery_config.get("pipelines", {})
for name, cfg in pipelines.items():
enabled = cfg.get("enabled", False)
priority = cfg.get("priority", "N/A")
budget = cfg.get("deep_dive_budget", "N/A")
status_color = COLORS["green"] if enabled else COLORS["red"]
status_dot = f'<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:{status_color};margin-right:0.4rem;vertical-align:middle;"></span>'
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-radius:8px;padding:0.65rem 0.85rem;margin-bottom:0.4rem;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-family:'JetBrains Mono',monospace;font-size:0.85rem;
font-weight:600;color:{COLORS['text_primary']};">
{status_dot}{name}
</span>
<div style="display:flex;gap:0.75rem;">
<span style="font-family:'JetBrains Mono',monospace;font-size:0.7rem;
color:{COLORS['text_muted']};">P:{priority}</span>
<span style="font-family:'JetBrains Mono',monospace;font-size:0.7rem;
color:{COLORS['text_muted']};">B:{budget}</span>
</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
with right_col:
st.markdown(
'<div class="section-title">Scanners <span class="accent">// sources</span></div>',
unsafe_allow_html=True,
)
with col3:
st.metric(
label="Final Recommendations",
value=discovery_config.get("final_recommendations", "N/A"),
)
scanners = discovery_config.get("scanners", {})
for name, cfg in scanners.items():
enabled = cfg.get("enabled", False)
pipeline = cfg.get("pipeline", "N/A")
limit = cfg.get("limit", "N/A")
status_color = COLORS["green"] if enabled else COLORS["red"]
status_dot = f'<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:{status_color};margin-right:0.4rem;vertical-align:middle;"></span>'
# Pipelines section
st.subheader("🔄 Pipelines")
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-radius:8px;padding:0.55rem 0.85rem;margin-bottom:0.35rem;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-family:'JetBrains Mono',monospace;font-size:0.8rem;
font-weight:500;color:{COLORS['text_primary']};">
{status_dot}{name.replace('_', ' ')}
</span>
<div style="display:flex;gap:0.75rem;">
<span style="font-family:'DM Sans',sans-serif;font-size:0.6rem;
font-weight:600;text-transform:uppercase;
padding:0.1rem 0.35rem;border-radius:3px;
background:rgba(59,130,246,0.12);
color:{COLORS['blue']};letter-spacing:0.04em;">{pipeline}</span>
<span style="font-family:'JetBrains Mono',monospace;font-size:0.65rem;
color:{COLORS['text_muted']};">limit:{limit}</span>
</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
pipelines = discovery_config.get("pipelines", {})
if pipelines:
for pipeline_name, pipeline_config in pipelines.items():
with st.expander(
f"{'' if pipeline_config.get('enabled') else ''} {pipeline_name.title()}"
):
col1, col2, col3 = st.columns(3)
with col1:
st.metric(
label="Enabled",
value="Yes" if pipeline_config.get("enabled") else "No",
)
with col2:
st.metric(
label="Priority",
value=pipeline_config.get("priority", "N/A"),
)
with col3:
st.metric(
label="Budget",
value=pipeline_config.get("deep_dive_budget", "N/A"),
)
if "ranker_prompt" in pipeline_config:
st.caption(f"Ranker: {pipeline_config.get('ranker_prompt', 'N/A')}")
else:
st.info("No pipelines configured")
# Scanners section
st.subheader("🔍 Scanners")
scanners = discovery_config.get("scanners", {})
if scanners:
col1, col2 = st.columns([2, 1])
with col1:
st.write("**Scanner Status**")
with col2:
st.write("**Enabled**")
# Display each scanner with checkbox showing enabled status
for scanner_name, scanner_config in scanners.items():
col1, col2 = st.columns([2, 1])
with col1:
st.write(f"{scanner_name.replace('_', ' ').title()}")
with col2:
is_enabled = scanner_config.get("enabled", False)
st.write("" if is_enabled else "")
# Additional scanner configuration in expander
with st.expander("📊 Scanner Details"):
for scanner_name, scanner_config in scanners.items():
pipeline = scanner_config.get("pipeline", "N/A")
limit = scanner_config.get("limit", "N/A")
enabled = scanner_config.get("enabled", False)
st.write(
f"**{scanner_name}** | "
f"Pipeline: {pipeline} | "
f"Limit: {limit} | "
f"Status: {'✅ Enabled' if enabled else '❌ Disabled'}"
)
else:
st.info("No scanners configured")
# Data sources section
st.subheader("📡 Data Sources")
# ---- Data Sources ----
st.markdown("<div style='height:1.5rem;'></div>", unsafe_allow_html=True)
st.markdown(
'<div class="section-title">Data Sources <span class="accent">// vendors</span></div>',
unsafe_allow_html=True,
)
data_vendors = config.get("data_vendors", {})
if data_vendors:
for vendor_type, vendor_name in data_vendors.items():
st.write(f"**{vendor_type.replace('_', ' ').title()}**: {vendor_name}")
cols = st.columns(3)
for i, (vendor_type, vendor_name) in enumerate(data_vendors.items()):
with cols[i % 3]:
st.markdown(
f"""
<div style="background:{COLORS['bg_card']};border:1px solid {COLORS['border']};
border-radius:6px;padding:0.5rem 0.75rem;margin-bottom:0.35rem;">
<div style="font-family:'DM Sans',sans-serif;font-size:0.6rem;
color:{COLORS['text_muted']};text-transform:uppercase;
letter-spacing:0.04em;">{vendor_type.replace('_', ' ')}</div>
<div style="font-family:'JetBrains Mono',monospace;font-size:0.8rem;
color:{COLORS['text_primary']};font-weight:500;margin-top:0.15rem;">
{vendor_name}</div>
</div>
""",
unsafe_allow_html=True,
)
else:
st.info("No data sources configured")
st.info("No data sources configured.")

View File

@ -1,4 +1,9 @@
"""Today's recommendations."""
"""
Signals page today's recommendation cards with rich visual indicators.
Each signal is displayed as a data-dense card with strategy badges,
confidence bars, and expandable thesis sections.
"""
from datetime import datetime
@ -6,6 +11,7 @@ import pandas as pd
import plotly.express as px
import streamlit as st
from tradingagents.ui.theme import COLORS, get_plotly_template, page_header, signal_card
from tradingagents.ui.utils import load_recommendations
@ -17,13 +23,8 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame:
return pd.DataFrame()
data = download_history(
ticker,
period=period,
interval="1d",
auto_adjust=True,
progress=False,
ticker, period=period, interval="1d", auto_adjust=True, progress=False,
)
if data is None or data.empty:
return pd.DataFrame()
@ -42,100 +43,116 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame:
def render():
st.title("📋 Today's Recommendations")
today = datetime.now().strftime("%Y-%m-%d")
recommendations, meta = load_recommendations(today, return_meta=True)
display_date = meta.get("date", today) if meta else today
st.markdown(
page_header("Signals", f"Recommendations for {display_date}"),
unsafe_allow_html=True,
)
if not recommendations:
st.warning(f"No recommendations for {today}")
st.warning(f"No recommendations for {today}.")
return
if meta.get("is_fallback") and meta.get("date"):
st.info(f"No recommendations for {today}. Showing latest from {meta['date']}.")
st.info(f"Showing latest signals from **{meta['date']}** (none for today).")
show_charts = st.checkbox("Show price charts", value=True)
chart_window = st.selectbox(
"Price history window",
["1mo", "3mo", "6mo", "1y"],
index=1,
)
# Filters
col1, col2, col3 = st.columns(3)
with col1:
pipelines = list(
set(
(r.get("pipeline") or r.get("strategy_match") or "unknown") for r in recommendations
)
)
pipeline_filter = st.multiselect("Pipeline", pipelines, default=pipelines)
with col2:
min_confidence = st.slider("Min Confidence", 1, 10, 7)
with col3:
min_score = st.slider("Min Score", 0, 100, 70)
# ---- Controls row ----
ctrl_cols = st.columns([1, 1, 1, 1])
with ctrl_cols[0]:
pipelines = sorted(set(
(r.get("pipeline") or r.get("strategy_match") or "unknown") for r in recommendations
))
pipeline_filter = st.multiselect("Strategy", pipelines, default=pipelines)
with ctrl_cols[1]:
min_confidence = st.slider("Min Confidence", 1, 10, 1)
with ctrl_cols[2]:
min_score = st.slider("Min Score", 0, 100, 0)
with ctrl_cols[3]:
show_charts = st.checkbox("Price Charts", value=False)
if show_charts:
chart_window = st.selectbox("Window", ["1mo", "3mo", "6mo", "1y"], index=1)
else:
chart_window = "3mo"
# Apply filters
filtered = [
r
for r in recommendations
r for r in recommendations
if (r.get("pipeline") or r.get("strategy_match") or "unknown") in pipeline_filter
and r.get("confidence", 0) >= min_confidence
and r.get("final_score", 0) >= min_score
]
st.write(f"**{len(filtered)}** of **{len(recommendations)}** recommendations")
# ---- Summary bar ----
st.markdown(
f"""
<div style="display:flex;justify-content:space-between;align-items:center;
padding:0.5rem 0;margin-bottom:0.75rem;border-bottom:1px solid {COLORS['border']};">
<span style="font-family:'JetBrains Mono',monospace;font-size:0.8rem;
color:{COLORS['text_secondary']};">
Showing <span style="color:{COLORS['text_primary']};font-weight:700;">
{len(filtered)}</span> of {len(recommendations)} signals
</span>
<span style="font-family:'JetBrains Mono',monospace;font-size:0.7rem;
color:{COLORS['text_muted']};">
{display_date}
</span>
</div>
""",
unsafe_allow_html=True,
)
# Display recommendations
for i, rec in enumerate(filtered, 1):
ticker = rec.get("ticker", "UNKNOWN")
score = rec.get("final_score", 0)
confidence = rec.get("confidence", 0)
pipeline = (rec.get("pipeline") or rec.get("strategy_match") or "unknown").title()
scanner = rec.get("scanner") or rec.get("strategy_match") or "unknown"
entry_price = rec.get("entry_price")
current_price = rec.get("current_price")
# ---- Signal cards in 2-column grid ----
for i in range(0, len(filtered), 2):
cols = st.columns(2)
for j, col in enumerate(cols):
idx = i + j
if idx >= len(filtered):
break
rec = filtered[idx]
ticker = rec.get("ticker", "UNKNOWN")
rank = rec.get("rank", idx + 1)
score = rec.get("final_score", 0)
confidence = rec.get("confidence", 0)
strategy = (rec.get("pipeline") or rec.get("strategy_match") or "unknown").title()
entry_price = rec.get("entry_price", 0)
reason = rec.get("reason", "No thesis provided.")
with st.expander(
f"#{i} {ticker} - {rec.get('company_name', '')} (Score: {score}, Conf: {confidence}/10)"
):
col1, col2 = st.columns([2, 1])
with col:
st.markdown(
signal_card(rank, ticker, score, confidence, strategy, entry_price, reason),
unsafe_allow_html=True,
)
with col1:
st.write(f"**Pipeline:** {pipeline}")
st.write(f"**Scanner/Strategy:** {scanner}")
if entry_price is not None:
st.write(f"**Entry Price:** ${entry_price:.2f}")
if current_price is not None:
st.write(f"**Current Price:** ${current_price:.2f}")
st.write(f"**Thesis:** {rec.get('reason', 'N/A')}")
if show_charts:
history = _load_price_history(ticker, chart_window)
if history.empty:
st.caption("Price history unavailable.")
else:
last_close = history["close"].iloc[-1]
st.caption(f"Last close: ${last_close:.2f}")
fig = px.line(
history,
x="date",
y="close",
title=None,
labels={"date": "", "close": "Price"},
)
fig.update_traces(line=dict(color="#1f77b4", width=2))
fig.update_layout(
height=260,
margin=dict(l=10, r=10, t=10, b=10),
xaxis=dict(showgrid=False),
yaxis=dict(showgrid=True, gridcolor="rgba(0,0,0,0.08)"),
hovermode="x unified",
)
fig.update_yaxes(tickprefix="$")
st.plotly_chart(fig, use_container_width=True)
if not history.empty:
template = get_plotly_template()
fig = px.line(history, x="date", y="close", labels={"date": "", "close": "Price"})
with col2:
if st.button("✅ Enter Position", key=f"enter_{ticker}"):
st.info("Position entry modal (TODO)")
if st.button("👀 Watch", key=f"watch_{ticker}"):
st.success(f"Added {ticker} to watchlist")
# Color line green if trending up, red if down
first_close = history["close"].iloc[0]
last_close = history["close"].iloc[-1]
line_color = COLORS["green"] if last_close >= first_close else COLORS["red"]
fig.update_traces(line=dict(color=line_color, width=1.5))
fig.update_layout(
**template,
height=160,
showlegend=False,
)
fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
fig.update_xaxes(showticklabels=False, showgrid=False)
fig.update_yaxes(showgrid=True, gridcolor="rgba(42,53,72,0.3)", tickprefix="$")
st.plotly_chart(fig, width="stretch")
# Action buttons
btn_cols = st.columns(2)
with btn_cols[0]:
if st.button("Enter Position", key=f"enter_{ticker}_{idx}"):
st.info(f"Position entry for {ticker} (TODO)")
with btn_cols[1]:
if st.button("Watchlist", key=f"watch_{ticker}_{idx}"):
st.success(f"Added {ticker} to watchlist")